From edf4baae9def71b7924e86699d34ef37ce4cf2ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Garc=C3=ADa=20Prado?= Date: Tue, 28 Sep 2021 15:58:20 +0200 Subject: [PATCH 01/21] ISSUE #398 * Implement `hash` into `minos.networks.Response`. * Add `minos.networks.PeriodicEnrouteDecorator`. * Allow to setup multiple handling functions for same event. * Add `crontab` dependency. --- minos/networks/__init__.py | 2 + minos/networks/decorators/__init__.py | 2 + minos/networks/decorators/analyzers.py | 9 ++++ minos/networks/decorators/api.py | 12 +++++ minos/networks/decorators/builders.py | 50 +++++++++++++++---- .../decorators/definitions/__init__.py | 4 ++ .../decorators/definitions/periodic.py | 40 +++++++++++++++ minos/networks/messages.py | 3 ++ poetry.lock | 33 +++++++++++- pyproject.toml | 1 + .../test_decorators/test_analyzers.py | 14 ++++++ .../test_networks/test_decorators/test_api.py | 5 ++ .../test_decorators/test_builders.py | 10 ++++ tests/test_networks/test_messages.py | 3 ++ tests/utils.py | 10 ++++ 15 files changed, 187 insertions(+), 11 deletions(-) create mode 100644 minos/networks/decorators/definitions/periodic.py diff --git a/minos/networks/__init__.py b/minos/networks/__init__.py index 80aabab2..2922add4 100644 --- a/minos/networks/__init__.py +++ b/minos/networks/__init__.py @@ -18,6 +18,8 @@ EnrouteBuilder, EnrouteDecorator, EnrouteDecoratorKind, + PeriodicEnrouteDecorator, + PeriodicEventEnrouteDecorator, RestCommandEnrouteDecorator, RestEnrouteDecorator, RestQueryEnrouteDecorator, diff --git a/minos/networks/decorators/__init__.py b/minos/networks/decorators/__init__.py index 6fdbc8d1..f48ff701 100644 --- a/minos/networks/decorators/__init__.py +++ b/minos/networks/decorators/__init__.py @@ -14,6 +14,8 @@ BrokerQueryEnrouteDecorator, EnrouteDecorator, EnrouteDecoratorKind, + PeriodicEnrouteDecorator, + PeriodicEventEnrouteDecorator, RestCommandEnrouteDecorator, RestEnrouteDecorator, RestQueryEnrouteDecorator, diff --git a/minos/networks/decorators/analyzers.py b/minos/networks/decorators/analyzers.py index 8c41cc43..b449eca1 100644 --- a/minos/networks/decorators/analyzers.py +++ b/minos/networks/decorators/analyzers.py @@ -21,6 +21,7 @@ BrokerEventEnrouteDecorator, BrokerQueryEnrouteDecorator, EnrouteDecorator, + PeriodicEventEnrouteDecorator, RestCommandEnrouteDecorator, RestEnrouteDecorator, RestQueryEnrouteDecorator, @@ -71,6 +72,14 @@ def get_broker_event(self) -> dict[str, set[BrokerEnrouteDecorator]]: # noinspection PyTypeChecker return self._get_items({BrokerEventEnrouteDecorator}) + def get_periodic_event(self) -> dict[str, set[PeriodicEventEnrouteDecorator]]: + """TODO + + :return: TODO + """ + # noinspection PyTypeChecker + return self._get_items({PeriodicEventEnrouteDecorator}) + def _get_items(self, expected_types: set[Type[EnrouteDecorator]]) -> dict[str, set[EnrouteDecorator]]: items = dict() for fn, decorators in self.get_all().items(): diff --git a/minos/networks/decorators/api.py b/minos/networks/decorators/api.py index eeb32988..b3eddd75 100644 --- a/minos/networks/decorators/api.py +++ b/minos/networks/decorators/api.py @@ -1,7 +1,12 @@ +from __future__ import ( + annotations, +) + from .definitions import ( BrokerCommandEnrouteDecorator, BrokerEventEnrouteDecorator, BrokerQueryEnrouteDecorator, + PeriodicEventEnrouteDecorator, RestCommandEnrouteDecorator, RestQueryEnrouteDecorator, ) @@ -22,11 +27,18 @@ class RestEnroute: query = RestQueryEnrouteDecorator +class PeriodicEnroute: + """Periodic Enroute class.""" + + event = PeriodicEventEnrouteDecorator + + class Enroute: """Enroute decorator main class""" broker = BrokerEnroute rest = RestEnroute + periodic = PeriodicEnroute enroute = Enroute diff --git a/minos/networks/decorators/builders.py b/minos/networks/decorators/builders.py index 356a53d6..15ac3bcc 100644 --- a/minos/networks/decorators/builders.py +++ b/minos/networks/decorators/builders.py @@ -1,3 +1,9 @@ +from asyncio import ( + gather, +) +from collections import ( + defaultdict, +) from inspect import ( iscoroutinefunction, ) @@ -26,9 +32,13 @@ from .definitions import ( BrokerEnrouteDecorator, EnrouteDecorator, + EnrouteDecoratorKind, + PeriodicEnrouteDecorator, RestEnrouteDecorator, ) +Handler = Callable[[Request], Awaitable[Response]] + class EnrouteBuilder: """Enroute builder class.""" @@ -40,7 +50,7 @@ def __init__(self, decorated: Union[str, Type], *args, **kwargs): self.decorated = decorated self.analyzer = EnrouteAnalyzer(decorated, *args, **kwargs) - def get_rest_command_query(self) -> dict[RestEnrouteDecorator, Callable[[Request], Awaitable[Response]]]: + def get_rest_command_query(self) -> dict[RestEnrouteDecorator, Handler]: """Get the rest handlers for commands and queries. :return: A dictionary with decorator classes as keys and callable handlers as values. @@ -49,7 +59,7 @@ def get_rest_command_query(self) -> dict[RestEnrouteDecorator, Callable[[Request # noinspection PyTypeChecker return self._build(mapping) - def get_broker_command_query(self) -> dict[BrokerEnrouteDecorator, Callable[[Request], Awaitable[Response]]]: + def get_broker_command_query(self) -> dict[BrokerEnrouteDecorator, Handler]: """Get the broker handlers for commands and queries. :return: A dictionary with decorator classes as keys and callable handlers as values. @@ -58,7 +68,7 @@ def get_broker_command_query(self) -> dict[BrokerEnrouteDecorator, Callable[[Req # noinspection PyTypeChecker return self._build(mapping) - def get_broker_event(self) -> dict[BrokerEnrouteDecorator, Callable[[Request], Awaitable[Response]]]: + def get_broker_event(self) -> dict[BrokerEnrouteDecorator, Handler]: """Get the broker handlers for events. :return: A dictionary with decorator classes as keys and callable handlers as values. @@ -67,16 +77,36 @@ def get_broker_event(self) -> dict[BrokerEnrouteDecorator, Callable[[Request], A # noinspection PyTypeChecker return self._build(mapping) - def _build( - self, mapping: dict[str, set[EnrouteDecorator]] - ) -> dict[EnrouteDecorator, Callable[[Request], Awaitable[Response]]]: + def get_periodic_event(self) -> dict[PeriodicEnrouteDecorator, Handler]: + """TODO + + :return: A dictionary with decorator classes as keys and callable handlers as values. + """ + mapping = self.analyzer.get_periodic_event() + # noinspection PyTypeChecker + return self._build(mapping) - ans = dict() + def _build(self, mapping: dict[str, set[EnrouteDecorator]]) -> dict[EnrouteDecorator, Handler]: + + ans = defaultdict(set) for name, decorators in mapping.items(): for decorator in decorators: - if decorator in ans: - raise MinosRedefinedEnrouteDecoratorException(f"{decorator!r} can be used only once.") - ans[decorator] = self._build_one(name, decorator.pre_fn_name) + ans[decorator].add(self._build_one(name, decorator.pre_fn_name)) + + def _make_fn(d, fns: set[Handler]) -> Handler: + if len(fns) == 1: + return next(iter(fns)) + + if d.KIND != EnrouteDecoratorKind.Event: + raise MinosRedefinedEnrouteDecoratorException(f"{d!r} can be used only once.") + + async def _fn(*args, **kwargs): + return await gather(*(fn(*args, **kwargs) for fn in fns)) + + return _fn + + ans = {decorator: _make_fn(decorator, fns) for decorator, fns in ans.items()} + return ans def _build_one(self, name: str, pref_fn_name: str) -> Callable: diff --git a/minos/networks/decorators/definitions/__init__.py b/minos/networks/decorators/definitions/__init__.py index 7dae0e73..eeca14e5 100644 --- a/minos/networks/decorators/definitions/__init__.py +++ b/minos/networks/decorators/definitions/__init__.py @@ -10,6 +10,10 @@ from .kinds import ( EnrouteDecoratorKind, ) +from .periodic import ( + PeriodicEnrouteDecorator, + PeriodicEventEnrouteDecorator, +) from .rest import ( RestCommandEnrouteDecorator, RestEnrouteDecorator, diff --git a/minos/networks/decorators/definitions/periodic.py b/minos/networks/decorators/definitions/periodic.py new file mode 100644 index 00000000..c2ba5122 --- /dev/null +++ b/minos/networks/decorators/definitions/periodic.py @@ -0,0 +1,40 @@ +from abc import ( + ABC, +) +from typing import ( + Final, + Iterable, + Union, +) + +from crontab import ( + CronTab, +) + +from .abc import ( + EnrouteDecorator, +) +from .kinds import ( + EnrouteDecoratorKind, +) + + +class PeriodicEnrouteDecorator(EnrouteDecorator, ABC): + """Periodic Enroute class""" + + def __init__(self, crontab: Union[str, CronTab]): + if isinstance(crontab, str): + crontab = CronTab(crontab) + self.crontab = crontab + + def __iter__(self) -> Iterable: + yield from (self.crontab,) + + def __hash__(self): + return hash(tuple((s if not isinstance(s, CronTab) else s.matchers) for s in self)) + + +class PeriodicEventEnrouteDecorator(PeriodicEnrouteDecorator): + """Periodic Command Enroute class""" + + KIND: Final[EnrouteDecoratorKind] = EnrouteDecoratorKind.Event diff --git a/minos/networks/messages.py b/minos/networks/messages.py index c9564e17..180942de 100644 --- a/minos/networks/messages.py +++ b/minos/networks/messages.py @@ -123,6 +123,9 @@ def __eq__(self, other: Response) -> bool: def __repr__(self) -> str: return f"{type(self).__name__}({self._data!r})" + def __hash__(self): + return hash(self._data) + class ResponseException(MinosException): """Response Exception class.""" diff --git a/poetry.lock b/poetry.lock index d7d9b2f4..6c9e2483 100644 --- a/poetry.lock +++ b/poetry.lock @@ -254,6 +254,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] toml = ["toml"] +[[package]] +name = "crontab" +version = "0.23.0" +description = "Parse and use crontab schedules in Python" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "dependency-injector" version = "4.36.0" @@ -902,7 +910,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "6eb8fe705fd1bb8709ec7475c4605438f8d162f3d93cc44e11f36c4543619e68" +content-hash = "9be4268bb344ac8e3b15799e3855c5646cf7fa18884e6b43774da86b8ba9c8e0" [metadata.files] aiohttp = [ @@ -1140,6 +1148,9 @@ coverage = [ {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, ] +crontab = [ + {file = "crontab-0.23.0.tar.gz", hash = "sha256:ca79dede9c2f572bb32f38703e8fddcf3427e86edc838f2ffe7ae4b9ee2b0733"}, +] dependency-injector = [ {file = "dependency-injector-4.36.0.tar.gz", hash = "sha256:a5f6090062c7d19947a65fad932f37992cc8a8558d375f040322b72b244a135f"}, {file = "dependency_injector-4.36.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:01dfc4aaee8bdd6b12e13a052571501eaa2e395c1729f00d956bdf62b34d8436"}, @@ -1296,12 +1307,22 @@ m2r2 = [ {file = "m2r2-0.2.8.tar.gz", hash = "sha256:ca39e1db74991818d667c7367e4fc2de13ecefd2a04d69d83b0ffa76d20d7e29"}, ] markupsafe = [ + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -1310,14 +1331,21 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -1327,6 +1355,9 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, diff --git a/pyproject.toml b/pyproject.toml index 01bec400..29d5c41f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ aiopg = "^1.2.1" aiohttp = "^3.7.4" dependency-injector = "^4.32.2" minos-microservice-common = "^0.1.13" +crontab = "^0.23.0" [tool.poetry.dev-dependencies] black = "^19.10b" diff --git a/tests/test_networks/test_decorators/test_analyzers.py b/tests/test_networks/test_decorators/test_analyzers.py index 9ca612d8..ab1bfae5 100644 --- a/tests/test_networks/test_decorators/test_analyzers.py +++ b/tests/test_networks/test_decorators/test_analyzers.py @@ -8,6 +8,7 @@ BrokerEventEnrouteDecorator, BrokerQueryEnrouteDecorator, EnrouteAnalyzer, + PeriodicEventEnrouteDecorator, RestCommandEnrouteDecorator, RestQueryEnrouteDecorator, ) @@ -38,6 +39,8 @@ def test_get_all(self): BrokerCommandEnrouteDecorator("DeleteTicket"), RestCommandEnrouteDecorator("orders/", "DELETE"), }, + "send_newsletter": {PeriodicEventEnrouteDecorator("@daily")}, + "check_inactive_users": {PeriodicEventEnrouteDecorator("@daily")}, } self.assertEqual(expected, observed) @@ -93,6 +96,17 @@ def test_get_broker_event(self): self.assertEqual(expected, observed) + def test_get_periodic_event(self): + analyzer = EnrouteAnalyzer(FakeService) + + observed = analyzer.get_periodic_event() + expected = { + "send_newsletter": {PeriodicEventEnrouteDecorator("@daily")}, + "check_inactive_users": {PeriodicEventEnrouteDecorator("@daily")}, + } + + self.assertEqual(expected, observed) + def test_with_get_enroute(self): analyzer = EnrouteAnalyzer(FakeServiceWithGetEnroute) diff --git a/tests/test_networks/test_decorators/test_api.py b/tests/test_networks/test_decorators/test_api.py index 4b055068..3ac33cd1 100644 --- a/tests/test_networks/test_decorators/test_api.py +++ b/tests/test_networks/test_decorators/test_api.py @@ -4,6 +4,7 @@ BrokerCommandEnrouteDecorator, BrokerEventEnrouteDecorator, BrokerQueryEnrouteDecorator, + PeriodicEventEnrouteDecorator, RestCommandEnrouteDecorator, RestQueryEnrouteDecorator, enroute, @@ -35,6 +36,10 @@ def test_broker_event_decorators(self): decorator = enroute.broker.event("CreateTicket") self.assertEqual(BrokerEventEnrouteDecorator("CreateTicket"), decorator) + def test_periodic_command_decorators(self): + decorator = enroute.periodic.event("0 */2 * * *") + self.assertEqual(PeriodicEventEnrouteDecorator("0 */2 * * *"), decorator) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_networks/test_decorators/test_builders.py b/tests/test_networks/test_decorators/test_builders.py index b18a3588..f6a67172 100644 --- a/tests/test_networks/test_decorators/test_builders.py +++ b/tests/test_networks/test_decorators/test_builders.py @@ -9,6 +9,7 @@ BrokerQueryEnrouteDecorator, EnrouteBuilder, MinosRedefinedEnrouteDecoratorException, + PeriodicEventEnrouteDecorator, Response, RestCommandEnrouteDecorator, RestQueryEnrouteDecorator, @@ -53,6 +54,15 @@ async def test_get_broker_event(self): observed = await handlers[BrokerEventEnrouteDecorator("TicketAdded")](self.request) self.assertEqual(expected, observed) + async def test_get_periodic_event(self): + handlers = self.builder.get_periodic_event() + self.assertEqual(1, len(handlers)) + + expected = {Response("newsletter sent!"), Response("checked inactive users!")} + # noinspection PyTypeChecker + observed = set(await handlers[PeriodicEventEnrouteDecorator("@daily")](self.request)) + self.assertEqual(expected, observed) + async def test_get_broker_command_query(self): handlers = self.builder.get_broker_command_query() self.assertEqual(4, len(handlers)) diff --git a/tests/test_networks/test_messages.py b/tests/test_networks/test_messages.py index fba66c0f..6829b8a3 100644 --- a/tests/test_networks/test_messages.py +++ b/tests/test_networks/test_messages.py @@ -85,6 +85,9 @@ async def test_repr(self): response = Response(self.data) self.assertEqual("Response([FakeModel(text=blue), FakeModel(text=red)])", repr(response)) + def test_hash(self): + self.assertIsInstance(hash(Response("test")), int) + if __name__ == "__main__": unittest.main() diff --git a/tests/utils.py b/tests/utils.py index e9d1c3fb..48b08253 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -211,6 +211,16 @@ async def ticket_added(request: Request) -> Response: """For testing purposes.""" return Response(": ".join(("Ticket Added", await request.content(),))) + @enroute.periodic.event("@daily") + async def send_newsletter(self, request: Request): + """For testing purposes.""" + return Response("newsletter sent!") + + @enroute.periodic.event("@daily") + async def check_inactive_users(self, request: Request): + """For testing purposes.""" + return Response("checked inactive users!") + # noinspection PyMethodMayBeStatic,PyUnusedLocal def bar(self, request: Request): """For testing purposes.""" From 9d8169c2e3bb4d13020472d5f6748796cd1f2b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Garc=C3=ADa=20Prado?= Date: Tue, 28 Sep 2021 16:00:17 +0200 Subject: [PATCH 02/21] Revert "ISSUE #398" This reverts commit edf4baae9def71b7924e86699d34ef37ce4cf2ff. --- minos/networks/__init__.py | 2 - minos/networks/decorators/__init__.py | 2 - minos/networks/decorators/analyzers.py | 9 ---- minos/networks/decorators/api.py | 12 ----- minos/networks/decorators/builders.py | 50 ++++--------------- .../decorators/definitions/__init__.py | 4 -- .../decorators/definitions/periodic.py | 40 --------------- minos/networks/messages.py | 3 -- poetry.lock | 33 +----------- pyproject.toml | 1 - .../test_decorators/test_analyzers.py | 14 ------ .../test_networks/test_decorators/test_api.py | 5 -- .../test_decorators/test_builders.py | 10 ---- tests/test_networks/test_messages.py | 3 -- tests/utils.py | 10 ---- 15 files changed, 11 insertions(+), 187 deletions(-) delete mode 100644 minos/networks/decorators/definitions/periodic.py diff --git a/minos/networks/__init__.py b/minos/networks/__init__.py index 2922add4..80aabab2 100644 --- a/minos/networks/__init__.py +++ b/minos/networks/__init__.py @@ -18,8 +18,6 @@ EnrouteBuilder, EnrouteDecorator, EnrouteDecoratorKind, - PeriodicEnrouteDecorator, - PeriodicEventEnrouteDecorator, RestCommandEnrouteDecorator, RestEnrouteDecorator, RestQueryEnrouteDecorator, diff --git a/minos/networks/decorators/__init__.py b/minos/networks/decorators/__init__.py index f48ff701..6fdbc8d1 100644 --- a/minos/networks/decorators/__init__.py +++ b/minos/networks/decorators/__init__.py @@ -14,8 +14,6 @@ BrokerQueryEnrouteDecorator, EnrouteDecorator, EnrouteDecoratorKind, - PeriodicEnrouteDecorator, - PeriodicEventEnrouteDecorator, RestCommandEnrouteDecorator, RestEnrouteDecorator, RestQueryEnrouteDecorator, diff --git a/minos/networks/decorators/analyzers.py b/minos/networks/decorators/analyzers.py index b449eca1..8c41cc43 100644 --- a/minos/networks/decorators/analyzers.py +++ b/minos/networks/decorators/analyzers.py @@ -21,7 +21,6 @@ BrokerEventEnrouteDecorator, BrokerQueryEnrouteDecorator, EnrouteDecorator, - PeriodicEventEnrouteDecorator, RestCommandEnrouteDecorator, RestEnrouteDecorator, RestQueryEnrouteDecorator, @@ -72,14 +71,6 @@ def get_broker_event(self) -> dict[str, set[BrokerEnrouteDecorator]]: # noinspection PyTypeChecker return self._get_items({BrokerEventEnrouteDecorator}) - def get_periodic_event(self) -> dict[str, set[PeriodicEventEnrouteDecorator]]: - """TODO - - :return: TODO - """ - # noinspection PyTypeChecker - return self._get_items({PeriodicEventEnrouteDecorator}) - def _get_items(self, expected_types: set[Type[EnrouteDecorator]]) -> dict[str, set[EnrouteDecorator]]: items = dict() for fn, decorators in self.get_all().items(): diff --git a/minos/networks/decorators/api.py b/minos/networks/decorators/api.py index b3eddd75..eeb32988 100644 --- a/minos/networks/decorators/api.py +++ b/minos/networks/decorators/api.py @@ -1,12 +1,7 @@ -from __future__ import ( - annotations, -) - from .definitions import ( BrokerCommandEnrouteDecorator, BrokerEventEnrouteDecorator, BrokerQueryEnrouteDecorator, - PeriodicEventEnrouteDecorator, RestCommandEnrouteDecorator, RestQueryEnrouteDecorator, ) @@ -27,18 +22,11 @@ class RestEnroute: query = RestQueryEnrouteDecorator -class PeriodicEnroute: - """Periodic Enroute class.""" - - event = PeriodicEventEnrouteDecorator - - class Enroute: """Enroute decorator main class""" broker = BrokerEnroute rest = RestEnroute - periodic = PeriodicEnroute enroute = Enroute diff --git a/minos/networks/decorators/builders.py b/minos/networks/decorators/builders.py index 15ac3bcc..356a53d6 100644 --- a/minos/networks/decorators/builders.py +++ b/minos/networks/decorators/builders.py @@ -1,9 +1,3 @@ -from asyncio import ( - gather, -) -from collections import ( - defaultdict, -) from inspect import ( iscoroutinefunction, ) @@ -32,13 +26,9 @@ from .definitions import ( BrokerEnrouteDecorator, EnrouteDecorator, - EnrouteDecoratorKind, - PeriodicEnrouteDecorator, RestEnrouteDecorator, ) -Handler = Callable[[Request], Awaitable[Response]] - class EnrouteBuilder: """Enroute builder class.""" @@ -50,7 +40,7 @@ def __init__(self, decorated: Union[str, Type], *args, **kwargs): self.decorated = decorated self.analyzer = EnrouteAnalyzer(decorated, *args, **kwargs) - def get_rest_command_query(self) -> dict[RestEnrouteDecorator, Handler]: + def get_rest_command_query(self) -> dict[RestEnrouteDecorator, Callable[[Request], Awaitable[Response]]]: """Get the rest handlers for commands and queries. :return: A dictionary with decorator classes as keys and callable handlers as values. @@ -59,7 +49,7 @@ def get_rest_command_query(self) -> dict[RestEnrouteDecorator, Handler]: # noinspection PyTypeChecker return self._build(mapping) - def get_broker_command_query(self) -> dict[BrokerEnrouteDecorator, Handler]: + def get_broker_command_query(self) -> dict[BrokerEnrouteDecorator, Callable[[Request], Awaitable[Response]]]: """Get the broker handlers for commands and queries. :return: A dictionary with decorator classes as keys and callable handlers as values. @@ -68,7 +58,7 @@ def get_broker_command_query(self) -> dict[BrokerEnrouteDecorator, Handler]: # noinspection PyTypeChecker return self._build(mapping) - def get_broker_event(self) -> dict[BrokerEnrouteDecorator, Handler]: + def get_broker_event(self) -> dict[BrokerEnrouteDecorator, Callable[[Request], Awaitable[Response]]]: """Get the broker handlers for events. :return: A dictionary with decorator classes as keys and callable handlers as values. @@ -77,36 +67,16 @@ def get_broker_event(self) -> dict[BrokerEnrouteDecorator, Handler]: # noinspection PyTypeChecker return self._build(mapping) - def get_periodic_event(self) -> dict[PeriodicEnrouteDecorator, Handler]: - """TODO - - :return: A dictionary with decorator classes as keys and callable handlers as values. - """ - mapping = self.analyzer.get_periodic_event() - # noinspection PyTypeChecker - return self._build(mapping) + def _build( + self, mapping: dict[str, set[EnrouteDecorator]] + ) -> dict[EnrouteDecorator, Callable[[Request], Awaitable[Response]]]: - def _build(self, mapping: dict[str, set[EnrouteDecorator]]) -> dict[EnrouteDecorator, Handler]: - - ans = defaultdict(set) + ans = dict() for name, decorators in mapping.items(): for decorator in decorators: - ans[decorator].add(self._build_one(name, decorator.pre_fn_name)) - - def _make_fn(d, fns: set[Handler]) -> Handler: - if len(fns) == 1: - return next(iter(fns)) - - if d.KIND != EnrouteDecoratorKind.Event: - raise MinosRedefinedEnrouteDecoratorException(f"{d!r} can be used only once.") - - async def _fn(*args, **kwargs): - return await gather(*(fn(*args, **kwargs) for fn in fns)) - - return _fn - - ans = {decorator: _make_fn(decorator, fns) for decorator, fns in ans.items()} - + if decorator in ans: + raise MinosRedefinedEnrouteDecoratorException(f"{decorator!r} can be used only once.") + ans[decorator] = self._build_one(name, decorator.pre_fn_name) return ans def _build_one(self, name: str, pref_fn_name: str) -> Callable: diff --git a/minos/networks/decorators/definitions/__init__.py b/minos/networks/decorators/definitions/__init__.py index eeca14e5..7dae0e73 100644 --- a/minos/networks/decorators/definitions/__init__.py +++ b/minos/networks/decorators/definitions/__init__.py @@ -10,10 +10,6 @@ from .kinds import ( EnrouteDecoratorKind, ) -from .periodic import ( - PeriodicEnrouteDecorator, - PeriodicEventEnrouteDecorator, -) from .rest import ( RestCommandEnrouteDecorator, RestEnrouteDecorator, diff --git a/minos/networks/decorators/definitions/periodic.py b/minos/networks/decorators/definitions/periodic.py deleted file mode 100644 index c2ba5122..00000000 --- a/minos/networks/decorators/definitions/periodic.py +++ /dev/null @@ -1,40 +0,0 @@ -from abc import ( - ABC, -) -from typing import ( - Final, - Iterable, - Union, -) - -from crontab import ( - CronTab, -) - -from .abc import ( - EnrouteDecorator, -) -from .kinds import ( - EnrouteDecoratorKind, -) - - -class PeriodicEnrouteDecorator(EnrouteDecorator, ABC): - """Periodic Enroute class""" - - def __init__(self, crontab: Union[str, CronTab]): - if isinstance(crontab, str): - crontab = CronTab(crontab) - self.crontab = crontab - - def __iter__(self) -> Iterable: - yield from (self.crontab,) - - def __hash__(self): - return hash(tuple((s if not isinstance(s, CronTab) else s.matchers) for s in self)) - - -class PeriodicEventEnrouteDecorator(PeriodicEnrouteDecorator): - """Periodic Command Enroute class""" - - KIND: Final[EnrouteDecoratorKind] = EnrouteDecoratorKind.Event diff --git a/minos/networks/messages.py b/minos/networks/messages.py index 180942de..c9564e17 100644 --- a/minos/networks/messages.py +++ b/minos/networks/messages.py @@ -123,9 +123,6 @@ def __eq__(self, other: Response) -> bool: def __repr__(self) -> str: return f"{type(self).__name__}({self._data!r})" - def __hash__(self): - return hash(self._data) - class ResponseException(MinosException): """Response Exception class.""" diff --git a/poetry.lock b/poetry.lock index 6c9e2483..d7d9b2f4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -254,14 +254,6 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] toml = ["toml"] -[[package]] -name = "crontab" -version = "0.23.0" -description = "Parse and use crontab schedules in Python" -category = "main" -optional = false -python-versions = "*" - [[package]] name = "dependency-injector" version = "4.36.0" @@ -910,7 +902,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "9be4268bb344ac8e3b15799e3855c5646cf7fa18884e6b43774da86b8ba9c8e0" +content-hash = "6eb8fe705fd1bb8709ec7475c4605438f8d162f3d93cc44e11f36c4543619e68" [metadata.files] aiohttp = [ @@ -1148,9 +1140,6 @@ coverage = [ {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, ] -crontab = [ - {file = "crontab-0.23.0.tar.gz", hash = "sha256:ca79dede9c2f572bb32f38703e8fddcf3427e86edc838f2ffe7ae4b9ee2b0733"}, -] dependency-injector = [ {file = "dependency-injector-4.36.0.tar.gz", hash = "sha256:a5f6090062c7d19947a65fad932f37992cc8a8558d375f040322b72b244a135f"}, {file = "dependency_injector-4.36.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:01dfc4aaee8bdd6b12e13a052571501eaa2e395c1729f00d956bdf62b34d8436"}, @@ -1307,22 +1296,12 @@ m2r2 = [ {file = "m2r2-0.2.8.tar.gz", hash = "sha256:ca39e1db74991818d667c7367e4fc2de13ecefd2a04d69d83b0ffa76d20d7e29"}, ] markupsafe = [ - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -1331,21 +1310,14 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -1355,9 +1327,6 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, diff --git a/pyproject.toml b/pyproject.toml index 29d5c41f..01bec400 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,6 @@ aiopg = "^1.2.1" aiohttp = "^3.7.4" dependency-injector = "^4.32.2" minos-microservice-common = "^0.1.13" -crontab = "^0.23.0" [tool.poetry.dev-dependencies] black = "^19.10b" diff --git a/tests/test_networks/test_decorators/test_analyzers.py b/tests/test_networks/test_decorators/test_analyzers.py index ab1bfae5..9ca612d8 100644 --- a/tests/test_networks/test_decorators/test_analyzers.py +++ b/tests/test_networks/test_decorators/test_analyzers.py @@ -8,7 +8,6 @@ BrokerEventEnrouteDecorator, BrokerQueryEnrouteDecorator, EnrouteAnalyzer, - PeriodicEventEnrouteDecorator, RestCommandEnrouteDecorator, RestQueryEnrouteDecorator, ) @@ -39,8 +38,6 @@ def test_get_all(self): BrokerCommandEnrouteDecorator("DeleteTicket"), RestCommandEnrouteDecorator("orders/", "DELETE"), }, - "send_newsletter": {PeriodicEventEnrouteDecorator("@daily")}, - "check_inactive_users": {PeriodicEventEnrouteDecorator("@daily")}, } self.assertEqual(expected, observed) @@ -96,17 +93,6 @@ def test_get_broker_event(self): self.assertEqual(expected, observed) - def test_get_periodic_event(self): - analyzer = EnrouteAnalyzer(FakeService) - - observed = analyzer.get_periodic_event() - expected = { - "send_newsletter": {PeriodicEventEnrouteDecorator("@daily")}, - "check_inactive_users": {PeriodicEventEnrouteDecorator("@daily")}, - } - - self.assertEqual(expected, observed) - def test_with_get_enroute(self): analyzer = EnrouteAnalyzer(FakeServiceWithGetEnroute) diff --git a/tests/test_networks/test_decorators/test_api.py b/tests/test_networks/test_decorators/test_api.py index 3ac33cd1..4b055068 100644 --- a/tests/test_networks/test_decorators/test_api.py +++ b/tests/test_networks/test_decorators/test_api.py @@ -4,7 +4,6 @@ BrokerCommandEnrouteDecorator, BrokerEventEnrouteDecorator, BrokerQueryEnrouteDecorator, - PeriodicEventEnrouteDecorator, RestCommandEnrouteDecorator, RestQueryEnrouteDecorator, enroute, @@ -36,10 +35,6 @@ def test_broker_event_decorators(self): decorator = enroute.broker.event("CreateTicket") self.assertEqual(BrokerEventEnrouteDecorator("CreateTicket"), decorator) - def test_periodic_command_decorators(self): - decorator = enroute.periodic.event("0 */2 * * *") - self.assertEqual(PeriodicEventEnrouteDecorator("0 */2 * * *"), decorator) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_networks/test_decorators/test_builders.py b/tests/test_networks/test_decorators/test_builders.py index f6a67172..b18a3588 100644 --- a/tests/test_networks/test_decorators/test_builders.py +++ b/tests/test_networks/test_decorators/test_builders.py @@ -9,7 +9,6 @@ BrokerQueryEnrouteDecorator, EnrouteBuilder, MinosRedefinedEnrouteDecoratorException, - PeriodicEventEnrouteDecorator, Response, RestCommandEnrouteDecorator, RestQueryEnrouteDecorator, @@ -54,15 +53,6 @@ async def test_get_broker_event(self): observed = await handlers[BrokerEventEnrouteDecorator("TicketAdded")](self.request) self.assertEqual(expected, observed) - async def test_get_periodic_event(self): - handlers = self.builder.get_periodic_event() - self.assertEqual(1, len(handlers)) - - expected = {Response("newsletter sent!"), Response("checked inactive users!")} - # noinspection PyTypeChecker - observed = set(await handlers[PeriodicEventEnrouteDecorator("@daily")](self.request)) - self.assertEqual(expected, observed) - async def test_get_broker_command_query(self): handlers = self.builder.get_broker_command_query() self.assertEqual(4, len(handlers)) diff --git a/tests/test_networks/test_messages.py b/tests/test_networks/test_messages.py index 6829b8a3..fba66c0f 100644 --- a/tests/test_networks/test_messages.py +++ b/tests/test_networks/test_messages.py @@ -85,9 +85,6 @@ async def test_repr(self): response = Response(self.data) self.assertEqual("Response([FakeModel(text=blue), FakeModel(text=red)])", repr(response)) - def test_hash(self): - self.assertIsInstance(hash(Response("test")), int) - if __name__ == "__main__": unittest.main() diff --git a/tests/utils.py b/tests/utils.py index 48b08253..e9d1c3fb 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -211,16 +211,6 @@ async def ticket_added(request: Request) -> Response: """For testing purposes.""" return Response(": ".join(("Ticket Added", await request.content(),))) - @enroute.periodic.event("@daily") - async def send_newsletter(self, request: Request): - """For testing purposes.""" - return Response("newsletter sent!") - - @enroute.periodic.event("@daily") - async def check_inactive_users(self, request: Request): - """For testing purposes.""" - return Response("checked inactive users!") - # noinspection PyMethodMayBeStatic,PyUnusedLocal def bar(self, request: Request): """For testing purposes.""" From 93f978ef2b85b41e2a37c0c0038975f11fe80fd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Garc=C3=ADa=20Prado?= Date: Tue, 28 Sep 2021 15:58:20 +0200 Subject: [PATCH 03/21] ISSUE #398 * Implement `hash` into `minos.networks.Response`. * Add `minos.networks.PeriodicEnrouteDecorator`. * Allow to setup multiple handling functions for same event. * Add `crontab` dependency. --- minos/networks/__init__.py | 2 + minos/networks/decorators/__init__.py | 2 + minos/networks/decorators/analyzers.py | 9 ++++ minos/networks/decorators/api.py | 12 +++++ minos/networks/decorators/builders.py | 50 +++++++++++++++---- .../decorators/definitions/__init__.py | 4 ++ .../decorators/definitions/periodic.py | 40 +++++++++++++++ minos/networks/messages.py | 3 ++ poetry.lock | 33 +++++++++++- pyproject.toml | 1 + .../test_decorators/test_analyzers.py | 14 ++++++ .../test_networks/test_decorators/test_api.py | 5 ++ .../test_decorators/test_builders.py | 10 ++++ tests/test_networks/test_messages.py | 3 ++ tests/utils.py | 10 ++++ 15 files changed, 187 insertions(+), 11 deletions(-) create mode 100644 minos/networks/decorators/definitions/periodic.py diff --git a/minos/networks/__init__.py b/minos/networks/__init__.py index 80aabab2..2922add4 100644 --- a/minos/networks/__init__.py +++ b/minos/networks/__init__.py @@ -18,6 +18,8 @@ EnrouteBuilder, EnrouteDecorator, EnrouteDecoratorKind, + PeriodicEnrouteDecorator, + PeriodicEventEnrouteDecorator, RestCommandEnrouteDecorator, RestEnrouteDecorator, RestQueryEnrouteDecorator, diff --git a/minos/networks/decorators/__init__.py b/minos/networks/decorators/__init__.py index 6fdbc8d1..f48ff701 100644 --- a/minos/networks/decorators/__init__.py +++ b/minos/networks/decorators/__init__.py @@ -14,6 +14,8 @@ BrokerQueryEnrouteDecorator, EnrouteDecorator, EnrouteDecoratorKind, + PeriodicEnrouteDecorator, + PeriodicEventEnrouteDecorator, RestCommandEnrouteDecorator, RestEnrouteDecorator, RestQueryEnrouteDecorator, diff --git a/minos/networks/decorators/analyzers.py b/minos/networks/decorators/analyzers.py index 8c41cc43..b449eca1 100644 --- a/minos/networks/decorators/analyzers.py +++ b/minos/networks/decorators/analyzers.py @@ -21,6 +21,7 @@ BrokerEventEnrouteDecorator, BrokerQueryEnrouteDecorator, EnrouteDecorator, + PeriodicEventEnrouteDecorator, RestCommandEnrouteDecorator, RestEnrouteDecorator, RestQueryEnrouteDecorator, @@ -71,6 +72,14 @@ def get_broker_event(self) -> dict[str, set[BrokerEnrouteDecorator]]: # noinspection PyTypeChecker return self._get_items({BrokerEventEnrouteDecorator}) + def get_periodic_event(self) -> dict[str, set[PeriodicEventEnrouteDecorator]]: + """TODO + + :return: TODO + """ + # noinspection PyTypeChecker + return self._get_items({PeriodicEventEnrouteDecorator}) + def _get_items(self, expected_types: set[Type[EnrouteDecorator]]) -> dict[str, set[EnrouteDecorator]]: items = dict() for fn, decorators in self.get_all().items(): diff --git a/minos/networks/decorators/api.py b/minos/networks/decorators/api.py index eeb32988..b3eddd75 100644 --- a/minos/networks/decorators/api.py +++ b/minos/networks/decorators/api.py @@ -1,7 +1,12 @@ +from __future__ import ( + annotations, +) + from .definitions import ( BrokerCommandEnrouteDecorator, BrokerEventEnrouteDecorator, BrokerQueryEnrouteDecorator, + PeriodicEventEnrouteDecorator, RestCommandEnrouteDecorator, RestQueryEnrouteDecorator, ) @@ -22,11 +27,18 @@ class RestEnroute: query = RestQueryEnrouteDecorator +class PeriodicEnroute: + """Periodic Enroute class.""" + + event = PeriodicEventEnrouteDecorator + + class Enroute: """Enroute decorator main class""" broker = BrokerEnroute rest = RestEnroute + periodic = PeriodicEnroute enroute = Enroute diff --git a/minos/networks/decorators/builders.py b/minos/networks/decorators/builders.py index 356a53d6..15ac3bcc 100644 --- a/minos/networks/decorators/builders.py +++ b/minos/networks/decorators/builders.py @@ -1,3 +1,9 @@ +from asyncio import ( + gather, +) +from collections import ( + defaultdict, +) from inspect import ( iscoroutinefunction, ) @@ -26,9 +32,13 @@ from .definitions import ( BrokerEnrouteDecorator, EnrouteDecorator, + EnrouteDecoratorKind, + PeriodicEnrouteDecorator, RestEnrouteDecorator, ) +Handler = Callable[[Request], Awaitable[Response]] + class EnrouteBuilder: """Enroute builder class.""" @@ -40,7 +50,7 @@ def __init__(self, decorated: Union[str, Type], *args, **kwargs): self.decorated = decorated self.analyzer = EnrouteAnalyzer(decorated, *args, **kwargs) - def get_rest_command_query(self) -> dict[RestEnrouteDecorator, Callable[[Request], Awaitable[Response]]]: + def get_rest_command_query(self) -> dict[RestEnrouteDecorator, Handler]: """Get the rest handlers for commands and queries. :return: A dictionary with decorator classes as keys and callable handlers as values. @@ -49,7 +59,7 @@ def get_rest_command_query(self) -> dict[RestEnrouteDecorator, Callable[[Request # noinspection PyTypeChecker return self._build(mapping) - def get_broker_command_query(self) -> dict[BrokerEnrouteDecorator, Callable[[Request], Awaitable[Response]]]: + def get_broker_command_query(self) -> dict[BrokerEnrouteDecorator, Handler]: """Get the broker handlers for commands and queries. :return: A dictionary with decorator classes as keys and callable handlers as values. @@ -58,7 +68,7 @@ def get_broker_command_query(self) -> dict[BrokerEnrouteDecorator, Callable[[Req # noinspection PyTypeChecker return self._build(mapping) - def get_broker_event(self) -> dict[BrokerEnrouteDecorator, Callable[[Request], Awaitable[Response]]]: + def get_broker_event(self) -> dict[BrokerEnrouteDecorator, Handler]: """Get the broker handlers for events. :return: A dictionary with decorator classes as keys and callable handlers as values. @@ -67,16 +77,36 @@ def get_broker_event(self) -> dict[BrokerEnrouteDecorator, Callable[[Request], A # noinspection PyTypeChecker return self._build(mapping) - def _build( - self, mapping: dict[str, set[EnrouteDecorator]] - ) -> dict[EnrouteDecorator, Callable[[Request], Awaitable[Response]]]: + def get_periodic_event(self) -> dict[PeriodicEnrouteDecorator, Handler]: + """TODO + + :return: A dictionary with decorator classes as keys and callable handlers as values. + """ + mapping = self.analyzer.get_periodic_event() + # noinspection PyTypeChecker + return self._build(mapping) - ans = dict() + def _build(self, mapping: dict[str, set[EnrouteDecorator]]) -> dict[EnrouteDecorator, Handler]: + + ans = defaultdict(set) for name, decorators in mapping.items(): for decorator in decorators: - if decorator in ans: - raise MinosRedefinedEnrouteDecoratorException(f"{decorator!r} can be used only once.") - ans[decorator] = self._build_one(name, decorator.pre_fn_name) + ans[decorator].add(self._build_one(name, decorator.pre_fn_name)) + + def _make_fn(d, fns: set[Handler]) -> Handler: + if len(fns) == 1: + return next(iter(fns)) + + if d.KIND != EnrouteDecoratorKind.Event: + raise MinosRedefinedEnrouteDecoratorException(f"{d!r} can be used only once.") + + async def _fn(*args, **kwargs): + return await gather(*(fn(*args, **kwargs) for fn in fns)) + + return _fn + + ans = {decorator: _make_fn(decorator, fns) for decorator, fns in ans.items()} + return ans def _build_one(self, name: str, pref_fn_name: str) -> Callable: diff --git a/minos/networks/decorators/definitions/__init__.py b/minos/networks/decorators/definitions/__init__.py index 7dae0e73..eeca14e5 100644 --- a/minos/networks/decorators/definitions/__init__.py +++ b/minos/networks/decorators/definitions/__init__.py @@ -10,6 +10,10 @@ from .kinds import ( EnrouteDecoratorKind, ) +from .periodic import ( + PeriodicEnrouteDecorator, + PeriodicEventEnrouteDecorator, +) from .rest import ( RestCommandEnrouteDecorator, RestEnrouteDecorator, diff --git a/minos/networks/decorators/definitions/periodic.py b/minos/networks/decorators/definitions/periodic.py new file mode 100644 index 00000000..c2ba5122 --- /dev/null +++ b/minos/networks/decorators/definitions/periodic.py @@ -0,0 +1,40 @@ +from abc import ( + ABC, +) +from typing import ( + Final, + Iterable, + Union, +) + +from crontab import ( + CronTab, +) + +from .abc import ( + EnrouteDecorator, +) +from .kinds import ( + EnrouteDecoratorKind, +) + + +class PeriodicEnrouteDecorator(EnrouteDecorator, ABC): + """Periodic Enroute class""" + + def __init__(self, crontab: Union[str, CronTab]): + if isinstance(crontab, str): + crontab = CronTab(crontab) + self.crontab = crontab + + def __iter__(self) -> Iterable: + yield from (self.crontab,) + + def __hash__(self): + return hash(tuple((s if not isinstance(s, CronTab) else s.matchers) for s in self)) + + +class PeriodicEventEnrouteDecorator(PeriodicEnrouteDecorator): + """Periodic Command Enroute class""" + + KIND: Final[EnrouteDecoratorKind] = EnrouteDecoratorKind.Event diff --git a/minos/networks/messages.py b/minos/networks/messages.py index c9564e17..180942de 100644 --- a/minos/networks/messages.py +++ b/minos/networks/messages.py @@ -123,6 +123,9 @@ def __eq__(self, other: Response) -> bool: def __repr__(self) -> str: return f"{type(self).__name__}({self._data!r})" + def __hash__(self): + return hash(self._data) + class ResponseException(MinosException): """Response Exception class.""" diff --git a/poetry.lock b/poetry.lock index d7d9b2f4..6c9e2483 100644 --- a/poetry.lock +++ b/poetry.lock @@ -254,6 +254,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] toml = ["toml"] +[[package]] +name = "crontab" +version = "0.23.0" +description = "Parse and use crontab schedules in Python" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "dependency-injector" version = "4.36.0" @@ -902,7 +910,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "6eb8fe705fd1bb8709ec7475c4605438f8d162f3d93cc44e11f36c4543619e68" +content-hash = "9be4268bb344ac8e3b15799e3855c5646cf7fa18884e6b43774da86b8ba9c8e0" [metadata.files] aiohttp = [ @@ -1140,6 +1148,9 @@ coverage = [ {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, ] +crontab = [ + {file = "crontab-0.23.0.tar.gz", hash = "sha256:ca79dede9c2f572bb32f38703e8fddcf3427e86edc838f2ffe7ae4b9ee2b0733"}, +] dependency-injector = [ {file = "dependency-injector-4.36.0.tar.gz", hash = "sha256:a5f6090062c7d19947a65fad932f37992cc8a8558d375f040322b72b244a135f"}, {file = "dependency_injector-4.36.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:01dfc4aaee8bdd6b12e13a052571501eaa2e395c1729f00d956bdf62b34d8436"}, @@ -1296,12 +1307,22 @@ m2r2 = [ {file = "m2r2-0.2.8.tar.gz", hash = "sha256:ca39e1db74991818d667c7367e4fc2de13ecefd2a04d69d83b0ffa76d20d7e29"}, ] markupsafe = [ + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -1310,14 +1331,21 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -1327,6 +1355,9 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, diff --git a/pyproject.toml b/pyproject.toml index 01bec400..29d5c41f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ aiopg = "^1.2.1" aiohttp = "^3.7.4" dependency-injector = "^4.32.2" minos-microservice-common = "^0.1.13" +crontab = "^0.23.0" [tool.poetry.dev-dependencies] black = "^19.10b" diff --git a/tests/test_networks/test_decorators/test_analyzers.py b/tests/test_networks/test_decorators/test_analyzers.py index 9ca612d8..ab1bfae5 100644 --- a/tests/test_networks/test_decorators/test_analyzers.py +++ b/tests/test_networks/test_decorators/test_analyzers.py @@ -8,6 +8,7 @@ BrokerEventEnrouteDecorator, BrokerQueryEnrouteDecorator, EnrouteAnalyzer, + PeriodicEventEnrouteDecorator, RestCommandEnrouteDecorator, RestQueryEnrouteDecorator, ) @@ -38,6 +39,8 @@ def test_get_all(self): BrokerCommandEnrouteDecorator("DeleteTicket"), RestCommandEnrouteDecorator("orders/", "DELETE"), }, + "send_newsletter": {PeriodicEventEnrouteDecorator("@daily")}, + "check_inactive_users": {PeriodicEventEnrouteDecorator("@daily")}, } self.assertEqual(expected, observed) @@ -93,6 +96,17 @@ def test_get_broker_event(self): self.assertEqual(expected, observed) + def test_get_periodic_event(self): + analyzer = EnrouteAnalyzer(FakeService) + + observed = analyzer.get_periodic_event() + expected = { + "send_newsletter": {PeriodicEventEnrouteDecorator("@daily")}, + "check_inactive_users": {PeriodicEventEnrouteDecorator("@daily")}, + } + + self.assertEqual(expected, observed) + def test_with_get_enroute(self): analyzer = EnrouteAnalyzer(FakeServiceWithGetEnroute) diff --git a/tests/test_networks/test_decorators/test_api.py b/tests/test_networks/test_decorators/test_api.py index 4b055068..3ac33cd1 100644 --- a/tests/test_networks/test_decorators/test_api.py +++ b/tests/test_networks/test_decorators/test_api.py @@ -4,6 +4,7 @@ BrokerCommandEnrouteDecorator, BrokerEventEnrouteDecorator, BrokerQueryEnrouteDecorator, + PeriodicEventEnrouteDecorator, RestCommandEnrouteDecorator, RestQueryEnrouteDecorator, enroute, @@ -35,6 +36,10 @@ def test_broker_event_decorators(self): decorator = enroute.broker.event("CreateTicket") self.assertEqual(BrokerEventEnrouteDecorator("CreateTicket"), decorator) + def test_periodic_command_decorators(self): + decorator = enroute.periodic.event("0 */2 * * *") + self.assertEqual(PeriodicEventEnrouteDecorator("0 */2 * * *"), decorator) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_networks/test_decorators/test_builders.py b/tests/test_networks/test_decorators/test_builders.py index b18a3588..f6a67172 100644 --- a/tests/test_networks/test_decorators/test_builders.py +++ b/tests/test_networks/test_decorators/test_builders.py @@ -9,6 +9,7 @@ BrokerQueryEnrouteDecorator, EnrouteBuilder, MinosRedefinedEnrouteDecoratorException, + PeriodicEventEnrouteDecorator, Response, RestCommandEnrouteDecorator, RestQueryEnrouteDecorator, @@ -53,6 +54,15 @@ async def test_get_broker_event(self): observed = await handlers[BrokerEventEnrouteDecorator("TicketAdded")](self.request) self.assertEqual(expected, observed) + async def test_get_periodic_event(self): + handlers = self.builder.get_periodic_event() + self.assertEqual(1, len(handlers)) + + expected = {Response("newsletter sent!"), Response("checked inactive users!")} + # noinspection PyTypeChecker + observed = set(await handlers[PeriodicEventEnrouteDecorator("@daily")](self.request)) + self.assertEqual(expected, observed) + async def test_get_broker_command_query(self): handlers = self.builder.get_broker_command_query() self.assertEqual(4, len(handlers)) diff --git a/tests/test_networks/test_messages.py b/tests/test_networks/test_messages.py index fba66c0f..6829b8a3 100644 --- a/tests/test_networks/test_messages.py +++ b/tests/test_networks/test_messages.py @@ -85,6 +85,9 @@ async def test_repr(self): response = Response(self.data) self.assertEqual("Response([FakeModel(text=blue), FakeModel(text=red)])", repr(response)) + def test_hash(self): + self.assertIsInstance(hash(Response("test")), int) + if __name__ == "__main__": unittest.main() diff --git a/tests/utils.py b/tests/utils.py index e9d1c3fb..48b08253 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -211,6 +211,16 @@ async def ticket_added(request: Request) -> Response: """For testing purposes.""" return Response(": ".join(("Ticket Added", await request.content(),))) + @enroute.periodic.event("@daily") + async def send_newsletter(self, request: Request): + """For testing purposes.""" + return Response("newsletter sent!") + + @enroute.periodic.event("@daily") + async def check_inactive_users(self, request: Request): + """For testing purposes.""" + return Response("checked inactive users!") + # noinspection PyMethodMayBeStatic,PyUnusedLocal def bar(self, request: Request): """For testing purposes.""" From f009d9469b4cda30bf4ba48e7f0bcb034ad0c5c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Garc=C3=ADa=20Prado?= Date: Tue, 28 Sep 2021 17:25:15 +0200 Subject: [PATCH 04/21] ISSUE #399 * Add alpha version of `minos.networks.scheduling`. --- minos/networks/__init__.py | 5 ++ minos/networks/scheduling/__init__.py | 9 +++ minos/networks/scheduling/messages.py | 44 ++++++++++ minos/networks/scheduling/schedulers.py | 102 ++++++++++++++++++++++++ minos/networks/scheduling/services.py | 52 ++++++++++++ 5 files changed, 212 insertions(+) create mode 100644 minos/networks/scheduling/__init__.py create mode 100644 minos/networks/scheduling/messages.py create mode 100644 minos/networks/scheduling/schedulers.py create mode 100644 minos/networks/scheduling/services.py diff --git a/minos/networks/__init__.py b/minos/networks/__init__.py index 2922add4..578f0342 100644 --- a/minos/networks/__init__.py +++ b/minos/networks/__init__.py @@ -72,6 +72,11 @@ RestResponseException, RestService, ) +from .scheduling import ( + SchedulingRequest, + TaskScheduler, + TaskSchedulerService, +) from .snapshots import ( SnapshotService, ) diff --git a/minos/networks/scheduling/__init__.py b/minos/networks/scheduling/__init__.py new file mode 100644 index 00000000..f125f5c9 --- /dev/null +++ b/minos/networks/scheduling/__init__.py @@ -0,0 +1,9 @@ +from .messages import ( + SchedulingRequest, +) +from .schedulers import ( + TaskScheduler, +) +from .services import ( + TaskSchedulerService, +) diff --git a/minos/networks/scheduling/messages.py b/minos/networks/scheduling/messages.py new file mode 100644 index 00000000..077e1733 --- /dev/null +++ b/minos/networks/scheduling/messages.py @@ -0,0 +1,44 @@ +from datetime import ( + datetime, +) +from typing import ( + Any, + Optional, +) +from uuid import ( + UUID, +) + +from ..messages import ( + Request, +) + + +class SchedulingRequest(Request): + """TODO""" + + def __init__(self, scheduled_at: datetime): + super().__init__() + self._scheduled_at = scheduled_at + + @property + def user(self) -> Optional[UUID]: + """TODO + + :return: TODO + """ + return None + + async def content(self, **kwargs) -> Any: + """TODO + + :param kwargs: TODO + :return: TODO + """ + return {"scheduled_at": self._scheduled_at} + + def __eq__(self, other: Request) -> bool: + return False + + def __repr__(self) -> str: + return "" diff --git a/minos/networks/scheduling/schedulers.py b/minos/networks/scheduling/schedulers.py new file mode 100644 index 00000000..ed7f0c6c --- /dev/null +++ b/minos/networks/scheduling/schedulers.py @@ -0,0 +1,102 @@ +from __future__ import ( + annotations, +) + +import logging +from asyncio import ( + AbstractEventLoop, + gather, + get_event_loop, + sleep, +) +from datetime import ( + timedelta, +) +from itertools import ( + chain, +) +from typing import ( + Awaitable, + Callable, + NoReturn, + Optional, +) + +from crontab import ( + CronTab, +) + +from minos.common import ( + MinosConfig, + MinosSetup, + current_datetime, +) + +from ..decorators import ( + EnrouteBuilder, +) +from .messages import ( + SchedulingRequest, +) + +logger = logging.getLogger(__name__) + + +class TaskScheduler(MinosSetup): + """TODO""" + + def __init__( + self, tasks: list[tuple[CronTab, Callable]], loop: Optional[AbstractEventLoop] = None, *args, **kwargs + ): + super().__init__(*args, **kwargs) + if loop is None: + loop = get_event_loop() + + self._tasks = tasks + self._loop = loop + + @classmethod + def _from_config(cls, config: MinosConfig, **kwargs) -> TaskScheduler: + command_decorators = EnrouteBuilder(config.commands.service, config).get_periodic_event() + service_decorators = EnrouteBuilder(config.queries.service, config).get_periodic_event() + + tasks = [ + (decorator.crontab, fn) for decorator, fn in chain(command_decorators.items(), service_decorators.items()) + ] + return cls(tasks) + + async def start(self) -> None: + """TODO + + :return: TODO + """ + + await gather(*(self.callback(*task) for task in self._tasks)) + + @staticmethod + async def callback(crontab: CronTab, fn: Callable[[SchedulingRequest], Awaitable]) -> NoReturn: + """TODO + + :param crontab: TODO + :param fn: TODO + :return: TODO + """ + + now = current_datetime() + while True: + request = SchedulingRequest(now) + try: + await fn(request) + except Exception as exc: # TODO + logger.warning(f"Raised exception while executing task: {exc}") + now = current_datetime() + delay = crontab.next(now) + now += timedelta(seconds=delay) + await sleep(delay) + + async def stop(self) -> None: + """TODO + + :return: TODO + """ + pass diff --git a/minos/networks/scheduling/services.py b/minos/networks/scheduling/services.py new file mode 100644 index 00000000..62897919 --- /dev/null +++ b/minos/networks/scheduling/services.py @@ -0,0 +1,52 @@ +import logging + +from aiomisc import ( + Service, +) +from cached_property import ( + cached_property, +) + +from .schedulers import ( + TaskScheduler, +) + +logger = logging.getLogger(__name__) + + +class TaskSchedulerService(Service): + """TODO""" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._init_kwargs = kwargs + + async def start(self) -> None: + """Start the service execution. + + :return: This method does not return anything. + """ + await self.scheduler.setup() + + try: + self.start_event.set() + except RuntimeError: + logger.warning("Runtime is not properly setup.") + + await self.scheduler.start() + + async def stop(self, exception: Exception = None) -> None: + """Stop the service execution. + + :param exception: Optional exception that stopped the execution. + :return: This method does not return anything. + """ + await self.scheduler.destroy() + + @cached_property + def scheduler(self) -> TaskScheduler: + """Get the service scheduler. + + :return: A ``TaskScheduler`` instance. + """ + return TaskScheduler.from_config(**self._init_kwargs) From 618c3e50651a559c1f8885fe89c946555e540a3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Garc=C3=ADa=20Prado?= Date: Wed, 29 Sep 2021 12:25:41 +0200 Subject: [PATCH 05/21] ISSUE #398 * Improve `EnrouteBuilder` and add support for multiple decorated classes. --- minos/networks/decorators/builders.py | 41 ++++++++++--------- minos/networks/handlers/commands/handlers.py | 18 ++++---- minos/networks/handlers/events/handlers.py | 23 ++--------- minos/networks/rest/handlers.py | 11 ++--- .../test_decorators/test_builders.py | 2 +- 5 files changed, 37 insertions(+), 58 deletions(-) diff --git a/minos/networks/decorators/builders.py b/minos/networks/decorators/builders.py index 15ac3bcc..a2667527 100644 --- a/minos/networks/decorators/builders.py +++ b/minos/networks/decorators/builders.py @@ -43,55 +43,49 @@ class EnrouteBuilder: """Enroute builder class.""" - def __init__(self, decorated: Union[str, Type], *args, **kwargs): - if isinstance(decorated, str): - decorated = import_module(decorated) + def __init__(self, *klasses: Union[str, Type], **kwargs): + klasses = tuple((klass if not isinstance(klass, str) else import_module(klass)) for klass in klasses) - self.decorated = decorated - self.analyzer = EnrouteAnalyzer(decorated, *args, **kwargs) + self._init_kwargs = kwargs + self.klasses = klasses def get_rest_command_query(self) -> dict[RestEnrouteDecorator, Handler]: """Get the rest handlers for commands and queries. :return: A dictionary with decorator classes as keys and callable handlers as values. """ - mapping = self.analyzer.get_rest_command_query() # noinspection PyTypeChecker - return self._build(mapping) + return self._build("get_rest_command_query") def get_broker_command_query(self) -> dict[BrokerEnrouteDecorator, Handler]: """Get the broker handlers for commands and queries. :return: A dictionary with decorator classes as keys and callable handlers as values. """ - mapping = self.analyzer.get_broker_command_query() # noinspection PyTypeChecker - return self._build(mapping) + return self._build("get_broker_command_query") def get_broker_event(self) -> dict[BrokerEnrouteDecorator, Handler]: """Get the broker handlers for events. :return: A dictionary with decorator classes as keys and callable handlers as values. """ - mapping = self.analyzer.get_broker_event() # noinspection PyTypeChecker - return self._build(mapping) + return self._build("get_broker_event") def get_periodic_event(self) -> dict[PeriodicEnrouteDecorator, Handler]: """TODO :return: A dictionary with decorator classes as keys and callable handlers as values. """ - mapping = self.analyzer.get_periodic_event() # noinspection PyTypeChecker - return self._build(mapping) + return self._build("get_periodic_event") - def _build(self, mapping: dict[str, set[EnrouteDecorator]]) -> dict[EnrouteDecorator, Handler]: + def _build(self, method_name: str) -> dict[EnrouteDecorator, Handler]: ans = defaultdict(set) - for name, decorators in mapping.items(): - for decorator in decorators: - ans[decorator].add(self._build_one(name, decorator.pre_fn_name)) + for klass in self.klasses: + self._build_klass(klass, method_name, ans) def _make_fn(d, fns: set[Handler]) -> Handler: if len(fns) == 1: @@ -109,8 +103,17 @@ async def _fn(*args, **kwargs): return ans - def _build_one(self, name: str, pref_fn_name: str) -> Callable: - instance = self.decorated() + def _build_klass(self, klass: type, method_name: str, ans: dict[EnrouteDecorator, set[Handler]]) -> None: + analyzer = EnrouteAnalyzer(klass, **self._init_kwargs) + mapping = getattr(analyzer, method_name)() + + for name, decorators in mapping.items(): + for decorator in decorators: + ans[decorator].add(self._build_one(klass, name, decorator.pre_fn_name)) + + @staticmethod + def _build_one(klass: type, name: str, pref_fn_name: str) -> Handler: + instance = klass() fn = getattr(instance, name) pre_fn = getattr(instance, pref_fn_name, None) diff --git a/minos/networks/handlers/commands/handlers.py b/minos/networks/handlers/commands/handlers.py index 729e3151..6d8f1c7a 100644 --- a/minos/networks/handlers/commands/handlers.py +++ b/minos/networks/handlers/commands/handlers.py @@ -6,9 +6,6 @@ from inspect import ( isawaitable, ) -from itertools import ( - chain, -) from typing import ( Any, Awaitable, @@ -63,16 +60,17 @@ def __init__(self, broker: MinosBroker = Provide["command_reply_broker"], **kwar @classmethod def _from_config(cls, *args, config: MinosConfig, **kwargs) -> CommandHandler: - command_decorators = EnrouteBuilder(config.commands.service, config).get_broker_command_query() - query_decorators = EnrouteBuilder(config.queries.service, config).get_broker_command_query() - - handlers = { - decorator.topic: fn for decorator, fn in chain(command_decorators.items(), query_decorators.items()) - } - + handlers = cls._handlers_from_config(config) # noinspection PyProtectedMember return cls(handlers=handlers, **config.broker.queue._asdict(), **kwargs) + @staticmethod + def _handlers_from_config(config: MinosConfig) -> dict[str, Callable[[HandlerRequest], Awaitable]]: + builder = EnrouteBuilder(config.commands.service, config.queries.service, config=config) + decorators = builder.get_broker_command_query() + handlers = {decorator.topic: fn for decorator, fn in decorators.items()} + return handlers + async def dispatch_one(self, entry: HandlerEntry[Command]) -> None: """Dispatch one row. diff --git a/minos/networks/handlers/events/handlers.py b/minos/networks/handlers/events/handlers.py index 1a1a4db7..a8002fb1 100644 --- a/minos/networks/handlers/events/handlers.py +++ b/minos/networks/handlers/events/handlers.py @@ -12,9 +12,6 @@ from inspect import ( isawaitable, ) -from itertools import ( - chain, -) from operator import ( attrgetter, ) @@ -63,23 +60,9 @@ def _from_config(cls, *args, config: MinosConfig, **kwargs) -> EventHandler: @staticmethod def _handlers_from_config(config: MinosConfig) -> dict[str, Callable[[HandlerRequest], Awaitable]]: - command_decorators = EnrouteBuilder(config.commands.service, config).get_broker_event() - query_decorators = EnrouteBuilder(config.queries.service, config).get_broker_event() - - handlers = defaultdict(set) - for decorator, fn in chain(command_decorators.items(), query_decorators.items()): - handlers[decorator.topic].add(fn) - - def _make_fn(fns: set[Callable]) -> Callable: - if len(fns) == 1: - return next(iter(fns)) - - async def _fn(*args, **kwargs): - return await gather(*(fn(*args, **kwargs) for fn in fns)) - - return _fn - - handlers = {topic: _make_fn(fns) for topic, fns in handlers.items()} + builder = EnrouteBuilder(config.commands.service, config.queries.service, config=config) + handlers = builder.get_broker_event() + handlers = {decorator.topic: fn for decorator, fn in handlers.items()} return handlers async def _dispatch_entries(self, entries: list[HandlerEntry[Event]]) -> None: diff --git a/minos/networks/rest/handlers.py b/minos/networks/rest/handlers.py index 2967fff4..d5f14e77 100644 --- a/minos/networks/rest/handlers.py +++ b/minos/networks/rest/handlers.py @@ -9,9 +9,6 @@ from inspect import ( isawaitable, ) -from itertools import ( - chain, -) from typing import ( Awaitable, Callable, @@ -71,11 +68,9 @@ def _from_config(cls, *args, config: MinosConfig, **kwargs) -> RestHandler: @staticmethod def _endpoints_from_config(config: MinosConfig) -> dict[(str, str), Callable]: - command_decorators = EnrouteBuilder(config.commands.service, config).get_rest_command_query() - query_decorators = EnrouteBuilder(config.queries.service, config).get_rest_command_query() - - endpoints = chain(command_decorators.items(), query_decorators.items()) - endpoints = {(decorator.url, decorator.method): fn for decorator, fn in endpoints} + builder = EnrouteBuilder(config.commands.service, config.queries.service, config=config) + decorators = builder.get_rest_command_query() + endpoints = {(decorator.url, decorator.method): fn for decorator, fn in decorators.items()} return endpoints @property diff --git a/tests/test_networks/test_decorators/test_builders.py b/tests/test_networks/test_decorators/test_builders.py index f6a67172..d82a9315 100644 --- a/tests/test_networks/test_decorators/test_builders.py +++ b/tests/test_networks/test_decorators/test_builders.py @@ -28,7 +28,7 @@ def setUp(self) -> None: def test_instance_str(self): builder = EnrouteBuilder(classname(FakeService)) - self.assertEqual(FakeService, builder.decorated) + self.assertEqual((FakeService,), builder.klasses) async def test_get_rest_command_query(self): handlers = self.builder.get_rest_command_query() From 50b314dfaad35f39db0834a18e8b04528cb45d06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Garc=C3=ADa=20Prado?= Date: Wed, 29 Sep 2021 12:32:05 +0200 Subject: [PATCH 06/21] ISSUE #398 * Propagate kwargs. --- minos/networks/decorators/analyzers.py | 3 +- minos/networks/decorators/builders.py | 35 ++++++++++---------- minos/networks/handlers/commands/handlers.py | 8 ++--- minos/networks/handlers/events/handlers.py | 8 ++--- minos/networks/rest/handlers.py | 6 ++-- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/minos/networks/decorators/analyzers.py b/minos/networks/decorators/analyzers.py index b449eca1..19255219 100644 --- a/minos/networks/decorators/analyzers.py +++ b/minos/networks/decorators/analyzers.py @@ -31,7 +31,8 @@ class EnrouteAnalyzer: """Search decorators in specified class""" - def __init__(self, decorated: Union[str, Type], config: Optional[MinosConfig] = None): + # noinspection PyUnusedLocal + def __init__(self, decorated: Union[str, Type], config: Optional[MinosConfig] = None, **kwargs): if isinstance(decorated, str): decorated = import_module(decorated) diff --git a/minos/networks/decorators/builders.py b/minos/networks/decorators/builders.py index a2667527..6e79fea1 100644 --- a/minos/networks/decorators/builders.py +++ b/minos/networks/decorators/builders.py @@ -43,49 +43,48 @@ class EnrouteBuilder: """Enroute builder class.""" - def __init__(self, *klasses: Union[str, Type], **kwargs): + def __init__(self, *klasses: Union[str, Type]): klasses = tuple((klass if not isinstance(klass, str) else import_module(klass)) for klass in klasses) - self._init_kwargs = kwargs self.klasses = klasses - def get_rest_command_query(self) -> dict[RestEnrouteDecorator, Handler]: + def get_rest_command_query(self, **kwargs) -> dict[RestEnrouteDecorator, Handler]: """Get the rest handlers for commands and queries. :return: A dictionary with decorator classes as keys and callable handlers as values. """ # noinspection PyTypeChecker - return self._build("get_rest_command_query") + return self._build("get_rest_command_query", **kwargs) - def get_broker_command_query(self) -> dict[BrokerEnrouteDecorator, Handler]: + def get_broker_command_query(self, **kwargs) -> dict[BrokerEnrouteDecorator, Handler]: """Get the broker handlers for commands and queries. :return: A dictionary with decorator classes as keys and callable handlers as values. """ # noinspection PyTypeChecker - return self._build("get_broker_command_query") + return self._build("get_broker_command_query", **kwargs) - def get_broker_event(self) -> dict[BrokerEnrouteDecorator, Handler]: + def get_broker_event(self, **kwargs) -> dict[BrokerEnrouteDecorator, Handler]: """Get the broker handlers for events. :return: A dictionary with decorator classes as keys and callable handlers as values. """ # noinspection PyTypeChecker - return self._build("get_broker_event") + return self._build("get_broker_event", **kwargs) - def get_periodic_event(self) -> dict[PeriodicEnrouteDecorator, Handler]: + def get_periodic_event(self, **kwargs) -> dict[PeriodicEnrouteDecorator, Handler]: """TODO :return: A dictionary with decorator classes as keys and callable handlers as values. """ # noinspection PyTypeChecker - return self._build("get_periodic_event") + return self._build("get_periodic_event", **kwargs) - def _build(self, method_name: str) -> dict[EnrouteDecorator, Handler]: + def _build(self, method_name: str, **kwargs) -> dict[EnrouteDecorator, Handler]: ans = defaultdict(set) for klass in self.klasses: - self._build_klass(klass, method_name, ans) + self._build_klass(klass, method_name, ans, **kwargs) def _make_fn(d, fns: set[Handler]) -> Handler: if len(fns) == 1: @@ -94,8 +93,8 @@ def _make_fn(d, fns: set[Handler]) -> Handler: if d.KIND != EnrouteDecoratorKind.Event: raise MinosRedefinedEnrouteDecoratorException(f"{d!r} can be used only once.") - async def _fn(*args, **kwargs): - return await gather(*(fn(*args, **kwargs) for fn in fns)) + async def _fn(*ag, **kw): + return await gather(*(fn(*ag, **kw) for fn in fns)) return _fn @@ -103,8 +102,8 @@ async def _fn(*args, **kwargs): return ans - def _build_klass(self, klass: type, method_name: str, ans: dict[EnrouteDecorator, set[Handler]]) -> None: - analyzer = EnrouteAnalyzer(klass, **self._init_kwargs) + def _build_klass(self, klass: type, method_name: str, ans: dict[EnrouteDecorator, set[Handler]], **kwargs) -> None: + analyzer = EnrouteAnalyzer(klass, **kwargs) mapping = getattr(analyzer, method_name)() for name, decorators in mapping.items(): @@ -112,8 +111,8 @@ def _build_klass(self, klass: type, method_name: str, ans: dict[EnrouteDecorator ans[decorator].add(self._build_one(klass, name, decorator.pre_fn_name)) @staticmethod - def _build_one(klass: type, name: str, pref_fn_name: str) -> Handler: - instance = klass() + def _build_one(klass: type, name: str, pref_fn_name: str, **kwargs) -> Handler: + instance = klass(**kwargs) fn = getattr(instance, name) pre_fn = getattr(instance, pref_fn_name, None) diff --git a/minos/networks/handlers/commands/handlers.py b/minos/networks/handlers/commands/handlers.py index 6d8f1c7a..585eabda 100644 --- a/minos/networks/handlers/commands/handlers.py +++ b/minos/networks/handlers/commands/handlers.py @@ -60,14 +60,14 @@ def __init__(self, broker: MinosBroker = Provide["command_reply_broker"], **kwar @classmethod def _from_config(cls, *args, config: MinosConfig, **kwargs) -> CommandHandler: - handlers = cls._handlers_from_config(config) + handlers = cls._handlers_from_config(config, **kwargs) # noinspection PyProtectedMember return cls(handlers=handlers, **config.broker.queue._asdict(), **kwargs) @staticmethod - def _handlers_from_config(config: MinosConfig) -> dict[str, Callable[[HandlerRequest], Awaitable]]: - builder = EnrouteBuilder(config.commands.service, config.queries.service, config=config) - decorators = builder.get_broker_command_query() + def _handlers_from_config(config: MinosConfig, **kwargs) -> dict[str, Callable[[HandlerRequest], Awaitable]]: + builder = EnrouteBuilder(config.commands.service, config.queries.service) + decorators = builder.get_broker_command_query(config=config, **kwargs) handlers = {decorator.topic: fn for decorator, fn in decorators.items()} return handlers diff --git a/minos/networks/handlers/events/handlers.py b/minos/networks/handlers/events/handlers.py index a8002fb1..0720040d 100644 --- a/minos/networks/handlers/events/handlers.py +++ b/minos/networks/handlers/events/handlers.py @@ -54,14 +54,14 @@ class EventHandler(Handler): @classmethod def _from_config(cls, *args, config: MinosConfig, **kwargs) -> EventHandler: - handlers = cls._handlers_from_config(config) + handlers = cls._handlers_from_config(config, **kwargs) # noinspection PyProtectedMember return cls(handlers=handlers, **config.broker.queue._asdict(), **kwargs) @staticmethod - def _handlers_from_config(config: MinosConfig) -> dict[str, Callable[[HandlerRequest], Awaitable]]: - builder = EnrouteBuilder(config.commands.service, config.queries.service, config=config) - handlers = builder.get_broker_event() + def _handlers_from_config(config: MinosConfig, **kwargs) -> dict[str, Callable[[HandlerRequest], Awaitable]]: + builder = EnrouteBuilder(config.commands.service, config.queries.service) + handlers = builder.get_broker_event(config=config, **kwargs) handlers = {decorator.topic: fn for decorator, fn in handlers.items()} return handlers diff --git a/minos/networks/rest/handlers.py b/minos/networks/rest/handlers.py index d5f14e77..88d44a22 100644 --- a/minos/networks/rest/handlers.py +++ b/minos/networks/rest/handlers.py @@ -67,9 +67,9 @@ def _from_config(cls, *args, config: MinosConfig, **kwargs) -> RestHandler: return cls(host=host, port=port, endpoints=endpoints, **kwargs) @staticmethod - def _endpoints_from_config(config: MinosConfig) -> dict[(str, str), Callable]: - builder = EnrouteBuilder(config.commands.service, config.queries.service, config=config) - decorators = builder.get_rest_command_query() + def _endpoints_from_config(config: MinosConfig, **kwargs) -> dict[(str, str), Callable]: + builder = EnrouteBuilder(config.commands.service, config.queries.service) + decorators = builder.get_rest_command_query(config=config, **kwargs) endpoints = {(decorator.url, decorator.method): fn for decorator, fn in decorators.items()} return endpoints From 1b3fd84d7d2250dfb381ba042bd952282fbf3a6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Garc=C3=ADa=20Prado?= Date: Wed, 29 Sep 2021 12:53:36 +0200 Subject: [PATCH 07/21] ISSUE #398 * Resolve TODO. * Minor improvements. --- minos/networks/decorators/analyzers.py | 4 ++-- minos/networks/decorators/builders.py | 32 +++++++++++++++----------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/minos/networks/decorators/analyzers.py b/minos/networks/decorators/analyzers.py index 19255219..ba849616 100644 --- a/minos/networks/decorators/analyzers.py +++ b/minos/networks/decorators/analyzers.py @@ -74,9 +74,9 @@ def get_broker_event(self) -> dict[str, set[BrokerEnrouteDecorator]]: return self._get_items({BrokerEventEnrouteDecorator}) def get_periodic_event(self) -> dict[str, set[PeriodicEventEnrouteDecorator]]: - """TODO + """Returns periodic event values. - :return: TODO + :return: A mapping with functions as keys and a sets of decorators as values. """ # noinspection PyTypeChecker return self._get_items({PeriodicEventEnrouteDecorator}) diff --git a/minos/networks/decorators/builders.py b/minos/networks/decorators/builders.py index 6e79fea1..ddbde8a1 100644 --- a/minos/networks/decorators/builders.py +++ b/minos/networks/decorators/builders.py @@ -73,7 +73,7 @@ def get_broker_event(self, **kwargs) -> dict[BrokerEnrouteDecorator, Handler]: return self._build("get_broker_event", **kwargs) def get_periodic_event(self, **kwargs) -> dict[PeriodicEnrouteDecorator, Handler]: - """TODO + """Get the periodic handlers for events. :return: A dictionary with decorator classes as keys and callable handlers as values. """ @@ -81,37 +81,41 @@ def get_periodic_event(self, **kwargs) -> dict[PeriodicEnrouteDecorator, Handler return self._build("get_periodic_event", **kwargs) def _build(self, method_name: str, **kwargs) -> dict[EnrouteDecorator, Handler]: - - ans = defaultdict(set) - for klass in self.klasses: - self._build_klass(klass, method_name, ans, **kwargs) - - def _make_fn(d, fns: set[Handler]) -> Handler: + def _flatten(decorator: EnrouteDecorator, fns: set[Handler]) -> Handler: if len(fns) == 1: return next(iter(fns)) - if d.KIND != EnrouteDecoratorKind.Event: - raise MinosRedefinedEnrouteDecoratorException(f"{d!r} can be used only once.") + if decorator.KIND != EnrouteDecoratorKind.Event: + raise MinosRedefinedEnrouteDecoratorException(f"{decorator!r} can be used only once.") async def _fn(*ag, **kw): return await gather(*(fn(*ag, **kw) for fn in fns)) return _fn - ans = {decorator: _make_fn(decorator, fns) for decorator, fns in ans.items()} + return { + decorator: _flatten(decorator, fns) + for decorator, fns in self._build_all_classes(method_name, **kwargs).items() + } - return ans + def _build_all_classes(self, method_name: str, **kwargs) -> dict[EnrouteDecorator, set[Handler]]: + decomposed_handlers = defaultdict(set) + for klass in self.klasses: + self._build_one_class(klass, method_name, decomposed_handlers, **kwargs) + return decomposed_handlers - def _build_klass(self, klass: type, method_name: str, ans: dict[EnrouteDecorator, set[Handler]], **kwargs) -> None: + def _build_one_class( + self, klass: type, method_name: str, ans: dict[EnrouteDecorator, set[Handler]], **kwargs + ) -> None: analyzer = EnrouteAnalyzer(klass, **kwargs) mapping = getattr(analyzer, method_name)() for name, decorators in mapping.items(): for decorator in decorators: - ans[decorator].add(self._build_one(klass, name, decorator.pre_fn_name)) + ans[decorator].add(self._build_one_method(klass, name, decorator.pre_fn_name)) @staticmethod - def _build_one(klass: type, name: str, pref_fn_name: str, **kwargs) -> Handler: + def _build_one_method(klass: type, name: str, pref_fn_name: str, **kwargs) -> Handler: instance = klass(**kwargs) fn = getattr(instance, name) pre_fn = getattr(instance, pref_fn_name, None) From 1d70ccb2918e5091bc955c0d8e18b494385b98e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Garc=C3=ADa=20Prado?= Date: Wed, 29 Sep 2021 14:07:18 +0200 Subject: [PATCH 08/21] ISSUE #398 * Minor improvements. --- minos/networks/decorators/builders.py | 20 +++++++++---------- .../test_decorators/test_builders.py | 7 +++++-- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/minos/networks/decorators/builders.py b/minos/networks/decorators/builders.py index ddbde8a1..45e43f85 100644 --- a/minos/networks/decorators/builders.py +++ b/minos/networks/decorators/builders.py @@ -43,10 +43,10 @@ class EnrouteBuilder: """Enroute builder class.""" - def __init__(self, *klasses: Union[str, Type]): - klasses = tuple((klass if not isinstance(klass, str) else import_module(klass)) for klass in klasses) + def __init__(self, *classes: Union[str, Type]): + classes = tuple((class_ if not isinstance(class_, str) else import_module(class_)) for class_ in classes) - self.klasses = klasses + self.classes = classes def get_rest_command_query(self, **kwargs) -> dict[RestEnrouteDecorator, Handler]: """Get the rest handlers for commands and queries. @@ -100,23 +100,23 @@ async def _fn(*ag, **kw): def _build_all_classes(self, method_name: str, **kwargs) -> dict[EnrouteDecorator, set[Handler]]: decomposed_handlers = defaultdict(set) - for klass in self.klasses: - self._build_one_class(klass, method_name, decomposed_handlers, **kwargs) + for class_ in self.classes: + self._build_one_class(class_, method_name, decomposed_handlers, **kwargs) return decomposed_handlers def _build_one_class( - self, klass: type, method_name: str, ans: dict[EnrouteDecorator, set[Handler]], **kwargs + self, class_: type, method_name: str, ans: dict[EnrouteDecorator, set[Handler]], **kwargs ) -> None: - analyzer = EnrouteAnalyzer(klass, **kwargs) + analyzer = EnrouteAnalyzer(class_, **kwargs) mapping = getattr(analyzer, method_name)() for name, decorators in mapping.items(): for decorator in decorators: - ans[decorator].add(self._build_one_method(klass, name, decorator.pre_fn_name)) + ans[decorator].add(self._build_one_method(class_, name, decorator.pre_fn_name)) @staticmethod - def _build_one_method(klass: type, name: str, pref_fn_name: str, **kwargs) -> Handler: - instance = klass(**kwargs) + def _build_one_method(class_: type, name: str, pref_fn_name: str, **kwargs) -> Handler: + instance = class_(**kwargs) fn = getattr(instance, name) pre_fn = getattr(instance, pref_fn_name, None) diff --git a/tests/test_networks/test_decorators/test_builders.py b/tests/test_networks/test_decorators/test_builders.py index d82a9315..a36dbbbd 100644 --- a/tests/test_networks/test_decorators/test_builders.py +++ b/tests/test_networks/test_decorators/test_builders.py @@ -26,9 +26,12 @@ def setUp(self) -> None: self.request = FakeRequest("test") self.builder = EnrouteBuilder(FakeService) - def test_instance_str(self): + def test_classes(self): + self.assertEqual((FakeService,), self.builder.classes) + + def test_classes_str(self): builder = EnrouteBuilder(classname(FakeService)) - self.assertEqual((FakeService,), builder.klasses) + self.assertEqual((FakeService,), builder.classes) async def test_get_rest_command_query(self): handlers = self.builder.get_rest_command_query() From 32819ca23245b60410d2861d09010be99734d8bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Garc=C3=ADa=20Prado?= Date: Wed, 29 Sep 2021 14:29:26 +0200 Subject: [PATCH 09/21] ISSUE #399 * Create `PeriodicTask` class. --- minos/networks/__init__.py | 1 + minos/networks/scheduling/__init__.py | 1 + minos/networks/scheduling/schedulers.py | 89 ++++++++++++++++--------- 3 files changed, 59 insertions(+), 32 deletions(-) diff --git a/minos/networks/__init__.py b/minos/networks/__init__.py index 578f0342..d3305a27 100644 --- a/minos/networks/__init__.py +++ b/minos/networks/__init__.py @@ -73,6 +73,7 @@ RestService, ) from .scheduling import ( + PeriodicTask, SchedulingRequest, TaskScheduler, TaskSchedulerService, diff --git a/minos/networks/scheduling/__init__.py b/minos/networks/scheduling/__init__.py index f125f5c9..21c06515 100644 --- a/minos/networks/scheduling/__init__.py +++ b/minos/networks/scheduling/__init__.py @@ -2,6 +2,7 @@ SchedulingRequest, ) from .schedulers import ( + PeriodicTask, TaskScheduler, ) from .services import ( diff --git a/minos/networks/scheduling/schedulers.py b/minos/networks/scheduling/schedulers.py index ed7f0c6c..7cdb60fc 100644 --- a/minos/networks/scheduling/schedulers.py +++ b/minos/networks/scheduling/schedulers.py @@ -4,17 +4,15 @@ import logging from asyncio import ( - AbstractEventLoop, + Task, + create_task, gather, - get_event_loop, sleep, + wait_for, ) from datetime import ( timedelta, ) -from itertools import ( - chain, -) from typing import ( Awaitable, Callable, @@ -42,43 +40,46 @@ logger = logging.getLogger(__name__) -class TaskScheduler(MinosSetup): +class PeriodicTask: """TODO""" - def __init__( - self, tasks: list[tuple[CronTab, Callable]], loop: Optional[AbstractEventLoop] = None, *args, **kwargs - ): - super().__init__(*args, **kwargs) - if loop is None: - loop = get_event_loop() + _task: Optional[Task] - self._tasks = tasks - self._loop = loop + def __init__(self, crontab: CronTab, fn: Callable[[SchedulingRequest], Awaitable[None]]): + self.crontab = crontab + self.fn = fn + self._task = None - @classmethod - def _from_config(cls, config: MinosConfig, **kwargs) -> TaskScheduler: - command_decorators = EnrouteBuilder(config.commands.service, config).get_periodic_event() - service_decorators = EnrouteBuilder(config.queries.service, config).get_periodic_event() + async def start(self) -> None: + """TODO - tasks = [ - (decorator.crontab, fn) for decorator, fn in chain(command_decorators.items(), service_decorators.items()) - ] - return cls(tasks) + :return: TODO + """ + self._task = create_task(self.callback()) - async def start(self) -> None: + @property + def started(self) -> bool: """TODO :return: TODO """ - await gather(*(self.callback(*task) for task in self._tasks)) + return self._task is not None + + async def stop(self, timeout: Optional[float] = None) -> None: + """TODO + + :param timeout: TODO + :return: TODO + """ + if self._task is not None: + self._task.cancel() + await wait_for(self._task, timeout) + self._task = None - @staticmethod - async def callback(crontab: CronTab, fn: Callable[[SchedulingRequest], Awaitable]) -> NoReturn: + async def callback(self) -> NoReturn: """TODO - :param crontab: TODO - :param fn: TODO :return: TODO """ @@ -86,17 +87,41 @@ async def callback(crontab: CronTab, fn: Callable[[SchedulingRequest], Awaitable while True: request = SchedulingRequest(now) try: - await fn(request) + await self.fn(request) except Exception as exc: # TODO logger.warning(f"Raised exception while executing task: {exc}") now = current_datetime() - delay = crontab.next(now) + delay = self.crontab.next(now) now += timedelta(seconds=delay) await sleep(delay) - async def stop(self) -> None: + +class TaskScheduler(MinosSetup): + """TODO""" + + def __init__(self, tasks: list[PeriodicTask], *args, **kwargs): + super().__init__(*args, **kwargs) + self._tasks = tasks + + @classmethod + def _from_config(cls, config: MinosConfig, **kwargs) -> TaskScheduler: + builder = EnrouteBuilder(config.commands.service, config.queries.service) + decorators = builder.get_periodic_event(config=config, **kwargs) + tasks = [PeriodicTask(decorator.crontab, fn) for decorator, fn in decorators.items()] + return cls(tasks, **kwargs) + + async def start(self) -> None: + """TODO + + :return: TODO + """ + + await gather(*(task.start() for task in self._tasks)) + + async def stop(self, timeout: Optional[float] = None) -> None: """TODO + :param timeout: TODO :return: TODO """ - pass + await gather(*(task.stop(timeout) for task in self._tasks)) From 1c32ec051cdbd598e9211cdbfe9347a150cbec38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Garc=C3=ADa=20Prado?= Date: Wed, 29 Sep 2021 14:29:49 +0200 Subject: [PATCH 10/21] ISSUE #398 * Minor type hint change. --- minos/networks/decorators/builders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minos/networks/decorators/builders.py b/minos/networks/decorators/builders.py index 45e43f85..1e0e5379 100644 --- a/minos/networks/decorators/builders.py +++ b/minos/networks/decorators/builders.py @@ -37,7 +37,7 @@ RestEnrouteDecorator, ) -Handler = Callable[[Request], Awaitable[Response]] +Handler = Callable[[Request], Awaitable[Optional[Response]]] class EnrouteBuilder: From 179a99d1cc18ee3bb10c7aa9a13852582d7a138a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Garc=C3=ADa=20Prado?= Date: Wed, 29 Sep 2021 15:07:15 +0200 Subject: [PATCH 11/21] ISSUE #399 * Improve `PeriodicTask` implementation. --- minos/networks/scheduling/schedulers.py | 71 +++++++++++++++++++------ 1 file changed, 55 insertions(+), 16 deletions(-) diff --git a/minos/networks/scheduling/schedulers.py b/minos/networks/scheduling/schedulers.py index 7cdb60fc..5b86605f 100644 --- a/minos/networks/scheduling/schedulers.py +++ b/minos/networks/scheduling/schedulers.py @@ -4,14 +4,19 @@ import logging from asyncio import ( + CancelledError, Task, + TimeoutError, create_task, gather, sleep, wait_for, ) +from contextlib import ( + suppress, +) from datetime import ( - timedelta, + datetime, ) from typing import ( Awaitable, @@ -46,16 +51,33 @@ class PeriodicTask: _task: Optional[Task] def __init__(self, crontab: CronTab, fn: Callable[[SchedulingRequest], Awaitable[None]]): - self.crontab = crontab - self.fn = fn + self._crontab = crontab + self._fn = fn self._task = None + self._running = False + + @property + def crontab(self) -> CronTab: + """TODO + + :return: TODO + """ + return self._crontab + + @property + def fn(self) -> Callable[[SchedulingRequest], Awaitable[None]]: + """TODO + + :return: TODO + """ + return self._fn async def start(self) -> None: """TODO :return: TODO """ - self._task = create_task(self.callback()) + self._task = create_task(self.run_forever()) @property def started(self) -> bool: @@ -66,6 +88,14 @@ def started(self) -> bool: return self._task is not None + @property + def running(self) -> bool: + """TODO + + :return: TODO + """ + return self._running + async def stop(self, timeout: Optional[float] = None) -> None: """TODO @@ -74,26 +104,35 @@ async def stop(self, timeout: Optional[float] = None) -> None: """ if self._task is not None: self._task.cancel() - await wait_for(self._task, timeout) + with suppress(TimeoutError, CancelledError): + await wait_for(self._task, timeout) self._task = None - async def callback(self) -> NoReturn: + async def run_forever(self) -> NoReturn: """TODO :return: TODO """ - - now = current_datetime() while True: - request = SchedulingRequest(now) - try: - await self.fn(request) - except Exception as exc: # TODO - logger.warning(f"Raised exception while executing task: {exc}") now = current_datetime() - delay = self.crontab.next(now) - now += timedelta(seconds=delay) - await sleep(delay) + await gather(self.run_one(now), sleep(self._crontab.next(now))) + + async def run_one(self, now: datetime) -> None: + """TODO + + :param now: TODO + :return: TODO + """ + + request = SchedulingRequest(now) + try: + self._running = True + with suppress(CancelledError): + await self._fn(request) + except Exception as exc: + logger.warning(f"Raised exception while executing periodic task: {exc}") + finally: + self._running = False class TaskScheduler(MinosSetup): From 04b135e5bae78ce9b29ed97f20a8d9928a70154d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Garc=C3=ADa=20Prado?= Date: Wed, 29 Sep 2021 15:27:17 +0200 Subject: [PATCH 12/21] ISSUE #399 * Improve `SchedulingRequest`. * Minor improvements. --- minos/networks/scheduling/messages.py | 27 ++++++++-- minos/networks/scheduling/schedulers.py | 67 +++++++++++++------------ 2 files changed, 58 insertions(+), 36 deletions(-) diff --git a/minos/networks/scheduling/messages.py b/minos/networks/scheduling/messages.py index 077e1733..bcec17e5 100644 --- a/minos/networks/scheduling/messages.py +++ b/minos/networks/scheduling/messages.py @@ -1,14 +1,21 @@ +from __future__ import ( + annotations, +) + from datetime import ( datetime, ) from typing import ( - Any, Optional, ) from uuid import ( UUID, ) +from minos.common import ( + DeclarativeModel, +) + from ..messages import ( Request, ) @@ -29,16 +36,26 @@ def user(self) -> Optional[UUID]: """ return None - async def content(self, **kwargs) -> Any: + async def content(self, **kwargs) -> SchedulingRequestContent: """TODO :param kwargs: TODO :return: TODO """ - return {"scheduled_at": self._scheduled_at} + return self._content def __eq__(self, other: Request) -> bool: - return False + return isinstance(other, type(self)) and self._content == other._content def __repr__(self) -> str: - return "" + return f"{type(self).__name__}({self._content!r})" + + @property + def _content(self) -> SchedulingRequestContent: + return SchedulingRequestContent(self._scheduled_at) + + +class SchedulingRequestContent(DeclarativeModel): + """TODO""" + + scheduled_at: datetime diff --git a/minos/networks/scheduling/schedulers.py b/minos/networks/scheduling/schedulers.py index 5b86605f..8fc90250 100644 --- a/minos/networks/scheduling/schedulers.py +++ b/minos/networks/scheduling/schedulers.py @@ -45,6 +45,42 @@ logger = logging.getLogger(__name__) +class TaskScheduler(MinosSetup): + """TODO""" + + def __init__(self, tasks: set[PeriodicTask], *args, **kwargs): + super().__init__(*args, **kwargs) + self._tasks = tasks + + @classmethod + def _from_config(cls, config: MinosConfig, **kwargs) -> TaskScheduler: + tasks = cls._tasks_from_config(config, **kwargs) + return cls(tasks, **kwargs) + + @staticmethod + def _tasks_from_config(config: MinosConfig, **kwargs) -> set[PeriodicTask]: + builder = EnrouteBuilder(config.commands.service, config.queries.service) + decorators = builder.get_periodic_event(config=config, **kwargs) + tasks = {PeriodicTask(decorator.crontab, fn) for decorator, fn in decorators.items()} + return tasks + + async def start(self) -> None: + """TODO + + :return: TODO + """ + + await gather(*(task.start() for task in self._tasks)) + + async def stop(self, timeout: Optional[float] = None) -> None: + """TODO + + :param timeout: TODO + :return: TODO + """ + await gather(*(task.stop(timeout) for task in self._tasks)) + + class PeriodicTask: """TODO""" @@ -133,34 +169,3 @@ async def run_one(self, now: datetime) -> None: logger.warning(f"Raised exception while executing periodic task: {exc}") finally: self._running = False - - -class TaskScheduler(MinosSetup): - """TODO""" - - def __init__(self, tasks: list[PeriodicTask], *args, **kwargs): - super().__init__(*args, **kwargs) - self._tasks = tasks - - @classmethod - def _from_config(cls, config: MinosConfig, **kwargs) -> TaskScheduler: - builder = EnrouteBuilder(config.commands.service, config.queries.service) - decorators = builder.get_periodic_event(config=config, **kwargs) - tasks = [PeriodicTask(decorator.crontab, fn) for decorator, fn in decorators.items()] - return cls(tasks, **kwargs) - - async def start(self) -> None: - """TODO - - :return: TODO - """ - - await gather(*(task.start() for task in self._tasks)) - - async def stop(self, timeout: Optional[float] = None) -> None: - """TODO - - :param timeout: TODO - :return: TODO - """ - await gather(*(task.stop(timeout) for task in self._tasks)) From d8fc6f12084e8838f4d674f69f95203a786e92f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Garc=C3=ADa=20Prado?= Date: Wed, 29 Sep 2021 15:38:16 +0200 Subject: [PATCH 13/21] ISSUE #399 * Add tests for `TaskSchedulerService`. --- .../test_networks/test_scheduling/__init__.py | 0 .../test_scheduling/test_services.py | 62 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 tests/test_networks/test_scheduling/__init__.py create mode 100644 tests/test_networks/test_scheduling/test_services.py diff --git a/tests/test_networks/test_scheduling/__init__.py b/tests/test_networks/test_scheduling/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_networks/test_scheduling/test_services.py b/tests/test_networks/test_scheduling/test_services.py new file mode 100644 index 00000000..8df66c13 --- /dev/null +++ b/tests/test_networks/test_scheduling/test_services.py @@ -0,0 +1,62 @@ +import unittest +from unittest.mock import ( + AsyncMock, +) + +from aiomisc import ( + Service, +) + +from minos.common.testing import ( + PostgresAsyncTestCase, +) +from minos.networks import ( + TaskScheduler, + TaskSchedulerService, +) +from tests.utils import ( + BASE_PATH, +) + + +class TestTaskSchedulerService(PostgresAsyncTestCase): + CONFIG_FILE_PATH = BASE_PATH / "test_config.yml" + + def test_is_instance(self): + service = TaskSchedulerService(config=self.config) + self.assertIsInstance(service, Service) + + def test_dispatcher(self): + service = TaskSchedulerService(config=self.config) + self.assertIsInstance(service.scheduler, TaskScheduler) + + async def test_start_stop(self): + service = TaskSchedulerService(config=self.config) + + setup_mock = AsyncMock() + destroy_mock = AsyncMock() + start = AsyncMock() + + service.scheduler.setup = setup_mock + service.scheduler.destroy = destroy_mock + service.scheduler.start = start + + await service.start() + + self.assertEqual(1, setup_mock.call_count) + self.assertEqual(1, start.call_count) + self.assertEqual(0, destroy_mock.call_count) + + setup_mock.reset_mock() + destroy_mock.reset_mock() + start.reset_mock() + + await service.stop() + + self.assertEqual(0, setup_mock.call_count) + self.assertEqual(0, start.call_count) + self.assertEqual(1, destroy_mock.call_count) + + +if __name__ == "__main__": + unittest.main() From 25dcf37cbe9b82fb345293b3a904e25a9808c2c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Garc=C3=ADa=20Prado?= Date: Wed, 29 Sep 2021 15:57:36 +0200 Subject: [PATCH 14/21] ISSUE #399 * Add tests for `minos.networks.scheduling.messages` module. --- minos/networks/__init__.py | 1 + minos/networks/scheduling/__init__.py | 1 + .../test_scheduling/test_messages.py | 43 +++++++++++++++++++ .../test_scheduling/test_schedulers.py | 0 4 files changed, 45 insertions(+) create mode 100644 tests/test_networks/test_scheduling/test_messages.py create mode 100644 tests/test_networks/test_scheduling/test_schedulers.py diff --git a/minos/networks/__init__.py b/minos/networks/__init__.py index d3305a27..4520befc 100644 --- a/minos/networks/__init__.py +++ b/minos/networks/__init__.py @@ -75,6 +75,7 @@ from .scheduling import ( PeriodicTask, SchedulingRequest, + SchedulingRequestContent, TaskScheduler, TaskSchedulerService, ) diff --git a/minos/networks/scheduling/__init__.py b/minos/networks/scheduling/__init__.py index 21c06515..ea106405 100644 --- a/minos/networks/scheduling/__init__.py +++ b/minos/networks/scheduling/__init__.py @@ -1,5 +1,6 @@ from .messages import ( SchedulingRequest, + SchedulingRequestContent, ) from .schedulers import ( PeriodicTask, diff --git a/tests/test_networks/test_scheduling/test_messages.py b/tests/test_networks/test_scheduling/test_messages.py new file mode 100644 index 00000000..62445f8e --- /dev/null +++ b/tests/test_networks/test_scheduling/test_messages.py @@ -0,0 +1,43 @@ +import unittest + +from minos.common import ( + DeclarativeModel, + current_datetime, +) +from minos.networks import ( + SchedulingRequest, + SchedulingRequestContent, +) + + +class TestSchedulingRequest(unittest.IsolatedAsyncioTestCase): + def setUp(self) -> None: + self.now = current_datetime() + self.request = SchedulingRequest(self.now) + + async def test_content(self): + self.assertEqual(SchedulingRequestContent(self.now), await self.request.content()) + + def test_user(self): + self.assertIsNone(self.request.user) + + def test_eq(self): + self.assertEqual(self.request, SchedulingRequest(self.now)) + self.assertNotEqual(self.request, SchedulingRequest(current_datetime())) + + def test_repr(self): + self.assertEqual(f"SchedulingRequest(SchedulingRequestContent(scheduled_at={self.now!s}))", repr(self.request)) + + +class TestSchedulingRequestContent(unittest.IsolatedAsyncioTestCase): + def test_subclass(self): + self.assertTrue(issubclass(SchedulingRequestContent, DeclarativeModel)) + + def test_scheduled_at(self): + now = current_datetime() + content = SchedulingRequestContent(now) + self.assertEqual(now, content.scheduled_at) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_networks/test_scheduling/test_schedulers.py b/tests/test_networks/test_scheduling/test_schedulers.py new file mode 100644 index 00000000..e69de29b From 298f95638027b97842e2aaee5790b008721890e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Garc=C3=ADa=20Prado?= Date: Wed, 29 Sep 2021 16:18:41 +0200 Subject: [PATCH 15/21] ISSUE #399 * Add tests for `TaskScheduler`. --- minos/networks/scheduling/schedulers.py | 16 ++++- tests/services/commands.py | 6 +- .../test_scheduling/test_schedulers.py | 71 +++++++++++++++++++ 3 files changed, 90 insertions(+), 3 deletions(-) diff --git a/minos/networks/scheduling/schedulers.py b/minos/networks/scheduling/schedulers.py index 8fc90250..d7c87d7b 100644 --- a/minos/networks/scheduling/schedulers.py +++ b/minos/networks/scheduling/schedulers.py @@ -23,6 +23,7 @@ Callable, NoReturn, Optional, + Union, ) from crontab import ( @@ -64,6 +65,14 @@ def _tasks_from_config(config: MinosConfig, **kwargs) -> set[PeriodicTask]: tasks = {PeriodicTask(decorator.crontab, fn) for decorator, fn in decorators.items()} return tasks + @property + def tasks(self) -> set[PeriodicTask]: + """TODO + + :return: TODO + """ + return self._tasks + async def start(self) -> None: """TODO @@ -78,7 +87,7 @@ async def stop(self, timeout: Optional[float] = None) -> None: :param timeout: TODO :return: TODO """ - await gather(*(task.stop(timeout) for task in self._tasks)) + await gather(*(task.stop(timeout=timeout) for task in self._tasks)) class PeriodicTask: @@ -86,7 +95,10 @@ class PeriodicTask: _task: Optional[Task] - def __init__(self, crontab: CronTab, fn: Callable[[SchedulingRequest], Awaitable[None]]): + def __init__(self, crontab: Union[str, CronTab], fn: Callable[[SchedulingRequest], Awaitable[None]]): + if isinstance(crontab, str): + crontab = CronTab(crontab) + self._crontab = crontab self._fn = fn self._task = None diff --git a/tests/services/commands.py b/tests/services/commands.py index f72a2e3b..365fde58 100644 --- a/tests/services/commands.py +++ b/tests/services/commands.py @@ -28,5 +28,9 @@ def update_order(self, request: Request) -> Response: return HandlerResponse("update_order") @enroute.broker.event("TicketAdded") - def ticket_added(self, request: Request): + def ticket_added(self, request: Request) -> None: return "command_service_ticket_added" + + @enroute.periodic.event("@daily") + def recompute_something(self, request: Request) -> None: + """For testing purposes.""" diff --git a/tests/test_networks/test_scheduling/test_schedulers.py b/tests/test_networks/test_scheduling/test_schedulers.py index e69de29b..e09858c6 100644 --- a/tests/test_networks/test_scheduling/test_schedulers.py +++ b/tests/test_networks/test_scheduling/test_schedulers.py @@ -0,0 +1,71 @@ +import unittest +from unittest.mock import ( + AsyncMock, + call, +) + +from minos.common import ( + MinosConfig, +) +from minos.networks import ( + PeriodicTask, + TaskScheduler, +) +from tests.utils import ( + BASE_PATH, +) + + +class TestTaskScheduler(unittest.IsolatedAsyncioTestCase): + def test_from_config(self): + config = MinosConfig(BASE_PATH / "test_config.yml") + scheduler = TaskScheduler.from_config(config) + self.assertEqual(1, len(scheduler.tasks)) + self.assertTrue(all(map(lambda t: isinstance(t, PeriodicTask), scheduler.tasks))) + + def test_tasks(self): + tasks = {PeriodicTask("@daily", None), PeriodicTask("@hourly", None)} + + scheduler = TaskScheduler(tasks) + + self.assertEqual(tasks, scheduler.tasks) + + async def test_start(self): + tasks_1_mock = AsyncMock() + tasks_2_mock = AsyncMock() + + tasks = {tasks_1_mock, tasks_2_mock} + + scheduler = TaskScheduler(tasks) + + await scheduler.start() + + self.assertEqual(1, tasks_1_mock.start.call_count) + self.assertEqual(call(), tasks_1_mock.start.call_args) + + self.assertEqual(1, tasks_2_mock.start.call_count) + self.assertEqual(call(), tasks_2_mock.start.call_args) + + async def test_stop(self): + tasks_1_mock = AsyncMock() + tasks_2_mock = AsyncMock() + + tasks = {tasks_1_mock, tasks_2_mock} + + scheduler = TaskScheduler(tasks) + + await scheduler.stop(timeout=30) + + self.assertEqual(1, tasks_1_mock.stop.call_count) + self.assertEqual(call(timeout=30), tasks_1_mock.stop.call_args) + + self.assertEqual(1, tasks_2_mock.stop.call_count) + self.assertEqual(call(timeout=30), tasks_2_mock.stop.call_args) + + +class TestPeriodicTask(unittest.IsolatedAsyncioTestCase): + pass + + +if __name__ == "__main__": + unittest.main() From 3435e7ebd5557cb210ce5cfa490744a58f0ac4e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Garc=C3=ADa=20Prado?= Date: Wed, 29 Sep 2021 17:16:42 +0200 Subject: [PATCH 16/21] ISSUE #399 * Add tests for `PeriodicTask`. --- minos/networks/scheduling/schedulers.py | 55 +++++++------ .../test_scheduling/test_schedulers.py | 78 ++++++++++++++++++- 2 files changed, 107 insertions(+), 26 deletions(-) diff --git a/minos/networks/scheduling/schedulers.py b/minos/networks/scheduling/schedulers.py index d7c87d7b..b394b4d8 100644 --- a/minos/networks/scheduling/schedulers.py +++ b/minos/networks/scheduling/schedulers.py @@ -2,16 +2,8 @@ annotations, ) +import asyncio import logging -from asyncio import ( - CancelledError, - Task, - TimeoutError, - create_task, - gather, - sleep, - wait_for, -) from contextlib import ( suppress, ) @@ -79,7 +71,7 @@ async def start(self) -> None: :return: TODO """ - await gather(*(task.start() for task in self._tasks)) + await asyncio.gather(*(task.start() for task in self._tasks)) async def stop(self, timeout: Optional[float] = None) -> None: """TODO @@ -87,13 +79,13 @@ async def stop(self, timeout: Optional[float] = None) -> None: :param timeout: TODO :return: TODO """ - await gather(*(task.stop(timeout=timeout) for task in self._tasks)) + await asyncio.gather(*(task.stop(timeout=timeout) for task in self._tasks)) class PeriodicTask: """TODO""" - _task: Optional[Task] + _task: Optional[asyncio.Task] def __init__(self, crontab: Union[str, CronTab], fn: Callable[[SchedulingRequest], Awaitable[None]]): if isinstance(crontab, str): @@ -120,29 +112,29 @@ def fn(self) -> Callable[[SchedulingRequest], Awaitable[None]]: """ return self._fn - async def start(self) -> None: + @property + def started(self) -> bool: """TODO :return: TODO """ - self._task = create_task(self.run_forever()) + + return self._task is not None @property - def started(self) -> bool: + def task(self) -> asyncio.Task: """TODO :return: TODO """ + return self._task - return self._task is not None - - @property - def running(self) -> bool: + async def start(self) -> None: """TODO :return: TODO """ - return self._running + self._task = asyncio.create_task(self.run_forever()) async def stop(self, timeout: Optional[float] = None) -> None: """TODO @@ -152,8 +144,8 @@ async def stop(self, timeout: Optional[float] = None) -> None: """ if self._task is not None: self._task.cancel() - with suppress(TimeoutError, CancelledError): - await wait_for(self._task, timeout) + with suppress(asyncio.TimeoutError, asyncio.CancelledError): + await asyncio.wait_for(self._task, timeout) self._task = None async def run_forever(self) -> NoReturn: @@ -161,21 +153,34 @@ async def run_forever(self) -> NoReturn: :return: TODO """ + now = current_datetime() + await asyncio.sleep(self._crontab.next(now)) + while True: now = current_datetime() - await gather(self.run_one(now), sleep(self._crontab.next(now))) + await asyncio.gather(asyncio.sleep(self._crontab.next(now)), self.run_once(now)) - async def run_one(self, now: datetime) -> None: + @property + def running(self) -> bool: + """TODO + + :return: TODO + """ + return self._running + + async def run_once(self, now: Optional[datetime] = None) -> None: """TODO :param now: TODO :return: TODO """ + if now is None: + now = current_datetime() request = SchedulingRequest(now) try: self._running = True - with suppress(CancelledError): + with suppress(asyncio.CancelledError): await self._fn(request) except Exception as exc: logger.warning(f"Raised exception while executing periodic task: {exc}") diff --git a/tests/test_networks/test_scheduling/test_schedulers.py b/tests/test_networks/test_scheduling/test_schedulers.py index e09858c6..1879dbb6 100644 --- a/tests/test_networks/test_scheduling/test_schedulers.py +++ b/tests/test_networks/test_scheduling/test_schedulers.py @@ -1,14 +1,22 @@ +import asyncio import unittest from unittest.mock import ( AsyncMock, call, + patch, +) + +from crontab import ( + CronTab, ) from minos.common import ( MinosConfig, + current_datetime, ) from minos.networks import ( PeriodicTask, + SchedulingRequest, TaskScheduler, ) from tests.utils import ( @@ -24,6 +32,7 @@ def test_from_config(self): self.assertTrue(all(map(lambda t: isinstance(t, PeriodicTask), scheduler.tasks))) def test_tasks(self): + # noinspection PyTypeChecker tasks = {PeriodicTask("@daily", None), PeriodicTask("@hourly", None)} scheduler = TaskScheduler(tasks) @@ -64,7 +73,74 @@ async def test_stop(self): class TestPeriodicTask(unittest.IsolatedAsyncioTestCase): - pass + def setUp(self) -> None: + self.fn_mock = AsyncMock() + self.periodic = PeriodicTask("@daily", self.fn_mock) + + def test_crontab(self) -> None: + self.assertEqual(CronTab("@daily").matchers, self.periodic.crontab.matchers) + + def test_fn(self) -> None: + self.assertEqual(self.fn_mock, self.periodic.fn) + + async def test_start(self) -> None: + self.assertFalse(self.periodic.started) + + with patch("asyncio.create_task", return_value="test") as mock_create: + await self.periodic.start() + self.assertEqual(1, mock_create.call_count) + self.assertEqual("run_forever", mock_create.call_args.args[0].__name__) + self.assertTrue(self.periodic.started) + self.assertEqual("test", self.periodic.task) + + async def test_stop(self) -> None: + mock = AsyncMock() + self.periodic._task = mock + with patch("asyncio.wait_for") as mock_wait: + await self.periodic.stop() + self.assertEqual(1, mock_wait.call_count) + self.assertEqual(call(mock, None), mock_wait.call_args) + + async def test_run_forever(self) -> None: + with patch("asyncio.sleep") as mock_sleep: + run_once_mock = AsyncMock(side_effect=ValueError) + self.periodic.run_once = run_once_mock + + with self.assertRaises(ValueError): + await self.periodic.run_forever() + + self.assertEqual(2, mock_sleep.call_count) + self.assertEqual(1, run_once_mock.call_count) + + async def test_run_once(self) -> None: + now = current_datetime() + await self.periodic.run_once(now) + self.assertEqual(1, self.fn_mock.call_count) + + observed = self.fn_mock.call_args.args[0] + self.assertIsInstance(observed, SchedulingRequest) + self.assertEqual(now, (await observed.content()).scheduled_at) + + async def test_run_once_handle_exceptions(self) -> None: + self.fn_mock.side_effect = asyncio.CancelledError + await self.periodic.run_once() + + self.fn_mock.side_effect = Exception + await self.periodic.run_once() + + self.assertTrue(True) + + async def test_run_once_running(self) -> None: + def _fn(*args, **kwargs): + self.assertTrue(self.periodic.running) + + self.fn_mock.side_effect = _fn + + self.assertFalse(self.periodic.running) + await self.periodic.run_once() + self.assertFalse(self.periodic.running) + + self.assertEqual(1, self.fn_mock.call_count) if __name__ == "__main__": From 3ac587d817b70a8b2567a89b42ae4e6866b281fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Garc=C3=ADa=20Prado?= Date: Wed, 29 Sep 2021 17:35:12 +0200 Subject: [PATCH 17/21] ISSUE #399 * Add docstring. --- minos/networks/scheduling/messages.py | 14 +++--- minos/networks/scheduling/schedulers.py | 58 ++++++++++++------------- minos/networks/scheduling/services.py | 2 +- 3 files changed, 37 insertions(+), 37 deletions(-) diff --git a/minos/networks/scheduling/messages.py b/minos/networks/scheduling/messages.py index bcec17e5..c30b1509 100644 --- a/minos/networks/scheduling/messages.py +++ b/minos/networks/scheduling/messages.py @@ -22,7 +22,7 @@ class SchedulingRequest(Request): - """TODO""" + """Scheduling Request class.""" def __init__(self, scheduled_at: datetime): super().__init__() @@ -30,17 +30,17 @@ def __init__(self, scheduled_at: datetime): @property def user(self) -> Optional[UUID]: - """TODO + """The user of the request. - :return: TODO + :return: Always return ``None`` as scheduled request are launched by the system. """ return None async def content(self, **kwargs) -> SchedulingRequestContent: - """TODO + """Get the request content. - :param kwargs: TODO - :return: TODO + :param kwargs: Additional named arguments. + :return: A ``SchedulingRequestContent` intance.`. """ return self._content @@ -56,6 +56,6 @@ def _content(self) -> SchedulingRequestContent: class SchedulingRequestContent(DeclarativeModel): - """TODO""" + """Scheduling Request Content class.""" scheduled_at: datetime diff --git a/minos/networks/scheduling/schedulers.py b/minos/networks/scheduling/schedulers.py index b394b4d8..6732a008 100644 --- a/minos/networks/scheduling/schedulers.py +++ b/minos/networks/scheduling/schedulers.py @@ -39,7 +39,7 @@ class TaskScheduler(MinosSetup): - """TODO""" + """Task Scheduler class.""" def __init__(self, tasks: set[PeriodicTask], *args, **kwargs): super().__init__(*args, **kwargs) @@ -59,31 +59,31 @@ def _tasks_from_config(config: MinosConfig, **kwargs) -> set[PeriodicTask]: @property def tasks(self) -> set[PeriodicTask]: - """TODO + """Get the set of periodic tasks. - :return: TODO + :return: A ``set`` of ``PeriodicTask`` instances. """ return self._tasks async def start(self) -> None: - """TODO + """Start the execution of periodic tasks. - :return: TODO + :return: This method does not return anything. """ await asyncio.gather(*(task.start() for task in self._tasks)) async def stop(self, timeout: Optional[float] = None) -> None: - """TODO + """Stop the execution of periodic tasks. - :param timeout: TODO - :return: TODO + :param timeout: An optional timeout expressed in seconds. + :return: This method does not return anything. """ await asyncio.gather(*(task.stop(timeout=timeout) for task in self._tasks)) class PeriodicTask: - """TODO""" + """Periodic Task class.""" _task: Optional[asyncio.Task] @@ -98,49 +98,49 @@ def __init__(self, crontab: Union[str, CronTab], fn: Callable[[SchedulingRequest @property def crontab(self) -> CronTab: - """TODO + """Get the crontab of the periodic task. - :return: TODO + :return: A ``CronTab`` instance. """ return self._crontab @property def fn(self) -> Callable[[SchedulingRequest], Awaitable[None]]: - """TODO + """Get the function to be called periodically. - :return: TODO + :return: A function returning an awaitable. """ return self._fn @property def started(self) -> bool: - """TODO + """Check if the periodic task has been started. - :return: TODO + :return: ``True`` if started or ``False`` otherwise. """ return self._task is not None @property def task(self) -> asyncio.Task: - """TODO + """Get the asyncio task. - :return: TODO + :return: An ``asyncio.Task`` instance. """ return self._task async def start(self) -> None: - """TODO + """Start the periodic task. - :return: TODO + :return: This method does not return anything. """ self._task = asyncio.create_task(self.run_forever()) async def stop(self, timeout: Optional[float] = None) -> None: - """TODO + """Stop the periodic task. - :param timeout: TODO - :return: TODO + :param timeout: An optional timeout expressed in seconds. + :return: This method does not return anything. """ if self._task is not None: self._task.cancel() @@ -149,9 +149,9 @@ async def stop(self, timeout: Optional[float] = None) -> None: self._task = None async def run_forever(self) -> NoReturn: - """TODO + """Run the periodic function forever. This method is equivalent to start, but it keeps waiting until infinite. - :return: TODO + :return: This method never returns. """ now = current_datetime() await asyncio.sleep(self._crontab.next(now)) @@ -162,17 +162,17 @@ async def run_forever(self) -> NoReturn: @property def running(self) -> bool: - """TODO + """Check if the periodic function is running. - :return: TODO + :return: ``True`` if it's running or ``False`` otherwise. """ return self._running async def run_once(self, now: Optional[datetime] = None) -> None: - """TODO + """Run the periodic function one time. - :param now: TODO - :return: TODO + :param now: An optional datetime expressing the current datetime. + :return: This method does not return anything. """ if now is None: now = current_datetime() diff --git a/minos/networks/scheduling/services.py b/minos/networks/scheduling/services.py index 62897919..40de2515 100644 --- a/minos/networks/scheduling/services.py +++ b/minos/networks/scheduling/services.py @@ -15,7 +15,7 @@ class TaskSchedulerService(Service): - """TODO""" + """Task Scheduler Service class.""" def __init__(self, **kwargs): super().__init__(**kwargs) From 350c564d3ad2cfb05521463b408a1addbeaf64eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Garc=C3=ADa=20Prado?= Date: Thu, 30 Sep 2021 08:08:45 +0200 Subject: [PATCH 18/21] ISSUE #399 * Add missing stop call into service. * Improve logging. --- minos/networks/scheduling/schedulers.py | 3 +++ minos/networks/scheduling/services.py | 1 + .../test_scheduling/test_services.py | 15 ++++++++++----- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/minos/networks/scheduling/schedulers.py b/minos/networks/scheduling/schedulers.py index 6732a008..3fdaaa75 100644 --- a/minos/networks/scheduling/schedulers.py +++ b/minos/networks/scheduling/schedulers.py @@ -134,6 +134,7 @@ async def start(self) -> None: :return: This method does not return anything. """ + logger.info("Starting periodic task...") self._task = asyncio.create_task(self.run_forever()) async def stop(self, timeout: Optional[float] = None) -> None: @@ -143,6 +144,7 @@ async def stop(self, timeout: Optional[float] = None) -> None: :return: This method does not return anything. """ if self._task is not None: + logger.info("Stopping periodic task...") self._task.cancel() with suppress(asyncio.TimeoutError, asyncio.CancelledError): await asyncio.wait_for(self._task, timeout) @@ -178,6 +180,7 @@ async def run_once(self, now: Optional[datetime] = None) -> None: now = current_datetime() request = SchedulingRequest(now) + logger.info("Running periodic task...") try: self._running = True with suppress(asyncio.CancelledError): diff --git a/minos/networks/scheduling/services.py b/minos/networks/scheduling/services.py index 40de2515..f5a462fb 100644 --- a/minos/networks/scheduling/services.py +++ b/minos/networks/scheduling/services.py @@ -41,6 +41,7 @@ async def stop(self, exception: Exception = None) -> None: :param exception: Optional exception that stopped the execution. :return: This method does not return anything. """ + await self.scheduler.stop() await self.scheduler.destroy() @cached_property diff --git a/tests/test_networks/test_scheduling/test_services.py b/tests/test_networks/test_scheduling/test_services.py index 8df66c13..e32112d2 100644 --- a/tests/test_networks/test_scheduling/test_services.py +++ b/tests/test_networks/test_scheduling/test_services.py @@ -35,26 +35,31 @@ async def test_start_stop(self): setup_mock = AsyncMock() destroy_mock = AsyncMock() - start = AsyncMock() + start_mock = AsyncMock() + stop_mock = AsyncMock() service.scheduler.setup = setup_mock service.scheduler.destroy = destroy_mock - service.scheduler.start = start + service.scheduler.start = start_mock + service.scheduler.stop = stop_mock await service.start() self.assertEqual(1, setup_mock.call_count) - self.assertEqual(1, start.call_count) + self.assertEqual(1, start_mock.call_count) + self.assertEqual(0, stop_mock.call_count) self.assertEqual(0, destroy_mock.call_count) setup_mock.reset_mock() destroy_mock.reset_mock() - start.reset_mock() + start_mock.reset_mock() + stop_mock.reset_mock() await service.stop() self.assertEqual(0, setup_mock.call_count) - self.assertEqual(0, start.call_count) + self.assertEqual(0, start_mock.call_count) + self.assertEqual(1, stop_mock.call_count) self.assertEqual(1, destroy_mock.call_count) From 752709260f4a1d9cc67b4cbc28545dcc67ad1b2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Garc=C3=ADa=20Prado?= Date: Thu, 30 Sep 2021 10:35:42 +0200 Subject: [PATCH 19/21] ISSUE #399 * Rename `SchedulingRequest` as `ScheduledRequest`. * Rename `SchedulingRequestContent` as `ScheduledRequestContent`. * Rename `TaskScheduler` as `PeriodicTaskScheduler`. * Rename `TaskSchedulerService` as `PeriodicTaskSchedulerService`. --- minos/networks/__init__.py | 8 +++---- minos/networks/scheduling/__init__.py | 8 +++---- minos/networks/scheduling/messages.py | 12 +++++----- minos/networks/scheduling/schedulers.py | 14 ++++++------ minos/networks/scheduling/services.py | 10 ++++----- .../test_scheduling/test_messages.py | 22 +++++++++---------- .../test_scheduling/test_schedulers.py | 16 +++++++------- .../test_scheduling/test_services.py | 14 ++++++------ 8 files changed, 52 insertions(+), 52 deletions(-) diff --git a/minos/networks/__init__.py b/minos/networks/__init__.py index 4520befc..bba9d61e 100644 --- a/minos/networks/__init__.py +++ b/minos/networks/__init__.py @@ -74,10 +74,10 @@ ) from .scheduling import ( PeriodicTask, - SchedulingRequest, - SchedulingRequestContent, - TaskScheduler, - TaskSchedulerService, + PeriodicTaskScheduler, + PeriodicTaskSchedulerService, + ScheduledRequest, + ScheduledRequestContent, ) from .snapshots import ( SnapshotService, diff --git a/minos/networks/scheduling/__init__.py b/minos/networks/scheduling/__init__.py index ea106405..5e6af8a4 100644 --- a/minos/networks/scheduling/__init__.py +++ b/minos/networks/scheduling/__init__.py @@ -1,11 +1,11 @@ from .messages import ( - SchedulingRequest, - SchedulingRequestContent, + ScheduledRequest, + ScheduledRequestContent, ) from .schedulers import ( PeriodicTask, - TaskScheduler, + PeriodicTaskScheduler, ) from .services import ( - TaskSchedulerService, + PeriodicTaskSchedulerService, ) diff --git a/minos/networks/scheduling/messages.py b/minos/networks/scheduling/messages.py index c30b1509..aeee1e05 100644 --- a/minos/networks/scheduling/messages.py +++ b/minos/networks/scheduling/messages.py @@ -21,7 +21,7 @@ ) -class SchedulingRequest(Request): +class ScheduledRequest(Request): """Scheduling Request class.""" def __init__(self, scheduled_at: datetime): @@ -36,11 +36,11 @@ def user(self) -> Optional[UUID]: """ return None - async def content(self, **kwargs) -> SchedulingRequestContent: + async def content(self, **kwargs) -> ScheduledRequestContent: """Get the request content. :param kwargs: Additional named arguments. - :return: A ``SchedulingRequestContent` intance.`. + :return: A ``ScheduledRequestContent` intance.`. """ return self._content @@ -51,11 +51,11 @@ def __repr__(self) -> str: return f"{type(self).__name__}({self._content!r})" @property - def _content(self) -> SchedulingRequestContent: - return SchedulingRequestContent(self._scheduled_at) + def _content(self) -> ScheduledRequestContent: + return ScheduledRequestContent(self._scheduled_at) -class SchedulingRequestContent(DeclarativeModel): +class ScheduledRequestContent(DeclarativeModel): """Scheduling Request Content class.""" scheduled_at: datetime diff --git a/minos/networks/scheduling/schedulers.py b/minos/networks/scheduling/schedulers.py index 3fdaaa75..835104c0 100644 --- a/minos/networks/scheduling/schedulers.py +++ b/minos/networks/scheduling/schedulers.py @@ -32,21 +32,21 @@ EnrouteBuilder, ) from .messages import ( - SchedulingRequest, + ScheduledRequest, ) logger = logging.getLogger(__name__) -class TaskScheduler(MinosSetup): - """Task Scheduler class.""" +class PeriodicTaskScheduler(MinosSetup): + """Periodic Task Scheduler class.""" def __init__(self, tasks: set[PeriodicTask], *args, **kwargs): super().__init__(*args, **kwargs) self._tasks = tasks @classmethod - def _from_config(cls, config: MinosConfig, **kwargs) -> TaskScheduler: + def _from_config(cls, config: MinosConfig, **kwargs) -> PeriodicTaskScheduler: tasks = cls._tasks_from_config(config, **kwargs) return cls(tasks, **kwargs) @@ -87,7 +87,7 @@ class PeriodicTask: _task: Optional[asyncio.Task] - def __init__(self, crontab: Union[str, CronTab], fn: Callable[[SchedulingRequest], Awaitable[None]]): + def __init__(self, crontab: Union[str, CronTab], fn: Callable[[ScheduledRequest], Awaitable[None]]): if isinstance(crontab, str): crontab = CronTab(crontab) @@ -105,7 +105,7 @@ def crontab(self) -> CronTab: return self._crontab @property - def fn(self) -> Callable[[SchedulingRequest], Awaitable[None]]: + def fn(self) -> Callable[[ScheduledRequest], Awaitable[None]]: """Get the function to be called periodically. :return: A function returning an awaitable. @@ -179,7 +179,7 @@ async def run_once(self, now: Optional[datetime] = None) -> None: if now is None: now = current_datetime() - request = SchedulingRequest(now) + request = ScheduledRequest(now) logger.info("Running periodic task...") try: self._running = True diff --git a/minos/networks/scheduling/services.py b/minos/networks/scheduling/services.py index f5a462fb..f5545bf8 100644 --- a/minos/networks/scheduling/services.py +++ b/minos/networks/scheduling/services.py @@ -8,13 +8,13 @@ ) from .schedulers import ( - TaskScheduler, + PeriodicTaskScheduler, ) logger = logging.getLogger(__name__) -class TaskSchedulerService(Service): +class PeriodicTaskSchedulerService(Service): """Task Scheduler Service class.""" def __init__(self, **kwargs): @@ -45,9 +45,9 @@ async def stop(self, exception: Exception = None) -> None: await self.scheduler.destroy() @cached_property - def scheduler(self) -> TaskScheduler: + def scheduler(self) -> PeriodicTaskScheduler: """Get the service scheduler. - :return: A ``TaskScheduler`` instance. + :return: A ``PeriodicTaskScheduler`` instance. """ - return TaskScheduler.from_config(**self._init_kwargs) + return PeriodicTaskScheduler.from_config(**self._init_kwargs) diff --git a/tests/test_networks/test_scheduling/test_messages.py b/tests/test_networks/test_scheduling/test_messages.py index 62445f8e..cec502a7 100644 --- a/tests/test_networks/test_scheduling/test_messages.py +++ b/tests/test_networks/test_scheduling/test_messages.py @@ -5,37 +5,37 @@ current_datetime, ) from minos.networks import ( - SchedulingRequest, - SchedulingRequestContent, + ScheduledRequest, + ScheduledRequestContent, ) -class TestSchedulingRequest(unittest.IsolatedAsyncioTestCase): +class TestScheduledRequest(unittest.IsolatedAsyncioTestCase): def setUp(self) -> None: self.now = current_datetime() - self.request = SchedulingRequest(self.now) + self.request = ScheduledRequest(self.now) async def test_content(self): - self.assertEqual(SchedulingRequestContent(self.now), await self.request.content()) + self.assertEqual(ScheduledRequestContent(self.now), await self.request.content()) def test_user(self): self.assertIsNone(self.request.user) def test_eq(self): - self.assertEqual(self.request, SchedulingRequest(self.now)) - self.assertNotEqual(self.request, SchedulingRequest(current_datetime())) + self.assertEqual(self.request, ScheduledRequest(self.now)) + self.assertNotEqual(self.request, ScheduledRequest(current_datetime())) def test_repr(self): - self.assertEqual(f"SchedulingRequest(SchedulingRequestContent(scheduled_at={self.now!s}))", repr(self.request)) + self.assertEqual(f"ScheduledRequest(ScheduledRequestContent(scheduled_at={self.now!s}))", repr(self.request)) -class TestSchedulingRequestContent(unittest.IsolatedAsyncioTestCase): +class TestScheduledRequestContent(unittest.IsolatedAsyncioTestCase): def test_subclass(self): - self.assertTrue(issubclass(SchedulingRequestContent, DeclarativeModel)) + self.assertTrue(issubclass(ScheduledRequestContent, DeclarativeModel)) def test_scheduled_at(self): now = current_datetime() - content = SchedulingRequestContent(now) + content = ScheduledRequestContent(now) self.assertEqual(now, content.scheduled_at) diff --git a/tests/test_networks/test_scheduling/test_schedulers.py b/tests/test_networks/test_scheduling/test_schedulers.py index 1879dbb6..229800b1 100644 --- a/tests/test_networks/test_scheduling/test_schedulers.py +++ b/tests/test_networks/test_scheduling/test_schedulers.py @@ -16,18 +16,18 @@ ) from minos.networks import ( PeriodicTask, - SchedulingRequest, - TaskScheduler, + PeriodicTaskScheduler, + ScheduledRequest, ) from tests.utils import ( BASE_PATH, ) -class TestTaskScheduler(unittest.IsolatedAsyncioTestCase): +class TestPeriodicTaskScheduler(unittest.IsolatedAsyncioTestCase): def test_from_config(self): config = MinosConfig(BASE_PATH / "test_config.yml") - scheduler = TaskScheduler.from_config(config) + scheduler = PeriodicTaskScheduler.from_config(config) self.assertEqual(1, len(scheduler.tasks)) self.assertTrue(all(map(lambda t: isinstance(t, PeriodicTask), scheduler.tasks))) @@ -35,7 +35,7 @@ def test_tasks(self): # noinspection PyTypeChecker tasks = {PeriodicTask("@daily", None), PeriodicTask("@hourly", None)} - scheduler = TaskScheduler(tasks) + scheduler = PeriodicTaskScheduler(tasks) self.assertEqual(tasks, scheduler.tasks) @@ -45,7 +45,7 @@ async def test_start(self): tasks = {tasks_1_mock, tasks_2_mock} - scheduler = TaskScheduler(tasks) + scheduler = PeriodicTaskScheduler(tasks) await scheduler.start() @@ -61,7 +61,7 @@ async def test_stop(self): tasks = {tasks_1_mock, tasks_2_mock} - scheduler = TaskScheduler(tasks) + scheduler = PeriodicTaskScheduler(tasks) await scheduler.stop(timeout=30) @@ -118,7 +118,7 @@ async def test_run_once(self) -> None: self.assertEqual(1, self.fn_mock.call_count) observed = self.fn_mock.call_args.args[0] - self.assertIsInstance(observed, SchedulingRequest) + self.assertIsInstance(observed, ScheduledRequest) self.assertEqual(now, (await observed.content()).scheduled_at) async def test_run_once_handle_exceptions(self) -> None: diff --git a/tests/test_networks/test_scheduling/test_services.py b/tests/test_networks/test_scheduling/test_services.py index e32112d2..9b59c954 100644 --- a/tests/test_networks/test_scheduling/test_services.py +++ b/tests/test_networks/test_scheduling/test_services.py @@ -11,27 +11,27 @@ PostgresAsyncTestCase, ) from minos.networks import ( - TaskScheduler, - TaskSchedulerService, + PeriodicTaskScheduler, + PeriodicTaskSchedulerService, ) from tests.utils import ( BASE_PATH, ) -class TestTaskSchedulerService(PostgresAsyncTestCase): +class TestPeriodicTaskSchedulerService(PostgresAsyncTestCase): CONFIG_FILE_PATH = BASE_PATH / "test_config.yml" def test_is_instance(self): - service = TaskSchedulerService(config=self.config) + service = PeriodicTaskSchedulerService(config=self.config) self.assertIsInstance(service, Service) def test_dispatcher(self): - service = TaskSchedulerService(config=self.config) - self.assertIsInstance(service.scheduler, TaskScheduler) + service = PeriodicTaskSchedulerService(config=self.config) + self.assertIsInstance(service.scheduler, PeriodicTaskScheduler) async def test_start_stop(self): - service = TaskSchedulerService(config=self.config) + service = PeriodicTaskSchedulerService(config=self.config) setup_mock = AsyncMock() destroy_mock = AsyncMock() From a3624e2161520d4185255e4805131e7dea6c2065 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Garc=C3=ADa=20Prado?= Date: Mon, 4 Oct 2021 12:39:13 +0200 Subject: [PATCH 20/21] ISSUE #399 * Add `ScheduledResponseException`. * Minor improvements. --- minos/networks/__init__.py | 1 + minos/networks/handlers/commands/handlers.py | 8 ++------ minos/networks/handlers/events/handlers.py | 7 ++----- minos/networks/rest/handlers.py | 8 ++------ minos/networks/scheduling/__init__.py | 1 + minos/networks/scheduling/messages.py | 5 +++++ minos/networks/scheduling/schedulers.py | 14 ++++++++++++-- .../test_handlers/test_commands/test_handlers.py | 10 ---------- .../test_handlers/test_events/test_handlers.py | 9 --------- tests/test_networks/test_rest/test_handlers.py | 10 ---------- .../test_scheduling/test_schedulers.py | 4 ++++ 11 files changed, 29 insertions(+), 48 deletions(-) diff --git a/minos/networks/__init__.py b/minos/networks/__init__.py index bba9d61e..8d4a2149 100644 --- a/minos/networks/__init__.py +++ b/minos/networks/__init__.py @@ -78,6 +78,7 @@ PeriodicTaskSchedulerService, ScheduledRequest, ScheduledRequestContent, + ScheduledResponseException, ) from .snapshots import ( SnapshotService, diff --git a/minos/networks/handlers/commands/handlers.py b/minos/networks/handlers/commands/handlers.py index 585eabda..31ea5973 100644 --- a/minos/networks/handlers/commands/handlers.py +++ b/minos/networks/handlers/commands/handlers.py @@ -25,7 +25,6 @@ CommandStatus, MinosBroker, MinosConfig, - MinosException, ) from ...decorators import ( @@ -105,13 +104,10 @@ async def _fn(command: Command) -> Tuple[Any, CommandStatus]: response = await response.content() return response, CommandStatus.SUCCESS except ResponseException as exc: - logger.info(f"Raised a user exception: {exc!s}") + logger.warning(f"Raised an application exception: {exc!s}") return repr(exc), CommandStatus.ERROR - except MinosException as exc: - logger.warning(f"Raised a 'minos' exception: {exc!r}") - return repr(exc), CommandStatus.SYSTEM_ERROR except Exception as exc: - logger.exception(f"Raised an exception: {exc!r}.") + logger.exception(f"Raised a system exception: {exc!r}") return repr(exc), CommandStatus.SYSTEM_ERROR return _fn diff --git a/minos/networks/handlers/events/handlers.py b/minos/networks/handlers/events/handlers.py index 0720040d..d92fd8d3 100644 --- a/minos/networks/handlers/events/handlers.py +++ b/minos/networks/handlers/events/handlers.py @@ -24,7 +24,6 @@ from minos.common import ( Event, MinosConfig, - MinosException, ) from ...decorators import ( @@ -106,10 +105,8 @@ async def _fn(event: Event) -> None: if isawaitable(response): await response except ResponseException as exc: - logger.info(f"Raised a user exception: {exc!s}") - except MinosException as exc: - logger.warning(f"Raised a 'minos' exception: {exc!r}") + logger.warning(f"Raised an application exception: {exc!s}") except Exception as exc: - logger.exception(f"Raised an exception: {exc!r}.") + logger.exception(f"Raised a system exception: {exc!r}") return _fn diff --git a/minos/networks/rest/handlers.py b/minos/networks/rest/handlers.py index 88d44a22..c99afe06 100644 --- a/minos/networks/rest/handlers.py +++ b/minos/networks/rest/handlers.py @@ -22,7 +22,6 @@ from minos.common import ( MinosConfig, - MinosException, MinosSetup, ) @@ -139,13 +138,10 @@ async def _fn(request: web.Request) -> web.Response: return web.json_response() return web.json_response(response) except ResponseException as exc: - logger.info(f"Raised a user exception: {exc!s}") + logger.warning(f"Raised an application exception: {exc!s}") raise web.HTTPBadRequest(text=str(exc)) - except MinosException as exc: - logger.warning(f"Raised a 'minos' exception: {exc!r}") - raise web.HTTPInternalServerError() except Exception as exc: - logger.exception(f"Raised an exception: {exc!r}.") + logger.exception(f"Raised a system exception: {exc!r}") raise web.HTTPInternalServerError() return _fn diff --git a/minos/networks/scheduling/__init__.py b/minos/networks/scheduling/__init__.py index 5e6af8a4..dc5fb006 100644 --- a/minos/networks/scheduling/__init__.py +++ b/minos/networks/scheduling/__init__.py @@ -1,6 +1,7 @@ from .messages import ( ScheduledRequest, ScheduledRequestContent, + ScheduledResponseException, ) from .schedulers import ( PeriodicTask, diff --git a/minos/networks/scheduling/messages.py b/minos/networks/scheduling/messages.py index aeee1e05..c1754b7e 100644 --- a/minos/networks/scheduling/messages.py +++ b/minos/networks/scheduling/messages.py @@ -18,6 +18,7 @@ from ..messages import ( Request, + ResponseException, ) @@ -59,3 +60,7 @@ class ScheduledRequestContent(DeclarativeModel): """Scheduling Request Content class.""" scheduled_at: datetime + + +class ScheduledResponseException(ResponseException): + """Scheduled Response Exception class.""" diff --git a/minos/networks/scheduling/schedulers.py b/minos/networks/scheduling/schedulers.py index 835104c0..12e82626 100644 --- a/minos/networks/scheduling/schedulers.py +++ b/minos/networks/scheduling/schedulers.py @@ -10,6 +10,9 @@ from datetime import ( datetime, ) +from inspect import ( + isawaitable, +) from typing import ( Awaitable, Callable, @@ -31,6 +34,9 @@ from ..decorators import ( EnrouteBuilder, ) +from ..messages import ( + ResponseException, +) from .messages import ( ScheduledRequest, ) @@ -184,8 +190,12 @@ async def run_once(self, now: Optional[datetime] = None) -> None: try: self._running = True with suppress(asyncio.CancelledError): - await self._fn(request) + response = self._fn(request) + if isawaitable(response): + await response + except ResponseException as exc: + logger.warning(f"Raised an application exception: {exc!s}") except Exception as exc: - logger.warning(f"Raised exception while executing periodic task: {exc}") + logger.exception(f"Raised a system exception: {exc!r}") finally: self._running = False diff --git a/tests/test_networks/test_handlers/test_commands/test_handlers.py b/tests/test_networks/test_handlers/test_commands/test_handlers.py index 94598645..2489fda9 100644 --- a/tests/test_networks/test_handlers/test_commands/test_handlers.py +++ b/tests/test_networks/test_handlers/test_commands/test_handlers.py @@ -21,7 +21,6 @@ HandlerRequest, HandlerResponse, HandlerResponseException, - MinosActionNotFoundException, Request, Response, ) @@ -45,10 +44,6 @@ async def _fn_none(request: Request): async def _fn_raises_response(request: Request) -> Response: raise HandlerResponseException("foo") - @staticmethod - async def _fn_raises_minos(request: Request) -> Response: - raise MinosActionNotFoundException("bar") - @staticmethod async def _fn_raises_exception(request: Request) -> Response: raise ValueError @@ -117,11 +112,6 @@ async def test_get_callback_raises_response(self): expected = (repr(HandlerResponseException("foo")), CommandStatus.ERROR) self.assertEqual(expected, await fn(self.command)) - async def test_get_callback_raises_minos(self): - fn = self.handler.get_callback(_Cls._fn_raises_minos) - expected = (repr(MinosActionNotFoundException("bar")), CommandStatus.SYSTEM_ERROR) - self.assertEqual(expected, await fn(self.command)) - async def test_get_callback_raises_exception(self): fn = self.handler.get_callback(_Cls._fn_raises_exception) expected = (repr(ValueError()), CommandStatus.SYSTEM_ERROR) diff --git a/tests/test_networks/test_handlers/test_events/test_handlers.py b/tests/test_networks/test_handlers/test_events/test_handlers.py index ab0d21db..7a79cd4a 100644 --- a/tests/test_networks/test_handlers/test_events/test_handlers.py +++ b/tests/test_networks/test_handlers/test_events/test_handlers.py @@ -31,7 +31,6 @@ HandlerEntry, HandlerRequest, HandlerResponseException, - MinosActionNotFoundException, Request, ) from tests.utils import ( @@ -49,10 +48,6 @@ async def _fn(request: Request): async def _fn_raises_response(request: Request): raise HandlerResponseException("") - @staticmethod - async def _fn_raises_minos(request: Request): - raise MinosActionNotFoundException("") - @staticmethod async def _fn_raises_exception(request: Request): raise ValueError @@ -114,10 +109,6 @@ async def test_get_callback_raises_response(self): fn = self.handler.get_callback(_Cls._fn_raises_response) await fn(self.event) - async def test_get_callback_raises_minos(self): - fn = self.handler.get_callback(_Cls._fn_raises_minos) - await fn(self.event) - async def test_get_callback_raises_exception(self): fn = self.handler.get_callback(_Cls._fn_raises_exception) await fn(self.event) diff --git a/tests/test_networks/test_rest/test_handlers.py b/tests/test_networks/test_rest/test_handlers.py index c5e7239c..f2185f61 100644 --- a/tests/test_networks/test_rest/test_handlers.py +++ b/tests/test_networks/test_rest/test_handlers.py @@ -15,7 +15,6 @@ PostgresAsyncTestCase, ) from minos.networks import ( - MinosActionNotFoundException, Request, Response, RestHandler, @@ -40,10 +39,6 @@ async def _fn_none(request: Request): async def _fn_raises_response(request: Request) -> Response: raise RestResponseException("") - @staticmethod - async def _fn_raises_minos(request: Request) -> Response: - raise MinosActionNotFoundException("") - @staticmethod async def _fn_raises_exception(request: Request) -> Response: raise ValueError @@ -97,11 +92,6 @@ async def test_get_callback_raises_response(self): with self.assertRaises(HTTPBadRequest): await handler(MockedRequest({"foo": "bar"})) - async def test_get_callback_raises_minos(self): - handler = self.handler.get_callback(_Cls._fn_raises_minos) - with self.assertRaises(HTTPInternalServerError): - await handler(MockedRequest({"foo": "bar"})) - async def test_get_callback_raises_exception(self): handler = self.handler.get_callback(_Cls._fn_raises_exception) with self.assertRaises(HTTPInternalServerError): diff --git a/tests/test_networks/test_scheduling/test_schedulers.py b/tests/test_networks/test_scheduling/test_schedulers.py index 229800b1..a6a15a1f 100644 --- a/tests/test_networks/test_scheduling/test_schedulers.py +++ b/tests/test_networks/test_scheduling/test_schedulers.py @@ -18,6 +18,7 @@ PeriodicTask, PeriodicTaskScheduler, ScheduledRequest, + ScheduledResponseException, ) from tests.utils import ( BASE_PATH, @@ -128,6 +129,9 @@ async def test_run_once_handle_exceptions(self) -> None: self.fn_mock.side_effect = Exception await self.periodic.run_once() + self.fn_mock.side_effect = ScheduledResponseException("") + await self.periodic.run_once() + self.assertTrue(True) async def test_run_once_running(self) -> None: From f5fa5853e68ec18fd602a9bec3306ccd8b12aabe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Garc=C3=ADa=20Prado?= Date: Mon, 4 Oct 2021 12:57:26 +0200 Subject: [PATCH 21/21] v0.0.18 --- HISTORY.md | 6 ++++++ minos/networks/__init__.py | 2 +- pyproject.toml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 2e9a50f2..a90344c2 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -103,3 +103,9 @@ History * Add support for multiple handling functions for events. * Fix troubles related with dependency injections. + +0.0.18 (2021-10-04) +------------------ + +* Add `PeriodicTask`, `PeriodicTaskScheduler` and `PeriodicTaskSchedulerService`. +* Add `@enroute.periodic.event` decorator diff --git a/minos/networks/__init__.py b/minos/networks/__init__.py index 8d4a2149..03124705 100644 --- a/minos/networks/__init__.py +++ b/minos/networks/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.0.17" +__version__ = "0.0.18" from .brokers import ( Broker, diff --git a/pyproject.toml b/pyproject.toml index 29d5c41f..81167d9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "minos_microservice_networks" -version = "0.0.17" +version = "0.0.18" description = "Python Package with the common network classes and utilities used in Minos Microservice." readme = "README.md" repository = "https://github.com/clariteia/minos_microservice_network"