Skip to content

Commit

Permalink
refactor: split and reorder code (#56)
Browse files Browse the repository at this point in the history
  • Loading branch information
edeckers authored Mar 13, 2022
1 parent d7f8dd7 commit 8205f29
Show file tree
Hide file tree
Showing 36 changed files with 732 additions and 186 deletions.
5 changes: 4 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,7 @@ repos:
rev: 1.7.3
hooks:
- id: bandit

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.940
hooks:
- id: mypy
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ log_cli_date_format = "%Y-%m-%d %H:%M:%S"

[tool.coverage.report]
show_missing = true
fail_under = 85
fail_under = 90

[tool.coverage.html]
directory = "reports/coverage/html"
Expand Down
5 changes: 3 additions & 2 deletions src/huemon/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
import sys

from huemon.commands.command_handler import create_default_command_handler
from huemon.const import EXIT_OK
from huemon.infrastructure.bootstrapper import bootstrap
from huemon.infrastructure.config_factory import create_config
from huemon.infrastructure.logger_factory import bootstrap_logger
from huemon.util import exit_fail, get_command_plugins_path
from huemon.utils.const import EXIT_OK
from huemon.utils.errors import exit_fail
from huemon.utils.plugins import get_command_plugins_path

CONFIG = create_config()
LOG = bootstrap_logger(CONFIG)
Expand Down
2 changes: 1 addition & 1 deletion src/huemon/api/api_factory.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from huemon.api.api import Api
from huemon.api.cached_api import CachedApi
from huemon.const import DEFAULT_MAX_CACHE_AGE_SECONDS
from huemon.utils.const import DEFAULT_MAX_CACHE_AGE_SECONDS


def create_hue_hub_url(config: dict):
Expand Down
3 changes: 2 additions & 1 deletion src/huemon/api/cached_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@

from huemon.api.api_interface import ApiInterface
from huemon.infrastructure.logger_factory import create_logger
from huemon.util import cache_output_to_temp, run_locked
from huemon.utils.caching import cache_output_to_temp
from huemon.utils.threading import run_locked

LOG = create_logger()

Expand Down
2 changes: 1 addition & 1 deletion src/huemon/api_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from huemon.commands.command_handler import create_default_command_handler
from huemon.infrastructure.logger_factory import create_logger
from huemon.util import get_command_plugins_path
from huemon.utils.plugins import get_command_plugins_path

LOG = create_logger()

Expand Down
13 changes: 9 additions & 4 deletions src/huemon/commands/command_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,22 @@

import os
from functools import reduce
from typing import List, Type

from huemon.api.api_factory import create_api
from huemon.api.api_interface import ApiInterface
from huemon.commands.hue_command_interface import HueCommand
from huemon.infrastructure.logger_factory import create_logger
from huemon.plugin_loader import load_plugins
from huemon.util import create_local_path, exit_fail
from huemon.infrastructure.plugin_loader import load_plugins
from huemon.utils.errors import exit_fail
from huemon.utils.monads.either import rights
from huemon.utils.paths import create_local_path

LOG = create_logger()


def create_name_to_command_mapping(
config: dict, api: ApiInterface, plugins: list
config: dict, api: ApiInterface, plugins: List[Type[HueCommand]]
) -> dict:
return reduce(lambda p, c: {**p, c.name(): c(config, api)}, plugins, {})

Expand All @@ -27,10 +30,12 @@ def __load_command_plugins(config: dict, command_plugins_path: str = None) -> di
if not command_plugins_path:
return {}

command_plugins = rights(load_plugins("command", command_plugins_path, HueCommand))

command_handler_plugins = create_name_to_command_mapping(
config,
create_api(config),
load_plugins("command", command_plugins_path, HueCommand),
command_plugins,
)
LOG.debug("Finished loading command plugins (path=%s)", command_plugins_path)

Expand Down
5 changes: 5 additions & 0 deletions src/huemon/commands/hue_command_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@

from functools import reduce

from huemon.api.api_interface import ApiInterface


class HueCommand:
def __init__(self, config: dict, api: ApiInterface):
raise NotImplementedError("Command requires a constructor")

@staticmethod
def get_by_unique_id(unique_id: str, items: list) -> list:
return list(
Expand Down
4 changes: 2 additions & 2 deletions src/huemon/commands/internal/agent_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
# This source code is licensed under the MPL-2.0 license found in the
# LICENSE file in the root directory of this source tree.

import uvicorn
import uvicorn # type: ignore

from huemon.api.api_interface import ApiInterface
from huemon.api_server import HuemonServerFactory
from huemon.commands.hue_command_interface import HueCommand
from huemon.infrastructure.logger_factory import create_logger
from huemon.util import assert_exists, assert_num_args
from huemon.utils.assertions import assert_exists, assert_num_args

LOG = create_logger()

Expand Down
38 changes: 23 additions & 15 deletions src/huemon/commands/internal/discover_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,25 @@

import os
from functools import reduce
from typing import Dict, List, Type

from huemon.api.api_interface import ApiInterface
from huemon.commands.hue_command_interface import HueCommand
from huemon.discoveries.discovery_interface import Discovery
from huemon.infrastructure.logger_factory import create_logger
from huemon.plugin_loader import load_plugins
from huemon.util import (
assert_exists,
assert_num_args,
create_local_path,
get_discovery_plugins_path,
)
from huemon.infrastructure.plugin_loader import load_plugins
from huemon.utils.assertions import assert_exists, assert_num_args
from huemon.utils.monads.either import Either, rights
from huemon.utils.monads.maybe import Maybe, maybe, of
from huemon.utils.paths import create_local_path
from huemon.utils.plugins import get_discovery_plugins_path

LOG = create_logger()


def create_discovery_handlers(api: ApiInterface, plugins: list):
def create_discovery_handlers(
api: ApiInterface, plugins: List[Type[Discovery]]
) -> Dict[str, Discovery]:
return reduce(lambda p, c: {**p, c.name(): c(api)}, plugins, {})


Expand Down Expand Up @@ -60,7 +62,11 @@ def __create_discovery_handler(self):
LOG.debug("Loading discovery plugins (path=%s)", self.discovery_plugins_path)
discovery_handler_plugins = create_discovery_handlers(
self.api,
Discover.__load_plugins_and_hardwired_handlers(self.discovery_plugins_path),
rights(
Discover.__load_plugins_and_hardwired_handlers(
of(self.discovery_plugins_path)
)
),
)
LOG.debug(
"Finished loading discovery plugins (path=%s)", self.discovery_plugins_path
Expand All @@ -70,26 +76,28 @@ def __create_discovery_handler(self):

@staticmethod
def __load_discovery_plugin(path: str):
return [] if not path else load_plugins("discovery", path, Discovery)
return load_plugins("discovery", path, Discovery)

@staticmethod
def __load_plugins_and_hardwired_handlers(
discovery_plugins_path: str = None,
) -> list:
discovery_plugins_path: Maybe[str],
) -> List[Either[str, Type[Discovery]]]:
hardwired_discoveries_path = create_local_path(
os.path.join("discoveries", "internal")
)

return Discover.__load_discovery_plugin(
discovery_plugins_path
return maybe(
[], Discover.__load_discovery_plugin, discovery_plugins_path
) + Discover.__load_discovery_plugin(hardwired_discoveries_path)

def discover(self, discovery_type):
self.handler.exec(discovery_type)


class DiscoverCommand(HueCommand):
def __init__(self, config: dict, api: ApiInterface):
def __init__(
self, config: dict, api: ApiInterface
): # pylint: disable=super-init-not-called
self.discovery = Discover(config, api)

@staticmethod
Expand Down
2 changes: 1 addition & 1 deletion src/huemon/commands/internal/light_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from huemon.api.api_interface import ApiInterface
from huemon.commands.hue_command_interface import HueCommand
from huemon.infrastructure.logger_factory import create_logger
from huemon.util import assert_exists, assert_num_args
from huemon.utils.assertions import assert_exists, assert_num_args

LOG = create_logger()

Expand Down
2 changes: 1 addition & 1 deletion src/huemon/commands/internal/sensor_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from huemon.api.api_interface import ApiInterface
from huemon.commands.hue_command_interface import HueCommand
from huemon.infrastructure.logger_factory import create_logger
from huemon.util import assert_exists, assert_num_args
from huemon.utils.assertions import assert_exists, assert_num_args

LOG = create_logger()

Expand Down
2 changes: 1 addition & 1 deletion src/huemon/commands/internal/system_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from huemon.api.api_interface import ApiInterface
from huemon.commands.hue_command_interface import HueCommand
from huemon.infrastructure.logger_factory import create_logger
from huemon.util import assert_exists, assert_num_args
from huemon.utils.assertions import assert_exists, assert_num_args

LOG = create_logger()

Expand Down
Empty file.
5 changes: 5 additions & 0 deletions src/huemon/discoveries/discovery_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@
import json
from functools import reduce

from huemon.api.api_interface import ApiInterface


class Discovery:
def __init__(self, api: ApiInterface):
raise NotImplementedError("Discoveries is missing its required constructor")

@staticmethod
def _item_to_discovery(item: dict):
return {
Expand Down
2 changes: 1 addition & 1 deletion src/huemon/discoveries/internal/sensors_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from huemon.api.api_interface import ApiInterface
from huemon.discoveries.discovery_interface import Discovery
from huemon.infrastructure.logger_factory import create_logger
from huemon.util import exit_fail
from huemon.utils.errors import exit_fail

LOG = create_logger()

Expand Down
2 changes: 1 addition & 1 deletion src/huemon/infrastructure/config_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import yaml
from genericpath import isfile

from huemon.util import exit_fail
from huemon.utils.errors import exit_fail

CONFIG_PATH_LOCAL = path.join(str(Path(__file__).parent.parent), "config.yml")
CONFIG_PATH_ENV_VARIABLE = environ.get("HUEMON_CONFIG_PATH")
Expand Down
88 changes: 88 additions & 0 deletions src/huemon/infrastructure/plugin_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Copyright (c) Ely Deckers.
#
# This source code is licensed under the MPL-2.0 license found in the
# LICENSE file in the root directory of this source tree.

import importlib.util
import inspect
from pathlib import Path
from typing import List, Tuple, Type, TypeVar, cast

from huemon.utils.errors import E_CODE_PLUGIN_LOADER, HueError
from huemon.utils.monads.either import Either, Left, right
from huemon.utils.monads.maybe import Maybe

TA = TypeVar("TA")


def __error(message: str) -> HueError:
return HueError(E_CODE_PLUGIN_LOADER, message)


def __lerror(message: str) -> Left[HueError, TA]:
return Left[HueError, TA](__error(message))


def __get_plugin_type(
module_name: str, path: str, sub_class: Type[TA]
) -> Either[HueError, Type[TA]]:
return (
Maybe.of(importlib.util.spec_from_file_location(module_name, path))
.maybe(
__lerror("ModuleSpec could not be loaded from the provided path"),
right,
)
.bind(
lambda spec: Maybe.of(spec.loader).maybe(
__lerror("ModuleSpec has no loader"), lambda _: right(spec)
)
)
.bind(
lambda spec: Maybe.of(importlib.util.module_from_spec(spec)).maybe(
__lerror("Module could not be loaded from ModuleSpec"),
lambda module: right((spec, module)),
),
)
.discard(
lambda sm: Either.pure(
Maybe.of(sm[0].loader).bind(
lambda loader: Maybe.of(loader.exec_module(sm[1]))
)
)
)
.fmap(lambda sm: sm[1])
.fmap(
lambda module: list(
map(
lambda obj: cast(Tuple[str, Type[TA]], obj),
filter(
lambda member: inspect.isclass(member[1])
and issubclass(member[1], sub_class)
and member[1] is not sub_class,
inspect.getmembers(module),
),
),
),
)
.bind(
lambda plugin_types: Maybe.of(len(plugin_types) > 0).maybe(
__lerror(f"No plugin of type `{sub_class}` found"),
lambda _: right(plugin_types[0][1]),
)
)
)


def load_plugins(
module_name: str, path: str, plugin_type: Type[TA]
) -> List[Either[HueError, Type[TA]]]:
return list(
map(
lambda plugin_path: __get_plugin_type(
f"{module_name}.{plugin_path.stem}",
str(plugin_path.absolute()),
plugin_type,
),
Path(path).glob("*.py"),
)
)
Loading

0 comments on commit 8205f29

Please sign in to comment.