diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0655bdb..c5feca8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,3 +27,4 @@ repos: rev: v0.940 hooks: - id: mypy + additional_dependencies: [types-PyYAML] diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..656e947 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Module", + "type": "python", + "request": "launch", + "module": "huemon", + "args": ["discover", "lights"], + "justMyCode": true + } + ] +} \ No newline at end of file diff --git a/src/huemon/__main__.py b/src/huemon/__main__.py index 2aebe8b..c75ff48 100755 --- a/src/huemon/__main__.py +++ b/src/huemon/__main__.py @@ -9,6 +9,7 @@ from huemon.infrastructure.bootstrapper import bootstrap from huemon.infrastructure.config_factory import create_config from huemon.infrastructure.logger_factory import bootstrap_logger +from huemon.sinks.stdout_sink import StdoutSink from huemon.utils.const import EXIT_OK from huemon.utils.errors import exit_fail from huemon.utils.plugins import get_command_plugins_path @@ -24,7 +25,7 @@ def main(argv): bootstrap() command_handler = create_default_command_handler( - CONFIG, get_command_plugins_path(CONFIG) + CONFIG, StdoutSink(), get_command_plugins_path(CONFIG) ) if len(argv) <= 1: diff --git a/src/huemon/api_server.py b/src/huemon/api_server.py index d020b32..94a5b5b 100644 --- a/src/huemon/api_server.py +++ b/src/huemon/api_server.py @@ -12,6 +12,7 @@ from huemon.commands.command_handler import create_default_command_handler from huemon.infrastructure.logger_factory import create_logger +from huemon.sinks.stdout_sink import StdoutSink from huemon.utils.plugins import get_command_plugins_path LOG = create_logger() @@ -47,7 +48,7 @@ def create(config: dict) -> FastAPI: app = FastAPI() command_handler = create_default_command_handler( - config, get_command_plugins_path(config) + config, StdoutSink(), get_command_plugins_path(config) ) for command_name in command_handler.available_commands(): diff --git a/src/huemon/commands/command_handler.py b/src/huemon/commands/command_handler.py index 06ae9fa..13f7b36 100644 --- a/src/huemon/commands/command_handler.py +++ b/src/huemon/commands/command_handler.py @@ -12,6 +12,7 @@ from huemon.commands.hue_command_interface import HueCommand from huemon.infrastructure.logger_factory import create_logger from huemon.infrastructure.plugin_loader import load_plugins +from huemon.sinks.sink_interface import SinkInterface from huemon.utils.errors import exit_fail from huemon.utils.monads.either import rights from huemon.utils.paths import create_local_path @@ -20,12 +21,17 @@ def create_name_to_command_mapping( - config: dict, api: ApiInterface, plugins: List[Type[HueCommand]] + config: dict, + api: ApiInterface, + processor: SinkInterface, + plugins: List[Type[HueCommand]], ) -> dict: - return reduce(lambda p, c: {**p, c.name(): c(config, api)}, plugins, {}) + return reduce(lambda p, c: {**p, c.name(): c(config, api, processor)}, plugins, {}) -def __load_command_plugins(config: dict, command_plugins_path: str = None) -> dict: +def __load_command_plugins( + config: dict, processor: SinkInterface, command_plugins_path: str = None +) -> dict: LOG.debug("Loading command plugins (path=%s)", command_plugins_path) if not command_plugins_path: return {} @@ -35,6 +41,7 @@ def __load_command_plugins(config: dict, command_plugins_path: str = None) -> di command_handler_plugins = create_name_to_command_mapping( config, create_api(config), + processor, command_plugins, ) LOG.debug("Finished loading command plugins (path=%s)", command_plugins_path) @@ -43,19 +50,21 @@ def __load_command_plugins(config: dict, command_plugins_path: str = None) -> di def __load_plugins_and_hardwired_handlers( - config: dict, command_plugins_path: str = None + config: dict, processor: SinkInterface, command_plugins_path: str = None ) -> dict: hardwired_commands_path = create_local_path(os.path.join("commands", "internal")) return { - **__load_command_plugins(config, command_plugins_path), - **__load_command_plugins(config, hardwired_commands_path), + **__load_command_plugins(config, processor, command_plugins_path), + **__load_command_plugins(config, processor, hardwired_commands_path), } -def create_default_command_handler(config: dict, command_plugins_path: str): +def create_default_command_handler( + config: dict, processor: SinkInterface, command_plugins_path: str = None +): return CommandHandler( - __load_plugins_and_hardwired_handlers(config, command_plugins_path) + __load_plugins_and_hardwired_handlers(config, processor, command_plugins_path) ) diff --git a/src/huemon/commands/hue_command_interface.py b/src/huemon/commands/hue_command_interface.py index bef84b5..28fd401 100644 --- a/src/huemon/commands/hue_command_interface.py +++ b/src/huemon/commands/hue_command_interface.py @@ -6,11 +6,14 @@ from functools import reduce from huemon.api.api_interface import ApiInterface +from huemon.sinks.sink_interface import SinkInterface class HueCommand: - def __init__(self, config: dict, api: ApiInterface): - raise NotImplementedError("Command requires a constructor") + def __init__(self, config: dict, api: ApiInterface, processor: SinkInterface): + self.config = config + self.api = api + self.processor = processor @staticmethod def get_by_unique_id(unique_id: str, items: list) -> list: @@ -20,10 +23,6 @@ def get_by_unique_id(unique_id: str, items: list) -> list: ) )[0] - @staticmethod - def _process(value): - print(value) - @staticmethod def _mapper(path: str, value_type): return lambda value: value_type( @@ -34,5 +33,8 @@ def _mapper(path: str, value_type): def name(): pass + def _process(self, value): + self.processor.process(value) + def exec(self, arguments): pass diff --git a/src/huemon/commands/internal/agent_command.py b/src/huemon/commands/internal/agent_command.py index bb1d82e..7759cb7 100644 --- a/src/huemon/commands/internal/agent_command.py +++ b/src/huemon/commands/internal/agent_command.py @@ -5,11 +5,10 @@ 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.utils.assertions import assert_exists, assert_num_args +from huemon.utils.assertions import assert_exists_e, assert_num_args_e LOG = create_logger() @@ -38,24 +37,23 @@ class AgentCommand(HueCommand): "start": MyServer.start, } - def __init__( - self, config: dict, _: ApiInterface - ): # pylint: disable=unused-argument - self.config = config - @staticmethod def name(): return "agent" def exec(self, arguments): LOG.debug("Running `%s` command (arguments=%s)", AgentCommand.name(), arguments) - assert_num_args(1, arguments, AgentCommand.name()) - - action, *_ = arguments - assert_exists(list(AgentCommand.__SYSTEM_ACTION_MAP), action) + self._process( + assert_num_args_e(1, arguments, AgentCommand.name()) + .bind( + lambda ax: assert_exists_e( + list(AgentCommand.__SYSTEM_ACTION_MAP), ax[0] + ) + ) + .fmap(lambda action: self.__SYSTEM_ACTION_MAP[action](self.config)) + ) - HueCommand._process(self.__SYSTEM_ACTION_MAP[action](self.config)) LOG.debug( "Finished `%s` command (arguments=%s)", AgentCommand.name(), arguments ) diff --git a/src/huemon/commands/internal/discover_command.py b/src/huemon/commands/internal/discover_command.py index 7ed4b73..4117bd8 100644 --- a/src/huemon/commands/internal/discover_command.py +++ b/src/huemon/commands/internal/discover_command.py @@ -5,14 +5,16 @@ import os from functools import reduce -from typing import Dict, List, Type +from typing import Dict, List, Type, TypeVar 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.infrastructure.plugin_loader import load_plugins -from huemon.utils.assertions import assert_exists, assert_num_args +from huemon.sinks.sink_interface import SinkInterface +from huemon.utils.assertions import assert_exists_e, assert_num_args_e +from huemon.utils.common import fst 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 @@ -20,11 +22,14 @@ LOG = create_logger() +TA = TypeVar("TA") +TB = TypeVar("TB") + def create_discovery_handlers( - api: ApiInterface, plugins: List[Type[Discovery]] + api: ApiInterface, sink: SinkInterface, plugins: List[Type[Discovery]] ) -> Dict[str, Discovery]: - return reduce(lambda p, c: {**p, c.name(): c(api)}, plugins, {}) + return reduce(lambda p, c: {**p, c.name(): c(api, sink)}, plugins, {}) class DiscoveryHandler: # pylint: disable=too-few-public-methods @@ -39,9 +44,9 @@ def exec(self, discovery_type): ) target, maybe_sub_target, *_ = discovery_type.split(":") + [None] - assert_exists(list(self.handlers), target) - - self.handlers[target].exec([maybe_sub_target] if maybe_sub_target else []) + assert_exists_e(list(self.handlers), target).fmap( + lambda tx: self.handlers[tx] + ).fmap(lambda hlr: hlr.exec([maybe_sub_target] if maybe_sub_target else [])) LOG.debug( "Finished `%s` command (discovery_type=%s)", @@ -51,17 +56,16 @@ def exec(self, discovery_type): class Discover: # pylint: disable=too-few-public-methods - def __init__(self, config: dict, api: ApiInterface): - self.api = api - + def __init__(self, config: dict, api: ApiInterface, sink: SinkInterface): self.discovery_plugins_path = get_discovery_plugins_path(config) - self.handler = self.__create_discovery_handler() + self.handler = self.__create_discovery_handler(api, sink) - def __create_discovery_handler(self): + def __create_discovery_handler(self, api: ApiInterface, sink: SinkInterface): LOG.debug("Loading discovery plugins (path=%s)", self.discovery_plugins_path) discovery_handler_plugins = create_discovery_handlers( - self.api, + api, + sink, rights( Discover.__load_plugins_and_hardwired_handlers( of(self.discovery_plugins_path) @@ -95,22 +99,30 @@ def discover(self, discovery_type): class DiscoverCommand(HueCommand): - def __init__( - self, config: dict, api: ApiInterface - ): # pylint: disable=super-init-not-called - self.discovery = Discover(config, api) + def __init__(self, config: dict, api: ApiInterface, processor: SinkInterface): + super().__init__(config, api, processor) + + self.discovery = Discover(config, api, processor) @staticmethod def name(): return "discover" - def exec(self, arguments): + def exec(self, arguments: List[str]): LOG.debug( "Running `%s` command (arguments=%s)", DiscoverCommand.name(), arguments ) - assert_num_args(1, arguments, DiscoverCommand.name()) - discovery_type, *_ = arguments + error, param = assert_num_args_e(1, arguments, DiscoverCommand.name()).fmap( + fst # type: ignore + ) - self.discovery.discover(discovery_type) - LOG.debug("Finished `discover` command (arguments=%s)", arguments) + if error: + self.processor.process(error) + return + + self.discovery.discover(param) + + LOG.debug( + "Finished `%s` command (arguments=%s)", DiscoverCommand.name(), arguments + ) diff --git a/src/huemon/commands/internal/light_command.py b/src/huemon/commands/internal/light_command.py index afa103e..2d06e4d 100644 --- a/src/huemon/commands/internal/light_command.py +++ b/src/huemon/commands/internal/light_command.py @@ -3,20 +3,14 @@ # This source code is licensed under the MPL-2.0 license found in the # LICENSE file in the root directory of this source tree. -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.utils.assertions import assert_exists, assert_num_args +from huemon.utils.assertions import assert_exists_e, assert_num_args_e LOG = create_logger() class LightCommand(HueCommand): - def __init__( - self, config: dict, api: ApiInterface - ): # pylint: disable=unused-argument - self.api = api - __LIGHT_ACTION_MAP = { "is_upgrade_available": lambda light: int( light["swupdate"]["state"] != "noupdates" @@ -38,13 +32,13 @@ def name(): def exec(self, arguments): LOG.debug("Running `%s` command (arguments=%s)", LightCommand.name(), arguments) - assert_num_args(2, arguments, LightCommand.name()) + assert_num_args_e(2, arguments, LightCommand.name()) light_id, action = arguments - assert_exists(list(LightCommand.__LIGHT_ACTION_MAP), action) + assert_exists_e(list(LightCommand.__LIGHT_ACTION_MAP), action) - HueCommand._process( + self._process( self.__map_light(light_id, LightCommand.__LIGHT_ACTION_MAP[action]) ) diff --git a/src/huemon/commands/internal/sensor_command.py b/src/huemon/commands/internal/sensor_command.py index e52ceb3..1a32569 100644 --- a/src/huemon/commands/internal/sensor_command.py +++ b/src/huemon/commands/internal/sensor_command.py @@ -3,7 +3,6 @@ # This source code is licensed under the MPL-2.0 license found in the # LICENSE file in the root directory of this source tree. -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.utils.assertions import assert_exists, assert_num_args @@ -12,11 +11,6 @@ class SensorCommand(HueCommand): - def __init__( - self, config: dict, api: ApiInterface - ): # pylint: disable=unused-argument - self.api = api - def __get_sensor(self, device_id): return HueCommand.get_by_unique_id(device_id, self.api.get_sensors()) @@ -45,7 +39,7 @@ def exec(self, arguments): assert_exists(list(SensorCommand.__SENSOR_ACTION_MAP), action) - HueCommand._process( + self._process( self.__map_sensor(device_id, SensorCommand.__SENSOR_ACTION_MAP[action]) ) LOG.debug( diff --git a/src/huemon/commands/internal/system_command.py b/src/huemon/commands/internal/system_command.py index 5fccd5b..692010b 100644 --- a/src/huemon/commands/internal/system_command.py +++ b/src/huemon/commands/internal/system_command.py @@ -3,7 +3,6 @@ # This source code is licensed under the MPL-2.0 license found in the # LICENSE file in the root directory of this source tree. -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.utils.assertions import assert_exists, assert_num_args @@ -12,11 +11,6 @@ class SystemCommand(HueCommand): - def __init__( - self, config: dict, api: ApiInterface - ): # pylint: disable=unused-argument - self.api = api - def __map_config(self, mapper): return mapper(self.api.get_system_config()) @@ -41,7 +35,7 @@ def exec(self, arguments): assert_exists(list(SystemCommand.__SYSTEM_ACTION_MAP), action) - HueCommand._process(self.__map_config(self.__SYSTEM_ACTION_MAP[action])) + self._process(self.__map_config(self.__SYSTEM_ACTION_MAP[action])) LOG.debug( "Finished `%s` command (arguments=%s)", SystemCommand.name(), arguments ) diff --git a/src/huemon/discoveries/discovery_interface.py b/src/huemon/discoveries/discovery_interface.py index 4f6e06a..bad643e 100644 --- a/src/huemon/discoveries/discovery_interface.py +++ b/src/huemon/discoveries/discovery_interface.py @@ -5,13 +5,17 @@ import json from functools import reduce +from typing import List from huemon.api.api_interface import ApiInterface +from huemon.sinks.sink_interface import SinkInterface class Discovery: - def __init__(self, api: ApiInterface): - raise NotImplementedError("Discoveries is missing its required constructor") + def __init__(self, api: ApiInterface, sink: SinkInterface): + self.api = api + + self.sink = sink @staticmethod def _item_to_discovery(item: dict): @@ -29,18 +33,18 @@ def _has_state_field(field: str): ) @staticmethod - def _print_array_as_discovery(items): - print( - json.dumps( - { - "data": reduce( - lambda p, item: [*p, Discovery._item_to_discovery(item)], - items, - [], - ) - } + def _array_as_discovery(items: List[dict]): + return { + "data": reduce( + lambda p, item: [*p, Discovery._item_to_discovery(item)], + items, + [], ) - ) + } + + @staticmethod + def _print_array_as_discovery(items: List[dict]): + print(json.dumps(Discovery._array_as_discovery(items))) @staticmethod def name(): diff --git a/src/huemon/discoveries/internal/batteries_discovery.py b/src/huemon/discoveries/internal/batteries_discovery.py index 108255a..5d4e800 100644 --- a/src/huemon/discoveries/internal/batteries_discovery.py +++ b/src/huemon/discoveries/internal/batteries_discovery.py @@ -3,17 +3,16 @@ # This source code is licensed under the MPL-2.0 license found in the # LICENSE file in the root directory of this source tree. -from huemon.api.api_interface import ApiInterface from huemon.discoveries.discovery_interface import Discovery +from huemon.utils.monads.either import right class BatteriesDiscovery(Discovery): - def __init__(self, api: ApiInterface): - self.api = api - @staticmethod def name(): return "batteries" def exec(self, arguments=None): - Discovery._print_array_as_discovery(self.api.get_batteries()) + self.sink.process( + right(Discovery._array_as_discovery(self.api.get_batteries())) + ) diff --git a/src/huemon/discoveries/internal/lights_discovery.py b/src/huemon/discoveries/internal/lights_discovery.py index ec6ae24..83624a8 100644 --- a/src/huemon/discoveries/internal/lights_discovery.py +++ b/src/huemon/discoveries/internal/lights_discovery.py @@ -3,17 +3,14 @@ # This source code is licensed under the MPL-2.0 license found in the # LICENSE file in the root directory of this source tree. -from huemon.api.api_interface import ApiInterface from huemon.discoveries.discovery_interface import Discovery +from huemon.utils.monads.either import right class LightsDiscovery(Discovery): - def __init__(self, api: ApiInterface): - self.api = api - @staticmethod def name(): return "lights" def exec(self, arguments=None): - Discovery._print_array_as_discovery(self.api.get_lights()) + self.sink.process(right(Discovery._array_as_discovery(self.api.get_lights()))) diff --git a/src/huemon/discoveries/internal/sensors_discovery.py b/src/huemon/discoveries/internal/sensors_discovery.py index 5ef249a..133455f 100644 --- a/src/huemon/discoveries/internal/sensors_discovery.py +++ b/src/huemon/discoveries/internal/sensors_discovery.py @@ -3,7 +3,6 @@ # This source code is licensed under the MPL-2.0 license found in the # LICENSE file in the root directory of this source tree. -from huemon.api.api_interface import ApiInterface from huemon.discoveries.discovery_interface import Discovery from huemon.infrastructure.logger_factory import create_logger from huemon.utils.errors import exit_fail @@ -18,9 +17,6 @@ class SensorsDiscovery(Discovery): - def __init__(self, api: ApiInterface): - self.api = api - @staticmethod def name(): return "sensors" diff --git a/src/huemon/infrastructure/config_factory.py b/src/huemon/infrastructure/config_factory.py index 2213db5..b693bc1 100644 --- a/src/huemon/infrastructure/config_factory.py +++ b/src/huemon/infrastructure/config_factory.py @@ -10,6 +10,7 @@ from genericpath import isfile from huemon.utils.errors import exit_fail +from huemon.utils.monads.maybe import Maybe, nothing CONFIG_PATH_LOCAL = path.join(str(Path(__file__).parent.parent), "config.yml") CONFIG_PATH_ENV_VARIABLE = environ.get("HUEMON_CONFIG_PATH") @@ -25,21 +26,25 @@ ) -def __first_existing_config_file(): +def __first_existing_config_file() -> Maybe[str]: for config_path in CONFIG_PATHS_ORDERED_PREFERENCE: if isfile(config_path): - return config_path + return Maybe.of(config_path) - return None + return nothing -def create_config(): - maybe_config_path = __first_existing_config_file() - if not maybe_config_path: +def __read_yaml_file(yaml_path: str): + with open(yaml_path, "r") as file: + return yaml.safe_load(file.read()) + + +def create_config() -> dict: + maybe_config_file = __first_existing_config_file() + if maybe_config_file.is_nothing(): exit_fail( "No configuration file found in %s", ",".join(CONFIG_PATHS_ORDERED_PREFERENCE), ) - with open(maybe_config_path, "r") as file: - return yaml.safe_load(file.read()) + return __read_yaml_file(maybe_config_file.from_just()) diff --git a/src/huemon/infrastructure/plugin_loader.py b/src/huemon/infrastructure/plugin_loader.py index 055cc42..8441ebc 100644 --- a/src/huemon/infrastructure/plugin_loader.py +++ b/src/huemon/infrastructure/plugin_loader.py @@ -5,70 +5,82 @@ import importlib.util import inspect +from importlib.machinery import ModuleSpec from pathlib import Path +from types import ModuleType 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.either import Either, left, right from huemon.utils.monads.maybe import Maybe TA = TypeVar("TA") +TB = TypeVar("TB") 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 __lerror(message: str) -> Either[HueError, TB]: + return left(__error(message)) + + +def __filter_subclasses_from_module(sub_class): + return ( + lambda member: inspect.isclass(member) + and issubclass(member, sub_class) + and member is not sub_class + ) + + +def __read_members_from_module(sub_class): + return lambda module: list( + map( + lambda obj: cast(Tuple[str, Type[TA]], obj), + inspect.getmembers(module, __filter_subclasses_from_module(sub_class)), + ), + ) + + +def __load_module(spec_and_module: Tuple[ModuleSpec, ModuleType]): + spec, module = spec_and_module + + Maybe.of(spec.loader).fmap(lambda loader: loader.exec_module(module)) + + return Either.pure(module) 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), - ), - ), - ), + + maybe_spec = Maybe.of(importlib.util.spec_from_file_location(module_name, path)) + + error_or_spec: Either[HueError, ModuleSpec] = maybe_spec.maybe( + __lerror("ModuleSpec could not be loaded from the provided path"), + right, # type: ignore + ).bind( + lambda spec: Maybe.of(spec.loader).maybe( # type: ignore + __lerror("ModuleSpec has no loader"), lambda _: right(spec) ) - .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]), - ) + ) + + error_or_module: Either[ + HueError, Tuple[ModuleSpec, ModuleType] + ] = error_or_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)), # type: ignore + ), + ).bind( + __load_module + ) + + return error_or_module.fmap(__read_members_from_module(sub_class)).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]), ) ) diff --git a/src/huemon/sinks/__init__.py b/src/huemon/sinks/__init__.py new file mode 100644 index 0000000..e42426a --- /dev/null +++ b/src/huemon/sinks/__init__.py @@ -0,0 +1,4 @@ +# 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. diff --git a/src/huemon/sinks/sink_interface.py b/src/huemon/sinks/sink_interface.py new file mode 100644 index 0000000..256360d --- /dev/null +++ b/src/huemon/sinks/sink_interface.py @@ -0,0 +1,16 @@ +# 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. + +from typing import Generic, TypeVar + +from huemon.utils.errors import HueError +from huemon.utils.monads.either import Either + +TA = TypeVar("TA") + + +class SinkInterface(Generic[TA]): # pylint: disable=too-few-public-methods + def process(self, value: Either[HueError, TA]): + raise NotImplementedError("Processor process implementation missing") diff --git a/src/huemon/sinks/stdout_sink.py b/src/huemon/sinks/stdout_sink.py new file mode 100644 index 0000000..9857ddb --- /dev/null +++ b/src/huemon/sinks/stdout_sink.py @@ -0,0 +1,22 @@ +# 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 json +import sys +from typing import TypeVar + +from huemon.sinks.sink_interface import SinkInterface +from huemon.utils.errors import HueError +from huemon.utils.monads.either import Either + +TA = TypeVar("TA") + + +class StdoutSink(SinkInterface): # pylint: disable=too-few-public-methods + def process(self, value: Either[HueError, TA]): + print(value.either(lambda error: error.message, json.dumps)) + + if value.is_left(): + sys.exit(2) diff --git a/src/huemon/utils/assertions.py b/src/huemon/utils/assertions.py index cb6a0d2..4170b74 100644 --- a/src/huemon/utils/assertions.py +++ b/src/huemon/utils/assertions.py @@ -3,7 +3,15 @@ # This source code is licensed under the MPL-2.0 license found in the # LICENSE file in the root directory of this source tree. -from huemon.utils.errors import exit_fail +from typing import List, Sequence + +from huemon.utils.errors import ( + E_CODE_ASSERT_EXISTS, + E_CODE_ASSERT_NUM_ARGS, + HueError, + exit_fail, +) +from huemon.utils.monads.either import Either, left, right def assert_num_args(expected_number_of_arguments: int, arguments: list, context: str): @@ -19,6 +27,26 @@ def assert_num_args(expected_number_of_arguments: int, arguments: list, context: ) +def assert_num_args_e( + expected_number_of_arguments: int, arguments: List[str], context: str +) -> Either[HueError, Sequence[str]]: + try: + assert_num_args(expected_number_of_arguments, arguments, context) + + return right(arguments) + except SystemExit: + return left(HueError(E_CODE_ASSERT_NUM_ARGS, "FIXME_NUM_ARGS")) + + def assert_exists(expected_values: list, value: str): if value not in expected_values: exit_fail("Received unknown value `%s` (expected=%s)", value, expected_values) + + +def assert_exists_e(expected_values: list, value: str): + try: + assert_exists(expected_values, value) + + return right(value) + except SystemExit: + return left(HueError(E_CODE_ASSERT_EXISTS, "FIXME_EXISTS")) diff --git a/src/huemon/utils/common.py b/src/huemon/utils/common.py new file mode 100644 index 0000000..f334596 --- /dev/null +++ b/src/huemon/utils/common.py @@ -0,0 +1,11 @@ +from typing import Sequence, TypeVar + +TS = TypeVar("TS") + + +def fst(seq: Sequence[TS]) -> TS: + return seq[0] + + +def snd(seq: Sequence[TS]) -> TS: + return seq[1] diff --git a/src/huemon/utils/errors.py b/src/huemon/utils/errors.py index 504175a..1b3f359 100644 --- a/src/huemon/utils/errors.py +++ b/src/huemon/utils/errors.py @@ -12,6 +12,10 @@ LOG = create_logger() E_CODE_PLUGIN_LOADER = -2 +E_CODE_CONFIG_NOT_FOUND = -3 + +E_CODE_ASSERT_NUM_ARGS = -4 +E_CODE_ASSERT_EXISTS = -5 class HueError(NamedTuple): diff --git a/src/huemon/utils/monads/either.py b/src/huemon/utils/monads/either.py index 375216f..655c41f 100644 --- a/src/huemon/utils/monads/either.py +++ b/src/huemon/utils/monads/either.py @@ -11,20 +11,37 @@ TA = TypeVar("TA") TB = TypeVar("TB") TC = TypeVar("TC") -TD = TypeVar("TD") class Either(Generic[TA, TB]): # pylint: disable=too-few-public-methods value: Union[TA, TB] + # def __new__(cls, _): + # return super(Either, cls).__new__(cls) + + def __init__(self, value: Union[TA, TB]): + self.value = value + + def __len__(self): + return 2 + + def __iter__(self): + return (self.if_right(None), self.if_left(None)).__iter__() + + def __or__(self, map_: Callable[[TB], TC]): + return self.fmap(map_) + + def __ge__(self, map_: Callable[[TB], Either[TA, TC]]) -> Either[TA, TC]: + return bind(self, map_) + def bind(self, map_: Callable[[TB], Either[TA, TC]]) -> Either[TA, TC]: return bind(self, map_) - def chain(self, em1: Either[TA, TB]) -> Either[TA, TB]: - return chain(self, em1) + def then(self, em1: Either[TA, TB]) -> Either[TA, TB]: + return then(self, em1) def discard(self, map_: Callable[[TB], Either[TA, TB]]) -> Either[TA, TB]: - return self.bind(map_).chain(self) + return self.bind(map_).then(self) def either(self, map_left: Callable[[TA], TC], map_right: Callable[[TB], TC]) -> TC: return either(map_left, map_right, self) @@ -32,9 +49,15 @@ def either(self, map_left: Callable[[TA], TC], map_right: Callable[[TB], TC]) -> def fmap(self, map_: Callable[[TB], TC]) -> Either[TA, TC]: return fmap(self, map_) + def if_left(self, fallback: TB) -> TB: + return if_left(self, fallback) + def is_left(self) -> bool: return is_left(self) + def if_right(self, fallback: TA) -> TA: + return if_right(self, fallback) + def is_right(self) -> bool: return is_right(self) @@ -52,25 +75,23 @@ def __eq__(self, __o: object) -> bool: return __o.value == self.value -class Left(Either[TA, TB]): # pylint: disable=too-few-public-methods - def __init__(self, value: TA): - self.value = value - +class _Left(Either[TA, TB]): # pylint: disable=too-few-public-methods def __str__(self) -> str: return f"Left(value={self.value.__str__()})" -class Right(Either[TA, TB]): # pylint: disable=too-few-public-methods - def __init__(self, value: TB): - self.value = value - +class _Right(Either[TA, TB]): # pylint: disable=too-few-public-methods def __str__(self) -> str: return f"Right(value={self.value.__str__()})" +def __id(value): + return value + + def bind(em0: Either[TC, TA], map_: Callable[[TA], Either[TC, TB]]) -> Either[TC, TB]: if is_left(em0): - return cast(Left[TC, TB], em0) + return cast(_Left[TC, TB], em0) result = map_(cast(TA, em0.value)) if not isinstance(result, Either): @@ -79,7 +100,7 @@ def bind(em0: Either[TC, TA], map_: Callable[[TA], Either[TC, TB]]) -> Either[TC return result -def chain(em0: Either[TC, TA], em1: Either[TC, TB]) -> Either[TC, TB]: +def then(em0: Either[TC, TA], em1: Either[TC, TB]) -> Either[TC, TB]: return bind(em0, lambda _: em1) @@ -93,12 +114,20 @@ def either( ) -def fmap(em0: Either[TC, TA], map_: Callable[[TA], TB]) -> Either[TC, TB]: +def fmap(em0: Either[TA, TB], map_: Callable[[TB], TC]) -> Either[TA, TC]: return bind(em0, lambda m0: pure(map_(m0))) +def if_left(em0: Either[TA, TB], lft: TB) -> TB: + return either(lambda _: lft, __id, em0) + + +def if_right(em0: Either[TA, TB], rgt: TA) -> TA: + return either(__id, lambda _: rgt, em0) + + def is_left(em0: Either[TA, TB]) -> bool: - return isinstance(em0, Left) + return isinstance(em0, _Left) def is_right(em0: Either[TA, TB]) -> bool: @@ -106,7 +135,7 @@ def is_right(em0: Either[TA, TB]) -> bool: def left(value: TA) -> Either[TA, TB]: - return Left(value) + return _Left(value) def lefts(eithers: List[Either[TA, TB]]) -> List[TA]: @@ -118,7 +147,7 @@ def rights(eithers: List[Either[TA, TB]]) -> List[TB]: def right(value: TB) -> Either[TA, TB]: - return Right(value) + return _Right(value) -pure = Right # pylint: disable=invalid-name +pure = right = _Right # pylint: disable=invalid-name diff --git a/src/huemon/utils/monads/maybe.py b/src/huemon/utils/monads/maybe.py index 3503196..eb64417 100644 --- a/src/huemon/utils/monads/maybe.py +++ b/src/huemon/utils/monads/maybe.py @@ -15,17 +15,26 @@ class Maybe(Generic[TA]): # pylint: disable=too-few-public-methods value: TA + def __or__(self, map_: Callable[[TA], TB]) -> Maybe[TB]: + return self.fmap(map_) + + def __ge__(self, map_: Callable[[TA], Maybe[TB]]) -> Maybe[TB]: + return bind(self, map_) + def bind(self, map_: Callable[[TA], Maybe[TB]]) -> Maybe[TB]: return bind(self, map_) def fmap(self, map_: Callable[[TA], TB]) -> Maybe[TB]: return fmap(self, map_) + def from_just(self) -> TA: + return from_just(self) + def is_nothing(self) -> bool: return is_nothing(self) @staticmethod - def of(value: TA): # pylint: disable=invalid-name + def of(value: TA | None) -> Maybe[TA]: # pylint: disable=invalid-name return of(value) def maybe(self: Maybe[TA], fallback: TB, map_: Callable[[TA], TB]) -> TB: @@ -41,7 +50,7 @@ def __eq__(self, __o: object) -> bool: return __o.value == self.value -class Just(Maybe[TA]): # pylint: disable=too-few-public-methods +class _Just(Maybe[TA]): # pylint: disable=too-few-public-methods value: TA def __init__(self, value: TA): @@ -51,14 +60,14 @@ def __str__(self) -> str: return f"Just(value={self.value.__str__()})" -class Nothing(Maybe[TA]): # pylint: disable=too-few-public-methods +class _Nothing(Maybe[TA]): # pylint: disable=too-few-public-methods def __str__(self) -> str: return "Nothing" -nothing: Maybe = Nothing() +nothing: Maybe = _Nothing() -pure = Just # pylint: disable=invalid-name +pure = _Just # pylint: disable=invalid-name def bind(em0: Maybe[TA], map_: Callable[[TA], Maybe[TB]]) -> Maybe[TB]: @@ -76,13 +85,20 @@ def fmap(em0: Maybe[TA], map_: Callable[[TA], TB]) -> Maybe[TB]: return bind(em0, lambda m0: pure(map_(m0))) +def from_just(em0: Maybe[TA]): + if em0.is_nothing(): + raise TypeError("fromJust failed, no instance of Just") + + return em0.value + + def is_nothing(em0: Maybe[TA]) -> bool: - return isinstance(em0, Nothing) + return isinstance(em0, _Nothing) def maybe(fallback: TB, map_: Callable[[TA], TB], em0: Maybe[TA]) -> TB: return fallback if is_nothing(em0) else map_(em0.value) -def of(value: TA): # pylint: disable=invalid-name +def of(value: TA | None) -> Maybe[TA]: # pylint: disable=invalid-name return nothing if not value else pure(value) diff --git a/src/mypy.ini b/src/mypy.ini index 47d58fc..af66412 100644 --- a/src/mypy.ini +++ b/src/mypy.ini @@ -1,3 +1,4 @@ [mypy] check_untyped_defs = true strict_optional = true +ignore_missing_imports = true diff --git a/src/tests/test_command_handler.py b/src/tests/test_command_handler.py index 82c23c4..b8473b2 100644 --- a/src/tests/test_command_handler.py +++ b/src/tests/test_command_handler.py @@ -11,17 +11,25 @@ create_name_to_command_mapping, ) from huemon.commands.internal.system_command import SystemCommand +from huemon.sinks.stdout_sink import StdoutSink from huemon.utils.const import EXIT_FAIL from tests.fixtures import MutableApi, create_system_config +def _ch(system_config: dict): + mutable_api = MutableApi() + mutable_api.set_system_config(system_config) + + return CommandHandler( + create_name_to_command_mapping({}, mutable_api, StdoutSink(), [SystemCommand]) + ) + + class TestCachedApi(unittest.TestCase): def test_when_command_is_loaded_it_should_be_listed_as_available(self): vanilla_command_handler = CommandHandler([]) - command_handler = CommandHandler( - create_name_to_command_mapping({}, MutableApi(), [SystemCommand]) - ) + command_handler = _ch({}) expected_command = SystemCommand.name() @@ -41,25 +49,17 @@ def test_when_command_is_loaded_it_should_be_listed_as_available(self): def test_when_cache_not_expired_return_cache(mock_print: MagicMock): some_version = "TEST_VERSION" - mutable_api = MutableApi() - - system_config_pre = create_system_config(version=some_version) - - mutable_api.set_system_config(system_config_pre) - - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [SystemCommand]) - ) + command_handler = _ch(create_system_config(version=some_version)) command_handler.exec("system", ["version"]) mock_print.assert_called_once_with(some_version) def test_when_unknown_command_received_system_exit_is_called(self): - command_handler = CommandHandler([]) + vanilla_command_handler = CommandHandler([]) with self.assertRaises(SystemExit) as failed_call_context: - command_handler.exec("system", ["version"]) + vanilla_command_handler.exec("system", ["version"]) self.assertEqual( EXIT_FAIL, diff --git a/src/tests/test_light_command.py b/src/tests/test_light_command.py index 1c47ac5..4ddd566 100644 --- a/src/tests/test_light_command.py +++ b/src/tests/test_light_command.py @@ -4,6 +4,7 @@ # LICENSE file in the root directory of this source tree. import unittest +from typing import List from unittest.mock import MagicMock, call, patch from huemon.commands.command_handler import ( @@ -11,6 +12,7 @@ create_name_to_command_mapping, ) from huemon.commands.internal.light_command import LightCommand +from huemon.sinks.stdout_sink import StdoutSink from huemon.utils.const import EXIT_FAIL from tests.fixtures import MutableApi @@ -18,25 +20,24 @@ SOME_LIGHT_MAC_1 = "SO:ME:LI:GH:TM:AC:01" +def _ch(lights: List[dict]): + mutable_api = MutableApi() + mutable_api.set_lights(lights) + + return CommandHandler( + create_name_to_command_mapping({}, mutable_api, StdoutSink(), [LightCommand]) + ) + + class TestLightCommand(unittest.TestCase): def test_when_light_doesnt_exist_raise(self): - mutable_api = MutableApi() - mutable_api.set_lights([]) - - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [LightCommand]) - ) + command_handler = _ch([]) with self.assertRaises(Exception): command_handler.exec("light", [SOME_LIGHT_MAC_0, "status"]) def test_when_not_enough_parameters_raise(self): - mutable_api = MutableApi() - mutable_api.set_lights([]) - - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [LightCommand]) - ) + command_handler = _ch([]) with self.assertRaises(SystemExit) as failed_call_context: command_handler.exec("light", []) @@ -48,12 +49,7 @@ def test_when_not_enough_parameters_raise(self): ) def test_when_unknown_action_raise(self): - mutable_api = MutableApi() - mutable_api.set_lights([]) - - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [LightCommand]) - ) + command_handler = _ch([]) with self.assertRaises(SystemExit) as failed_call_context: command_handler.exec("light", [SOME_LIGHT_MAC_0, "some_unknown_action"]) @@ -67,8 +63,7 @@ def test_when_unknown_action_raise(self): @staticmethod @patch("builtins.print") def test_when_light_exists_return_status(mock_print: MagicMock): - mutable_api = MutableApi() - mutable_api.set_lights( + command_handler = _ch( [ { "uniqueid": SOME_LIGHT_MAC_0, @@ -85,20 +80,15 @@ def test_when_light_exists_return_status(mock_print: MagicMock): ] ) - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [LightCommand]) - ) - command_handler.exec("light", [SOME_LIGHT_MAC_0, "status"]) command_handler.exec("light", [SOME_LIGHT_MAC_1, "status"]) - mock_print.assert_has_calls(map(call, [1, 0])) + mock_print.assert_has_calls(list(map(call, [1, 0]))) @staticmethod @patch("builtins.print") def test_when_light_exists_return_is_upgrade_available(mock_print: MagicMock): - mutable_api = MutableApi() - mutable_api.set_lights( + command_handler = _ch( [ { "uniqueid": SOME_LIGHT_MAC_0, @@ -115,20 +105,15 @@ def test_when_light_exists_return_is_upgrade_available(mock_print: MagicMock): ] ) - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [LightCommand]) - ) - command_handler.exec("light", [SOME_LIGHT_MAC_0, "is_upgrade_available"]) command_handler.exec("light", [SOME_LIGHT_MAC_1, "is_upgrade_available"]) - mock_print.assert_has_calls(map(call, [0, 1])) + mock_print.assert_has_calls(list(map(call, [0, 1]))) @staticmethod @patch("builtins.print") def test_when_light_exists_return_is_reachable(mock_print: MagicMock): - mutable_api = MutableApi() - mutable_api.set_lights( + command_handler = _ch( [ {"uniqueid": SOME_LIGHT_MAC_0, "state": {"reachable": 0}}, { @@ -140,14 +125,10 @@ def test_when_light_exists_return_is_reachable(mock_print: MagicMock): ] ) - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [LightCommand]) - ) - command_handler.exec("light", [SOME_LIGHT_MAC_0, "reachable"]) command_handler.exec("light", [SOME_LIGHT_MAC_1, "reachable"]) - mock_print.assert_has_calls(map(call, [0, 1])) + mock_print.assert_has_calls(list(map(call, [0, 1]))) @staticmethod @patch("builtins.print") @@ -155,8 +136,7 @@ def test_when_light_exists_return_version(mock_print: MagicMock): some_version_0 = "some_version_0" some_version_1 = "some_version_1" - mutable_api = MutableApi() - mutable_api.set_lights( + command_handler = _ch( [ { "uniqueid": SOME_LIGHT_MAC_0, @@ -169,11 +149,7 @@ def test_when_light_exists_return_version(mock_print: MagicMock): ] ) - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [LightCommand]) - ) - command_handler.exec("light", [SOME_LIGHT_MAC_0, "version"]) command_handler.exec("light", [SOME_LIGHT_MAC_1, "version"]) - mock_print.assert_has_calls(map(call, [some_version_0, some_version_1])) + mock_print.assert_has_calls(list(map(call, [some_version_0, some_version_1]))) diff --git a/src/tests/test_monad_either.py b/src/tests/test_monad_either.py index b8e0eee..3fbf1b3 100644 --- a/src/tests/test_monad_either.py +++ b/src/tests/test_monad_either.py @@ -85,7 +85,7 @@ def test_when_left_chain_should_not_do_anything(self): either_left = left(0) either_right = right(0) - result = either_left.chain(either_right) + result = either_left.then(either_right) self.assertEqual( either_left, @@ -97,7 +97,7 @@ def test_when_right_chain_should_return_chained_monad(self): either_right_0 = right(21) either_right_1 = right(42) - result = either_right_0.chain(either_right_1) + result = either_right_0.then(either_right_1) self.assertEqual( either_right_1, diff --git a/src/tests/test_sensor_command.py b/src/tests/test_sensor_command.py index fdd20b0..97befb0 100644 --- a/src/tests/test_sensor_command.py +++ b/src/tests/test_sensor_command.py @@ -4,6 +4,7 @@ # LICENSE file in the root directory of this source tree. import unittest +from typing import List from unittest.mock import MagicMock, call, patch from huemon.commands.command_handler import ( @@ -11,20 +12,25 @@ create_name_to_command_mapping, ) from huemon.commands.internal.sensor_command import SensorCommand +from huemon.sinks.stdout_sink import StdoutSink from tests.fixtures import MutableApi SOME_SENSOR_MAC_0 = "SO:ME:SE:NS:OR:MA:C0" SOME_SENSOR_MAC_1 = "SO:ME:SE:NS:OR:MA:C1" +def _ch(sensors: List[dict]): + mutable_api = MutableApi() + mutable_api.set_sensors(sensors) + + return CommandHandler( + create_name_to_command_mapping({}, mutable_api, StdoutSink(), [SensorCommand]) + ) + + class TestSensorCommand(unittest.TestCase): def test_when_sensor_doesnt_exist(self): - mutable_api = MutableApi() - mutable_api.set_sensors([]) - - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [SensorCommand]) - ) + command_handler = _ch([]) with self.assertRaises(Exception): command_handler.exec("sensor", [SOME_SENSOR_MAC_0, "battery:level"]) @@ -35,8 +41,7 @@ def test_when_sensor_exists_return_battery_level(mock_print: MagicMock): some_level_0 = 80 some_level_1 = 1 - mutable_api = MutableApi() - mutable_api.set_sensors( + command_handler = _ch( [ { "uniqueid": SOME_SENSOR_MAC_0, @@ -53,14 +58,10 @@ def test_when_sensor_exists_return_battery_level(mock_print: MagicMock): ] ) - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [SensorCommand]) - ) - command_handler.exec("sensor", [SOME_SENSOR_MAC_0, "battery:level"]) command_handler.exec("sensor", [SOME_SENSOR_MAC_1, "battery:level"]) - mock_print.assert_has_calls(map(call, [some_level_0, some_level_1])) + mock_print.assert_has_calls(list(map(call, [some_level_0, some_level_1]))) @staticmethod @patch("builtins.print") @@ -68,8 +69,7 @@ def test_when_sensor_exists_return_light_level(mock_print: MagicMock): some_level_0 = 80 some_level_1 = 1 - mutable_api = MutableApi() - mutable_api.set_sensors( + command_handler = _ch( [ { "uniqueid": SOME_SENSOR_MAC_0, @@ -86,14 +86,10 @@ def test_when_sensor_exists_return_light_level(mock_print: MagicMock): ] ) - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [SensorCommand]) - ) - command_handler.exec("sensor", [SOME_SENSOR_MAC_0, "light:level"]) command_handler.exec("sensor", [SOME_SENSOR_MAC_1, "light:level"]) - mock_print.assert_has_calls(map(call, [some_level_0, some_level_1])) + mock_print.assert_has_calls(list(map(call, [some_level_0, some_level_1]))) @staticmethod @patch("builtins.print") @@ -101,8 +97,7 @@ def test_when_sensor_exists_return_temperature(mock_print: MagicMock): some_temperature_0 = 80 some_temperature_1 = 1 - mutable_api = MutableApi() - mutable_api.set_sensors( + command_handler = _ch( [ { "uniqueid": SOME_SENSOR_MAC_0, @@ -119,15 +114,11 @@ def test_when_sensor_exists_return_temperature(mock_print: MagicMock): ] ) - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [SensorCommand]) - ) - command_handler.exec("sensor", [SOME_SENSOR_MAC_0, "temperature"]) command_handler.exec("sensor", [SOME_SENSOR_MAC_1, "temperature"]) mock_print.assert_has_calls( - map(call, [some_temperature_0 * 0.01, some_temperature_1 * 0.01]) + list(map(call, [some_temperature_0 * 0.01, some_temperature_1 * 0.01])) ) @staticmethod @@ -136,8 +127,7 @@ def test_when_sensor_exists_return_presence(mock_print: MagicMock): some_presence_0 = 80 some_presence_1 = 1 - mutable_api = MutableApi() - mutable_api.set_sensors( + command_handler = _ch( [ { "uniqueid": SOME_SENSOR_MAC_0, @@ -154,14 +144,10 @@ def test_when_sensor_exists_return_presence(mock_print: MagicMock): ] ) - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [SensorCommand]) - ) - command_handler.exec("sensor", [SOME_SENSOR_MAC_0, "presence"]) command_handler.exec("sensor", [SOME_SENSOR_MAC_1, "presence"]) - mock_print.assert_has_calls(map(call, [some_presence_0, some_presence_1])) + mock_print.assert_has_calls(list(map(call, [some_presence_0, some_presence_1]))) @staticmethod @patch("builtins.print") @@ -169,8 +155,7 @@ def test_when_sensor_exists_return_reachable(mock_print: MagicMock): some_reachability_0 = 0 some_reachability_1 = 1 - mutable_api = MutableApi() - mutable_api.set_sensors( + command_handler = _ch( [ { "uniqueid": SOME_SENSOR_MAC_0, @@ -187,13 +172,9 @@ def test_when_sensor_exists_return_reachable(mock_print: MagicMock): ] ) - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [SensorCommand]) - ) - command_handler.exec("sensor", [SOME_SENSOR_MAC_0, "reachable"]) command_handler.exec("sensor", [SOME_SENSOR_MAC_1, "reachable"]) mock_print.assert_has_calls( - map(call, [some_reachability_0, some_reachability_1]) + list(map(call, [some_reachability_0, some_reachability_1])) ) diff --git a/src/tests/test_system_command.py b/src/tests/test_system_command.py index 528139f..9058841 100644 --- a/src/tests/test_system_command.py +++ b/src/tests/test_system_command.py @@ -11,26 +11,31 @@ create_name_to_command_mapping, ) from huemon.commands.internal.system_command import SystemCommand +from huemon.sinks.stdout_sink import StdoutSink from tests.fixtures import MutableApi +def _ch(system_config: dict): + mutable_api = MutableApi() + mutable_api.set_system_config(system_config) + + return CommandHandler( + create_name_to_command_mapping({}, mutable_api, StdoutSink(), [SystemCommand]) + ) + + class TestCachedApi(unittest.TestCase): @staticmethod @patch("builtins.print") def test_when_system_version_received_print(mock_print: MagicMock): some_version = "SOME_VERSION_0" - mutable_api = MutableApi() - mutable_api.set_system_config( + command_handler = _ch( { "swversion": some_version, } ) - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [SystemCommand]) - ) - command_handler.exec("system", ["version"]) mock_print.assert_called_once_with(some_version) @@ -38,8 +43,7 @@ def test_when_system_version_received_print(mock_print: MagicMock): @staticmethod @patch("builtins.print") def test_when_system_upgrade_available_print(mock_print: MagicMock): - mutable_api_0 = MutableApi() - mutable_api_0.set_system_config( + command_handler_0 = _ch( { "swupdate2": { "bridge": { @@ -49,8 +53,7 @@ def test_when_system_upgrade_available_print(mock_print: MagicMock): } ) - mutable_api_1 = MutableApi() - mutable_api_1.set_system_config( + command_handler_1 = _ch( { "swupdate2": { "bridge": { @@ -60,13 +63,6 @@ def test_when_system_upgrade_available_print(mock_print: MagicMock): } ) - command_handler_0 = CommandHandler( - create_name_to_command_mapping({}, mutable_api_0, [SystemCommand]) - ) - command_handler_1 = CommandHandler( - create_name_to_command_mapping({}, mutable_api_1, [SystemCommand]) - ) - command_handler_0.exec("system", ["is_upgrade_available"]) command_handler_1.exec("system", ["is_upgrade_available"])