├── didiator ├── py.typed ├── ioc │ ├── __init__.py │ └── dishka.py ├── utils │ ├── __init__.py │ └── di_builder.py ├── interface │ ├── utils │ │ ├── __init__.py │ │ └── di_builder.py │ ├── entities.py │ ├── handlers │ │ ├── __init__.py │ │ ├── request.py │ │ └── event.py │ ├── __init__.py │ ├── exceptions.py │ ├── ioc.py │ └── mediator.py ├── middlewares │ ├── __init__.py │ ├── base.py │ ├── logging.py │ └── di.py ├── __init__.py └── mediator.py ├── tests ├── __init__.py ├── mocks │ ├── __init__.py │ └── middlewares.py ├── unit │ ├── __init__.py │ ├── test_middlewares.py │ ├── test_query_dispatcher.py │ ├── test_dispatcher.py │ ├── test_mediator.py │ └── test_di_middleware.py ├── integration │ ├── __init__.py │ └── test_di_library.py └── conftest.py ├── .gitignore ├── examples ├── __init__.py ├── simple.py ├── aiohttp_with_sqlalchemy.py └── aiogram_with_sqlalchemy.py ├── pytest.ini ├── mypy.ini ├── Makefile ├── .flake8 ├── LICENSE.rst ├── pyproject.toml ├── README.rst ├── .pylintrc └── poetry.lock /didiator/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/mocks/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /didiator/ioc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /didiator/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /didiator/interface/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = 3 | tests 4 | asyncio_mode = auto 5 | -------------------------------------------------------------------------------- /didiator/middlewares/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import Middleware 2 | 3 | __all__ = ( 4 | "Middleware", 5 | ) 6 | -------------------------------------------------------------------------------- /didiator/interface/entities.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol, TypeVar 2 | 3 | RRes = TypeVar("RRes") 4 | 5 | 6 | class Request(Protocol[RRes]): 7 | pass 8 | 9 | 10 | class Event(Request[None], Protocol): 11 | pass 12 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version = 3.10 3 | strict = true 4 | pretty = true 5 | disallow_any_explicit = false 6 | disallow_any_generics = false 7 | warn_unreachable = true 8 | show_column_numbers = true 9 | show_error_context = true 10 | -------------------------------------------------------------------------------- /didiator/interface/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | from .event import EventHandler, EventHandlerType 2 | from .request import Handler, HandlerType 3 | 4 | __all__ = ( 5 | "Handler", 6 | "HandlerType", 7 | "EventHandler", 8 | "EventHandlerType", 9 | ) 10 | -------------------------------------------------------------------------------- /didiator/interface/__init__.py: -------------------------------------------------------------------------------- 1 | from .entities import Event, Request 2 | from .handlers import EventHandler, Handler 3 | from .mediator import Mediator 4 | 5 | __all__ = ( 6 | "Mediator", 7 | "Request", 8 | "Handler", 9 | "Event", 10 | "EventHandler", 11 | ) 12 | -------------------------------------------------------------------------------- /didiator/interface/exceptions.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from didiator.interface.entities import Request 4 | 5 | 6 | class MediatorError(Exception): 7 | pass 8 | 9 | 10 | class HandlerNotFound(MediatorError, TypeError): 11 | request: Request[Any] 12 | 13 | def __init__(self, text: str, request: Request[Any]): 14 | super().__init__(text) 15 | self.request = request 16 | -------------------------------------------------------------------------------- /didiator/__init__.py: -------------------------------------------------------------------------------- 1 | from .interface.entities import Event, Request 2 | from .interface.handlers import EventHandler, Handler 3 | from .interface.ioc import Ioc 4 | from .interface.mediator import Mediator 5 | from .mediator import MediatorImpl 6 | 7 | __version__ = "0.4.0" 8 | 9 | __all__ = ( 10 | "__version__", 11 | "MediatorImpl", 12 | "Mediator", 13 | "Ioc", 14 | "Request", 15 | "Event", 16 | "Handler", 17 | "EventHandler", 18 | ) 19 | -------------------------------------------------------------------------------- /didiator/interface/handlers/request.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | from typing import Any, Protocol, TypeVar 4 | 5 | from didiator.interface.entities import Request 6 | 7 | RRes = TypeVar("RRes") 8 | R = TypeVar("R", bound=Request[Any]) 9 | 10 | 11 | class Handler(Protocol[R, RRes]): 12 | @abc.abstractmethod 13 | async def __call__(self, request: R) -> RRes: 14 | raise NotImplementedError 15 | 16 | 17 | HandlerType = type[Handler[R, RRes]] | Handler[R, RRes] 18 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from didiator.dispatchers.command import CommandDispatcherImpl 4 | from didiator.dispatchers.query import QueryDispatcherImpl 5 | 6 | 7 | @pytest.fixture() 8 | def command_dispatcher() -> CommandDispatcherImpl: 9 | command_dispatcher = CommandDispatcherImpl() 10 | return command_dispatcher 11 | 12 | 13 | @pytest.fixture() 14 | def query_dispatcher() -> QueryDispatcherImpl: 15 | query_dispatcher = QueryDispatcherImpl() 16 | return query_dispatcher 17 | -------------------------------------------------------------------------------- /didiator/interface/ioc.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from collections.abc import AsyncIterator 3 | from contextlib import asynccontextmanager 4 | 5 | from typing import Any, Protocol, TypeVar 6 | 7 | from didiator.interface import Handler, Request 8 | from didiator.interface.handlers import HandlerType 9 | 10 | R = TypeVar("R", bound=Request[Any]) 11 | RRes = TypeVar("RRes") 12 | 13 | 14 | class Ioc(Protocol): 15 | @abstractmethod 16 | @asynccontextmanager 17 | async def provide(self, handler: HandlerType[R, RRes]) -> AsyncIterator[Handler[R, RRes]]: 18 | raise NotImplementedError 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | py := poetry run 2 | package_dir := didiator 3 | tests_dir := tests 4 | code_dir := $(package_dir) $(tests_dir) 5 | 6 | .PHONY: help 7 | help: 8 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 9 | 10 | .PHONY: install 11 | install: ## Install package with dependencies 12 | poetry install --with dev,test,lint -E di 13 | 14 | .PHONY: lint 15 | lint: ## Lint code with flake8, pylint, mypy 16 | $(py) flake8 $(code_dir) --exit-zero 17 | $(py) pylint $(code_dir) --exit-zero 18 | $(py) mypy $(package_dir) || true 19 | 20 | .PHONY: test 21 | test: ## Run tests 22 | $(py) pytest $(tests_dir) 23 | -------------------------------------------------------------------------------- /didiator/ioc/dishka.py: -------------------------------------------------------------------------------- 1 | from collections.abc import AsyncIterator 2 | from contextlib import asynccontextmanager 3 | from typing import Any, TypeVar 4 | 5 | from didiator.interface import Handler, Request 6 | from didiator.interface.handlers import HandlerType 7 | from didiator.interface.ioc import Ioc 8 | from dishka import AsyncContainer 9 | 10 | R = TypeVar("R", bound=Request[Any]) 11 | RRes = TypeVar("RRes") 12 | 13 | 14 | class DishkaIoc(Ioc): 15 | def __init__(self, container: AsyncContainer) -> None: 16 | self._container = container 17 | 18 | @asynccontextmanager 19 | async def provide(self, handler: HandlerType[R, RRes]) -> AsyncIterator[Handler[R, RRes]]: 20 | async with self._container() as request_container: 21 | yield await request_container.get(handler) 22 | -------------------------------------------------------------------------------- /didiator/middlewares/base.py: -------------------------------------------------------------------------------- 1 | import functools 2 | from collections.abc import Sequence 3 | 4 | from typing import Any, Protocol, TypeVar 5 | 6 | from didiator.interface.entities import Request 7 | from didiator.interface.handlers import Handler 8 | 9 | RRes = TypeVar("RRes") 10 | R = TypeVar("R", bound=Request[Any]) 11 | 12 | 13 | class Middleware(Protocol[R, RRes]): 14 | async def __call__( 15 | self, 16 | handler: Handler[R, RRes], 17 | request: R, 18 | ) -> RRes: 19 | return await handler(request) 20 | 21 | 22 | def wrap_middleware( 23 | middlewares: Sequence[Middleware[R, RRes]], 24 | handler: Handler[R, RRes], 25 | ) -> Handler[R, RRes]: 26 | for middleware in reversed(middlewares): 27 | handler = functools.partial(middleware, handler) 28 | 29 | return handler 30 | -------------------------------------------------------------------------------- /didiator/interface/mediator.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Sequence 2 | 3 | from didiator.interface.entities import Event, Request 4 | from typing import Any, Protocol, Type, TypeVar 5 | 6 | from didiator.interface.handlers import HandlerType 7 | from didiator.interface.handlers.event import EventHandlerType 8 | 9 | R = TypeVar("R", bound=Request[Any]) 10 | RRes = TypeVar("RRes") 11 | E = TypeVar("E", bound=Event) 12 | 13 | 14 | class Mediator(Protocol): 15 | def register_request_handler(self, request: Type[R], handler: HandlerType[R, RRes]) -> None: 16 | raise NotImplementedError 17 | 18 | def register_event_handler(self, event: Type[E], handler: EventHandlerType[E]) -> None: 19 | raise NotImplementedError 20 | 21 | async def send(self, request: Request[RRes]) -> RRes: 22 | raise NotImplementedError 23 | 24 | async def publish(self, events: Event | Sequence[Event]) -> None: 25 | raise NotImplementedError 26 | -------------------------------------------------------------------------------- /didiator/interface/handlers/event.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | from typing import Any, Generic, Protocol, TypeVar 4 | 5 | from didiator.interface.entities import Event 6 | from .request import Handler 7 | 8 | E = TypeVar("E", bound=Event) 9 | 10 | 11 | class EventHandler(Handler[E, Any], Protocol[E]): 12 | @abc.abstractmethod 13 | async def __call__(self, event: E) -> Any: 14 | raise NotImplementedError 15 | 16 | 17 | EventHandlerType = type[EventHandler[E]] | EventHandler[E] 18 | 19 | 20 | class EventListener(Generic[E]): 21 | def __init__(self, event: type[E], handler: EventHandlerType[E]): 22 | self._event = event 23 | self._handler = handler 24 | 25 | def is_listen(self, event: Event) -> bool: 26 | return isinstance(event, self._event) 27 | 28 | @property 29 | def event(self) -> type[E]: 30 | return self._event 31 | 32 | @property 33 | def handler(self) -> EventHandlerType[E]: 34 | return self._handler 35 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | count=False 3 | statistics=False 4 | show-source=False 5 | 6 | max-line-length=120 7 | 8 | application-import-names=dataclass_factory 9 | exclude= 10 | .venv, 11 | docs, 12 | benchmarks 13 | docstring-convention=pep257 14 | ignore= 15 | # A003 class attribute "..." is shadowing a python builtin 16 | A003, 17 | # D100 Missing docstring in public module 18 | D100, 19 | # D101 Missing docstring in public class 20 | D101, 21 | # D102 Missing docstring in public method 22 | D102, 23 | # D103 Missing docstring in public function 24 | D103, 25 | # D104 Missing docstring in public package 26 | D104, 27 | # D105 Missing docstring in magic method 28 | D105, 29 | # D107 Missing docstring in __init__ 30 | D107, 31 | # W503 line break before binary operator 32 | W503, 33 | # W504 line break after binary operator 34 | W504, 35 | # B008 Do not perform function calls in argument defaults. 36 | B008, 37 | 38 | max-cognitive-complexity=12 39 | max-complexity=12 40 | per-file-ignores= 41 | **/__init__.py:F401 42 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 SamWarden 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/mocks/middlewares.py: -------------------------------------------------------------------------------- 1 | from typing import Any, TypeVar 2 | 3 | from didiator.interface.dispatchers.request import HandlerType 4 | from didiator.middlewares.base import Middleware 5 | from didiator.interface.entities.request import Request 6 | 7 | RRes = TypeVar("RRes") 8 | R = TypeVar("R", bound=Request[Any]) 9 | 10 | 11 | class DataAdderMiddlewareMock(Middleware): 12 | def __init__(self, **kwargs): 13 | self._additional_kwargs = kwargs 14 | 15 | async def __call__( 16 | self, 17 | handler: HandlerType[R, RRes], 18 | request: R, 19 | *args: Any, 20 | **kwargs: Any, 21 | ) -> RRes: 22 | kwargs |= self._additional_kwargs 23 | return await self._call(handler, request, *args, **kwargs) 24 | 25 | 26 | class DataRemoverMiddlewareMock(Middleware): 27 | def __init__(self, *args): 28 | self._removable_args = args 29 | 30 | async def __call__( 31 | self, 32 | handler: HandlerType, 33 | request: R, 34 | *args: Any, 35 | **kwargs: Any, 36 | ) -> RRes: 37 | kwargs = {key: val for key, val in kwargs.items() if key not in self._removable_args} 38 | return await self._call(handler, request, *args, **kwargs) 39 | -------------------------------------------------------------------------------- /didiator/interface/utils/di_builder.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Mapping 2 | from typing import Any, ContextManager, Protocol, TypeVar 3 | 4 | from di import ScopeState, SolvedDependent 5 | from di._container import BindHook 6 | from di._utils.types import FusedContextManager 7 | from di.api.providers import DependencyProvider, DependencyProviderType 8 | from di.api.scopes import Scope 9 | 10 | DependencyType = TypeVar("DependencyType") 11 | 12 | 13 | class DiBuilder(Protocol): 14 | di_scopes: list[Scope] 15 | 16 | def bind(self, hook: BindHook) -> ContextManager[None]: 17 | raise NotImplementedError 18 | 19 | def enter_scope(self, scope: Scope, state: ScopeState | None = None) -> FusedContextManager[ScopeState]: 20 | raise NotImplementedError 21 | 22 | async def execute( 23 | self, call: DependencyProviderType[DependencyType], scope: Scope, 24 | *, state: ScopeState, values: Mapping[DependencyProvider, Any] | None = None, 25 | ) -> DependencyType: 26 | raise NotImplementedError 27 | 28 | def solve(self, call: DependencyProviderType[DependencyType], scope: Scope) -> SolvedDependent[DependencyType]: 29 | raise NotImplementedError 30 | 31 | def copy(self) -> "DiBuilder": 32 | raise NotImplementedError 33 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "didiator" 3 | version = "0.4.0" 4 | description = "A library that implements the Mediator pattern and uses DI library" 5 | authors = [ 6 | "SamWarden ", 7 | ] 8 | maintainers = [ 9 | "SamWarden ", 10 | ] 11 | license = "MIT" 12 | readme = "README.rst" 13 | homepage = "https://github.com/SamWarden/didiator" 14 | repository = "https://github.com/SamWarden/didiator" 15 | keywords = [ 16 | "didiator", 17 | "mediatr", 18 | "mediator", 19 | "CQRS", 20 | "DI", 21 | "events", 22 | "ioc", 23 | ] 24 | classifiers = [ 25 | "License :: OSI Approved :: MIT License", 26 | "Typing :: Typed", 27 | "Operating System :: OS Independent", 28 | "Intended Audience :: Developers", 29 | "Programming Language :: Python :: 3.10", 30 | "Programming Language :: Python :: 3.11", 31 | "Topic :: Software Development :: Libraries :: Python Modules", 32 | ] 33 | 34 | [tool.poetry.urls] 35 | "Bug Tracker" = "https://github.com/SamWarden/didiator/issues" 36 | 37 | [tool.poetry.dependencies] 38 | python = "^3.11,<4" 39 | di = {version = "^0.79.2", extras = ["anyio"], optional = true} 40 | dishka = "^1.3.0" 41 | 42 | [tool.poetry.extras] 43 | di = ["di"] 44 | dishka = ["dishka"] 45 | 46 | [tool.poetry.group.dev] 47 | optional = true 48 | 49 | [tool.poetry.group.dev.dependencies] 50 | 51 | [tool.poetry.group.test] 52 | optional = true 53 | 54 | [tool.poetry.group.test.dependencies] 55 | pytest = "^7.2.0" 56 | pytest-asyncio = "^0.20.3" 57 | 58 | [tool.poetry.group.lint] 59 | optional = true 60 | 61 | [tool.poetry.group.lint.dependencies] 62 | pylint = "^2.15.9" 63 | mypy = "^0.991" 64 | flake8 = "^6.0.0" 65 | 66 | [build-system] 67 | requires = ["poetry-core>=1.0.0"] 68 | build-backend = "poetry.core.masonry.api" 69 | -------------------------------------------------------------------------------- /didiator/mediator.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Sequence 2 | 3 | from typing import Any, TypeVar 4 | 5 | from didiator.interface import Request 6 | from didiator.interface.entities import Event 7 | from didiator.interface.exceptions import HandlerNotFound 8 | from didiator.interface.handlers import HandlerType 9 | from didiator.interface.handlers.event import EventHandlerType 10 | from didiator.interface.handlers.event import EventListener 11 | from didiator.interface.ioc import Ioc 12 | from didiator.interface.mediator import Mediator 13 | from didiator.middlewares.base import Middleware, wrap_middleware 14 | 15 | R = TypeVar("R", bound=Request[Any]) 16 | RRes = TypeVar("RRes") 17 | E = TypeVar("E", bound=Event) 18 | 19 | 20 | class MediatorImpl(Mediator): 21 | def __init__(self, *, ioc: Ioc, middlewares: list[Middleware]) -> None: 22 | self._request_handlers: dict[type[Request[Any]], HandlerType[Any, Any]] = {} 23 | self._event_listeners: list[EventListener] = [] 24 | self._middlewares = middlewares 25 | self._ioc = ioc 26 | 27 | def register_request_handler(self, request: type[R], handler: HandlerType[R, RRes]) -> None: 28 | self._request_handlers[request] = handler 29 | 30 | def register_event_handler(self, event: type[E], handler: EventHandlerType[E]) -> None: 31 | listener = EventListener(event, handler) 32 | self._event_listeners.append(listener) 33 | 34 | async def send(self, request: Request[RRes]) -> RRes: 35 | try: 36 | handler = self._request_handlers[type(request)] 37 | except KeyError as err: 38 | raise HandlerNotFound( 39 | f"Request handler for {type(request).__name__} request is not registered", request, 40 | ) from err 41 | 42 | async with self._ioc.provide(handler) as initialized_handler: 43 | wrapped_handler = wrap_middleware(self._middlewares, initialized_handler) 44 | return await wrapped_handler(request) 45 | 46 | async def publish(self, events: Event | Sequence[Event]) -> None: 47 | if not isinstance(events, Sequence): 48 | events = [events] 49 | 50 | for event in events: 51 | for listener in self._event_listeners: 52 | if listener.is_listen(event): 53 | async with self._ioc.provide(listener.handler) as initialized_handler: 54 | wrapped_handler = wrap_middleware(self._middlewares, initialized_handler) 55 | return await wrapped_handler(event) 56 | -------------------------------------------------------------------------------- /didiator/middlewares/logging.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Protocol, TypeVar 2 | import logging 3 | 4 | from didiator.interface.entities.command import Command 5 | from didiator.interface.entities.event import Event 6 | from didiator.interface.entities.request import Request 7 | from didiator.interface.entities.query import Query 8 | from didiator.interface.handlers import HandlerType 9 | from didiator.middlewares import Middleware 10 | 11 | RRes = TypeVar("RRes") 12 | R = TypeVar("R", bound=Request[Any]) 13 | 14 | 15 | class Logger(Protocol): 16 | def log(self, level: int, msg: str, *args: Any, extra: dict[str, Any] | None = None) -> None: 17 | raise NotImplementedError 18 | 19 | 20 | class LoggingMiddleware(Middleware): 21 | def __init__(self, logger: Logger | str = __name__, level: int | str = logging.DEBUG): 22 | if isinstance(logger, str): 23 | logger = logging.getLogger(logger) 24 | 25 | self._logger: Logger = logger 26 | self._level: int = logging.getLevelName(level) if isinstance(level, str) else level 27 | 28 | async def __call__( 29 | self, 30 | handler: HandlerType[R, RRes], 31 | request: R, 32 | *args: Any, 33 | **kwargs: Any, 34 | ) -> RRes: 35 | if isinstance(request, Command): 36 | self._logger.log(self._level, "Send %s command", type(request).__name__, extra={"command": request}) 37 | elif isinstance(request, Query): 38 | self._logger.log(self._level, "Make %s query", type(request).__name__, extra={"query": request}) 39 | elif isinstance(request, Event): 40 | self._logger.log(self._level, "Publish %s event", type(request).__name__, extra={"event": request}) 41 | else: 42 | self._logger.log(self._level, "Execute %s request", type(request).__name__, extra={"request": request}) 43 | 44 | res = await self._call(handler, request, *args, **kwargs) 45 | if isinstance(request, Command): 46 | self._logger.log( 47 | self._level, "Command %s sent. Result: %s", type(request).__name__, res, extra={"result": res}, 48 | ) 49 | elif isinstance(request, Query): 50 | self._logger.log( 51 | self._level, "Query %s made. Result: %s", type(request).__name__, res, extra={"result": res}, 52 | ) 53 | elif isinstance(request, Event): 54 | self._logger.log(self._level, "Event %s published", type(request).__name__, extra={"event": request}) 55 | else: 56 | self._logger.log( 57 | self._level, "Request %s executed. Result: %s", type(request).__name__, res, extra={"result": res}, 58 | ) 59 | 60 | return res 61 | -------------------------------------------------------------------------------- /didiator/utils/di_builder.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Mapping 2 | from typing import Any, ContextManager, TypeVar 3 | 4 | from di import Container, ScopeState, SolvedDependent 5 | from di._container import BindHook 6 | from di._utils.types import FusedContextManager 7 | from di.api.executor import SupportsAsyncExecutor 8 | from di.api.providers import DependencyProvider, DependencyProviderType 9 | from di.api.scopes import Scope 10 | from di.dependent import Dependent 11 | 12 | from didiator.interface.utils.di_builder import DiBuilder 13 | 14 | DependencyType = TypeVar("DependencyType") 15 | 16 | 17 | class DiBuilderImpl(DiBuilder): 18 | def __init__( 19 | self, di_container: Container, di_executor: SupportsAsyncExecutor, di_scopes: list[Scope] | None = None, 20 | *, solved_dependencies: dict[Scope, dict[DependencyProviderType[Any], SolvedDependent[Any]]] | None = None, 21 | ) -> None: 22 | self._di_container = di_container 23 | self._di_executor = di_executor 24 | if di_scopes is None: 25 | di_scopes = [] 26 | self.di_scopes = di_scopes 27 | 28 | if solved_dependencies is None: 29 | solved_dependencies = {} 30 | self._solved_dependencies = solved_dependencies 31 | 32 | def bind(self, hook: BindHook) -> ContextManager[None]: 33 | return self._di_container.bind(hook) 34 | 35 | def enter_scope(self, scope: Scope, state: ScopeState | None = None) -> FusedContextManager[ScopeState]: 36 | return self._di_container.enter_scope(scope, state) 37 | 38 | async def execute( 39 | self, call: DependencyProviderType[DependencyType], scope: Scope, 40 | *, state: ScopeState, values: Mapping[DependencyProvider, Any] | None = None, 41 | ) -> DependencyType: 42 | solved_dependency = self.solve(call, scope) 43 | return await solved_dependency.execute_async(executor=self._di_executor, state=state, values=values) 44 | 45 | def solve(self, call: DependencyProviderType[DependencyType], scope: Scope) -> SolvedDependent[DependencyType]: 46 | solved_scope_dependencies = self._solved_dependencies.setdefault(scope, {}) 47 | try: 48 | solved_dependency = solved_scope_dependencies[call] 49 | except KeyError: 50 | solved_dependency = self._di_container.solve(Dependent(call, scope=scope), scopes=self.di_scopes) 51 | solved_scope_dependencies[call] = solved_dependency 52 | return solved_dependency 53 | 54 | def copy(self) -> "DiBuilderImpl": 55 | di_container = Container() 56 | di_container._bind_hooks = self._di_container._bind_hooks.copy() # noqa 57 | return DiBuilderImpl( 58 | di_container, self._di_executor, self.di_scopes, solved_dependencies=self._solved_dependencies, 59 | ) 60 | -------------------------------------------------------------------------------- /tests/unit/test_middlewares.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from functools import partial 3 | 4 | from didiator.interface.handlers import CommandHandler 5 | from didiator.interface.entities.command import Command 6 | from didiator.middlewares.base import Middleware 7 | from tests.mocks.middlewares import DataAdderMiddlewareMock, DataRemoverMiddlewareMock 8 | 9 | 10 | @dataclass 11 | class CommandMock(Command[bool]): 12 | pass 13 | 14 | 15 | class HandlerMock(CommandHandler[CommandMock, bool]): 16 | def __init__(self, *args, **kwargs): 17 | self._excluded_args = args 18 | self._expected_kwargs = kwargs 19 | 20 | async def __call__(self, command: CommandMock, *args, **kwargs) -> bool: 21 | assert not set(self._excluded_args) & set(kwargs) 22 | 23 | for key, val in self._expected_kwargs.items(): 24 | assert kwargs[key] == val 25 | 26 | return True 27 | 28 | 29 | async def mock_handle_command(command: CommandMock, *args, **kwargs) -> bool: 30 | assert "middleware_data" not in kwargs 31 | assert kwargs["additional_data"] == "data" 32 | return True 33 | 34 | 35 | class TestBaseMiddleware: 36 | def test_init(self) -> None: 37 | middleware = Middleware() 38 | 39 | assert isinstance(middleware, Middleware) 40 | 41 | async def test_handling_by_middleware(self) -> None: 42 | middleware = Middleware() 43 | 44 | assert await middleware(HandlerMock, CommandMock()) is True 45 | 46 | async def test_handling_by_two_middlewares(self) -> None: 47 | middleware = Middleware() 48 | middleware2 = Middleware() 49 | 50 | assert await middleware(partial(middleware2, HandlerMock), CommandMock()) is True 51 | 52 | async def test_handling_by_middlewares_and_initialized_command_handler(self) -> None: 53 | middleware = Middleware() 54 | middleware2 = Middleware() 55 | 56 | assert await middleware(partial(middleware2, HandlerMock()), CommandMock()) is True 57 | 58 | async def test_handling_by_middleware_with_params(self) -> None: 59 | middleware = DataAdderMiddlewareMock(additional_data="data") 60 | 61 | assert await middleware(HandlerMock(additional_data="data"), CommandMock()) is True 62 | 63 | async def test_handling_by_two_middlewares_with_params(self) -> None: 64 | middleware = DataAdderMiddlewareMock(additional_data="data", some_data="value") 65 | middleware2 = DataRemoverMiddlewareMock("middleware_data") 66 | 67 | assert await middleware( 68 | partial(middleware2, HandlerMock("middleware_data", additional_data="data", some_data="value")), 69 | CommandMock(), 70 | middleware_data="data", 71 | ) is True 72 | 73 | async def test_handling_with_middlewares_and_function_command_handler(self) -> None: 74 | middleware = DataAdderMiddlewareMock(additional_data="data") 75 | middleware2 = DataRemoverMiddlewareMock("middleware_data") 76 | 77 | assert await middleware(partial(middleware2, mock_handle_command), CommandMock(), middleware_data="data") is True 78 | -------------------------------------------------------------------------------- /tests/integration/test_di_library.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from typing import Annotated, Any, AsyncGenerator, Callable, Protocol 3 | 4 | import pytest 5 | from di.api.dependencies import DependentBase 6 | from di import bind_by_type, Container 7 | from di.dependent import Dependent, Marker 8 | from di.executors import AsyncExecutor 9 | 10 | # These tests are for understanding DI library, they're not for didiator 11 | 12 | 13 | class UnitOfWork(Protocol): 14 | async def commit(self) -> bool: 15 | ... 16 | 17 | 18 | class Repo(Protocol): 19 | pass 20 | 21 | 22 | class Handler: 23 | def __init__(self, uow: UnitOfWork, repo: Repo): 24 | print("Init handler") 25 | self._uow = uow 26 | self._repo = repo 27 | 28 | async def handle(self, value: bool) -> bool: 29 | print("Start handling") 30 | assert value 31 | assert await self._uow.commit() 32 | print("End handling") 33 | return True 34 | 35 | 36 | class Session: 37 | def __init__(self, uri: str): 38 | print("Init session") 39 | self.uri = uri 40 | 41 | 42 | class RepoImpl: 43 | def __init__(self, session: Session): 44 | print("Init repo") 45 | self._session = session 46 | 47 | 48 | def get_session_factory(uri: str) -> Callable[..., AsyncGenerator[Session, None]]: 49 | async def get_session() -> AsyncGenerator[Session, None]: 50 | print("Start session") 51 | yield Session(uri) 52 | print("Close session") 53 | return get_session 54 | 55 | 56 | class UnitOfWorkImpl: 57 | def __init__(self, session: Session): 58 | print("Init UoW") 59 | self._session = session 60 | 61 | async def commit(self) -> bool: 62 | print("Commit") 63 | return self._session.uri == "psql://uri" 64 | 65 | 66 | class Request(str): 67 | pass 68 | 69 | 70 | async def controller(request: Request, uow: UnitOfWork, repo: Repo) -> str: 71 | assert request == Request("val") 72 | print("Start controller") 73 | assert await uow.commit() 74 | print("End controller") 75 | return "data" 76 | 77 | 78 | @pytest.mark.asyncio 79 | async def test_class_initialization_with_deps() -> None: 80 | container = Container() 81 | executor = AsyncExecutor() 82 | 83 | container.bind(bind_by_type(Dependent(UnitOfWorkImpl, scope="request"), UnitOfWork)) 84 | container.bind(bind_by_type(Dependent(RepoImpl, scope="request"), Repo)) 85 | container.bind(bind_by_type(Dependent(get_session_factory("psql://uri"), scope="request"), Session)) 86 | 87 | solved = container.solve(Dependent(Handler, scope="request"), scopes=["request", "sessio"]) 88 | solved_func = container.solve(Dependent(partial(controller, Request("val"), uow=1), scope="request"), scopes=["request", "s"]) 89 | print("Before scope") 90 | async with container.enter_scope("sesson") as state: 91 | print("State1:", state) 92 | async with container.enter_scope("request", state) as di_state: 93 | print("Inside scope", di_state.stacks, state.stacks) 94 | handler = await solved.execute_async(executor=executor, state=di_state) 95 | print("After handler initialization") 96 | res = await handler.handle(True) 97 | controller_res = await solved_func.execute_async(executor=executor, state=di_state, values={lambda: Dependent(Request): Request("val")}) 98 | assert controller_res == "data" 99 | print("Controller res:", controller_res) 100 | 101 | print("Call handler") 102 | assert res 103 | -------------------------------------------------------------------------------- /examples/simple.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from dataclasses import dataclass 4 | from typing import Protocol 5 | 6 | from dishka import AsyncContainer, make_async_container, provide, Provider, Scope 7 | 8 | from didiator import Handler, Mediator, Request 9 | from didiator.ioc.dishka import DishkaIoc 10 | from didiator.mediator import MediatorImpl 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | # User entity 16 | @dataclass 17 | class User: 18 | id: int 19 | username: str 20 | 21 | 22 | class UserRepo(Protocol): 23 | async def add_user(self, user: User) -> None: 24 | ... 25 | 26 | async def get_user_by_id(self, user_id: int) -> User: 27 | ... 28 | 29 | async def commit(self) -> None: 30 | ... 31 | 32 | 33 | # Create user command and its handler 34 | @dataclass(frozen=True) 35 | class CreateUser(Request[int]): 36 | user_id: int 37 | username: str 38 | 39 | 40 | class CreateUserHandler(Handler[CreateUser, int]): 41 | def __init__(self, user_repo: UserRepo) -> None: 42 | self._user_repo = user_repo 43 | 44 | async def __call__(self, command: CreateUser) -> int: 45 | user = User(id=command.user_id, username=command.username) 46 | await self._user_repo.add_user(user) 47 | await self._user_repo.commit() 48 | return user.id 49 | 50 | 51 | # Get user query and its handler 52 | # @dataclass(frozen=True) 53 | # class GetUserById(Query[User]): 54 | # user_id: int 55 | # 56 | # 57 | # async def handle_get_user_by_id(query: GetUserById, user_repo: UserRepo) -> User: 58 | # user = await user_repo.get_user_by_id(query.user_id) 59 | # return user 60 | 61 | 62 | class UserRepoImpl(UserRepo): 63 | def __init__(self) -> None: 64 | self._db_mock: dict[int, User] = {} 65 | 66 | async def add_user(self, user: User) -> None: 67 | self._db_mock[user.id] = user 68 | 69 | async def get_user_by_id(self, user_id: int) -> User: 70 | if user_id not in self._db_mock: 71 | raise ValueError("User with given id doesn't exist") 72 | return self._db_mock[user_id] 73 | 74 | async def commit(self) -> None: 75 | ... 76 | 77 | 78 | def build_mediator(container: AsyncContainer) -> Mediator: 79 | mediator = MediatorImpl(ioc=DishkaIoc(container), middlewares=[]) 80 | 81 | mediator.register_request_handler(CreateUser, CreateUserHandler) 82 | # mediator.register_request_handler(GetUserById, handle_get_user_by_id) 83 | 84 | return mediator 85 | 86 | 87 | class MainProvider(Provider): 88 | # get_user_by_id = provide(GetUserByIdHandler, scope=Scope.REQUEST) 89 | create_user_handler = provide(CreateUserHandler, scope=Scope.REQUEST) 90 | 91 | @provide(scope=Scope.REQUEST) 92 | def user_repo(self) -> UserRepo: 93 | return UserRepoImpl() 94 | 95 | 96 | async def main() -> None: 97 | logging.basicConfig( 98 | level=logging.INFO, 99 | format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", 100 | ) 101 | # di_builder = setup_di_builder() 102 | container = make_async_container(MainProvider()) 103 | mediator = build_mediator(container) 104 | 105 | # It will call CreateUserHandler(UserRepoImpl()).__call__(command) 106 | # UserRepoImpl() created and injected automatically 107 | user_id = await mediator.send(CreateUser(1, "Jon")) 108 | logger.info(f"Created a user with id: {user_id}") 109 | 110 | # It will call handle_get_user_by_id(query, user_repo) 111 | # UserRepoImpl created earlier will be reused in this scope 112 | # user = await mediator.send(GetUserById(user_id)) 113 | # logger.info(f"User: {user}") 114 | 115 | 116 | if __name__ == "__main__": 117 | asyncio.run(main()) 118 | -------------------------------------------------------------------------------- /didiator/middlewares/di.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Mapping 2 | from dataclasses import dataclass 3 | from typing import Any, TypeVar 4 | 5 | from di import ScopeState 6 | from di.api.providers import DependencyProvider 7 | from di.api.scopes import Scope 8 | 9 | from didiator.interface.entities.request import Request 10 | from didiator.interface.handlers import HandlerType 11 | from didiator.interface.utils.di_builder import DiBuilder 12 | from didiator.middlewares import Middleware 13 | 14 | RRes = TypeVar("RRes") 15 | R = TypeVar("R", bound=Request[Any]) 16 | 17 | 18 | @dataclass(frozen=True) 19 | class DiScopes: 20 | cls_handler: Scope = ... 21 | func_handler: Scope = "mediator_request" 22 | 23 | def __post_init__(self) -> None: 24 | if self.cls_handler is ...: 25 | object.__setattr__(self, "cls_handler", self.func_handler) 26 | 27 | 28 | @dataclass(frozen=True) 29 | class DiKeys: 30 | state: str = "di_state" 31 | values: str = "di_values" 32 | builder: str = "di_builder" 33 | 34 | 35 | class DiMiddleware(Middleware): 36 | def __init__( 37 | self, di_builder: DiBuilder, 38 | *, scopes: DiScopes | None = None, di_keys: DiKeys | None = None, 39 | ) -> None: 40 | self._di_builder = di_builder 41 | 42 | if scopes is None: 43 | scopes = DiScopes() 44 | self._di_scopes = scopes 45 | self._register_di_scopes() 46 | 47 | if di_keys is None: 48 | di_keys = DiKeys() 49 | self._di_keys = di_keys 50 | 51 | def _register_di_scopes(self) -> None: 52 | if self._di_scopes.cls_handler not in self._di_builder.di_scopes: 53 | self._di_builder.di_scopes.append(self._di_scopes.cls_handler) 54 | if self._di_scopes.func_handler not in self._di_builder.di_scopes: 55 | self._di_builder.di_scopes.append(self._di_scopes.func_handler) 56 | 57 | async def _call( 58 | self, 59 | handler: HandlerType[R, RRes], 60 | request: R, 61 | *args: Any, 62 | **kwargs: Any, 63 | ) -> RRes: 64 | di_state: ScopeState | None = kwargs.pop(self._di_keys.state, None) 65 | di_values: Mapping[DependencyProvider, Any] = kwargs.pop(self._di_keys.values, {}) 66 | di_builder: DiBuilder = kwargs.pop(self._di_keys.builder, self._di_builder) 67 | 68 | if isinstance(handler, type): 69 | return await self._call_class_handler(handler, request, di_builder, di_state, di_values, *args, **kwargs) 70 | return await self._call_func_handler(handler, request, di_builder, di_state, di_values) 71 | 72 | async def _call_class_handler( 73 | self, handler: HandlerType[R, RRes], request: R, di_builder: DiBuilder, 74 | di_state: ScopeState | None, di_values: Mapping[DependencyProvider, Any], 75 | *args: Any, **kwargs: Any, 76 | ) -> RRes: 77 | async with di_builder.enter_scope(self._di_scopes.func_handler, di_state) as scoped_di_state: 78 | handler = await di_builder.execute( 79 | handler, self._di_scopes.cls_handler, state=scoped_di_state, values={ 80 | type(request): request, 81 | } | di_values, 82 | ) 83 | return await handler(request, *args, **kwargs) 84 | 85 | async def _call_func_handler( 86 | self, handler: HandlerType[R, RRes], request: R, di_builder: DiBuilder, 87 | di_state: ScopeState | None, di_values: Mapping[DependencyProvider, Any], 88 | ) -> RRes: 89 | async with di_builder.enter_scope(self._di_scopes.func_handler, di_state) as scoped_di_state: 90 | return await di_builder.execute( 91 | handler, self._di_scopes.func_handler, state=scoped_di_state, values={ 92 | type(request): request, 93 | } | di_values, 94 | ) 95 | -------------------------------------------------------------------------------- /tests/unit/test_query_dispatcher.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | import pytest 4 | 5 | from didiator.interface.exceptions import QueryHandlerNotFound 6 | from didiator.interface.entities.query import Query 7 | from didiator.interface.handlers import QueryHandler 8 | 9 | from didiator.dispatchers.query import QueryDispatcherImpl 10 | from tests.mocks.middlewares import DataAdderMiddlewareMock, DataRemoverMiddlewareMock 11 | 12 | 13 | @dataclass 14 | class GetUserQuery(Query[int]): 15 | user_id: int 16 | username: str 17 | 18 | 19 | @dataclass 20 | class CollectUserDataQuery(Query[str]): 21 | user_id: int 22 | username: str 23 | 24 | 25 | class NotQuery: 26 | pass 27 | 28 | 29 | class UserId(int): 30 | pass 31 | 32 | 33 | class GetUserHandler(QueryHandler[GetUserQuery, int]): 34 | async def __call__(self, query: GetUserQuery) -> int: 35 | return query.user_id 36 | 37 | 38 | class ExtendedGetUserHandler(QueryHandler[GetUserQuery, UserId]): 39 | async def __call__(self, query: GetUserQuery) -> UserId: 40 | return UserId(query.user_id) 41 | 42 | 43 | class CollectUserDataHandler(QueryHandler[CollectUserDataQuery, str]): 44 | async def __call__(self, query: CollectUserDataQuery, additional_data: str = "") -> str: 45 | return additional_data 46 | 47 | 48 | async def handle_update_user(query: CollectUserDataQuery, additional_data: str = "") -> str: 49 | return additional_data 50 | 51 | 52 | class TestQueryDispatcher: 53 | def test_init(self) -> None: 54 | query_dispatcher: QueryDispatcherImpl = QueryDispatcherImpl() 55 | 56 | assert isinstance(query_dispatcher, QueryDispatcherImpl) 57 | assert query_dispatcher.handlers == {} 58 | assert query_dispatcher.middlewares == () 59 | 60 | async def test_query_handler_registration(self, query_dispatcher: QueryDispatcherImpl) -> None: 61 | query_dispatcher.register_handler(GetUserQuery, GetUserHandler) 62 | assert query_dispatcher.handlers == {GetUserQuery: GetUserHandler} 63 | 64 | query_dispatcher.register_handler(CollectUserDataQuery, CollectUserDataHandler) 65 | assert query_dispatcher.handlers == {GetUserQuery: GetUserHandler, CollectUserDataQuery: CollectUserDataHandler} 66 | 67 | query_dispatcher.register_handler(GetUserQuery, ExtendedGetUserHandler) 68 | assert query_dispatcher.handlers == {GetUserQuery: ExtendedGetUserHandler, CollectUserDataQuery: CollectUserDataHandler} 69 | 70 | async def test_initialization_with_middlewares(self) -> None: 71 | middleware1 = DataAdderMiddlewareMock() 72 | query_dispatcher = QueryDispatcherImpl(middlewares=(middleware1,)) 73 | assert query_dispatcher.middlewares == (middleware1,) 74 | 75 | middleware2 = DataRemoverMiddlewareMock() 76 | query_dispatcher = QueryDispatcherImpl(middlewares=[middleware1, middleware2]) 77 | assert query_dispatcher.middlewares == (middleware1, middleware2) 78 | 79 | async def test_query_querying(self, query_dispatcher: QueryDispatcherImpl) -> None: 80 | query_dispatcher.register_handler(GetUserQuery, GetUserHandler) 81 | 82 | res = await query_dispatcher.query(GetUserQuery(1, "Jon")) 83 | assert res == 1 84 | 85 | async def test_querying_not_registered_query(self, query_dispatcher: QueryDispatcherImpl) -> None: 86 | query_dispatcher.register_handler(GetUserQuery, GetUserHandler) 87 | 88 | with pytest.raises(QueryHandlerNotFound): 89 | await query_dispatcher.query(CollectUserDataQuery(1, "Jon")) 90 | 91 | async def test_query_querying_with_middlewares(self) -> None: 92 | middleware1 = DataAdderMiddlewareMock(middleware_data="data", additional_data="value") 93 | middleware2 = DataRemoverMiddlewareMock("middleware_data") 94 | query_dispatcher = QueryDispatcherImpl(middlewares=[middleware1, middleware2]) 95 | 96 | query_dispatcher.register_handler(CollectUserDataQuery, CollectUserDataHandler) 97 | 98 | res = await query_dispatcher.query(CollectUserDataQuery(1, "Sam")) 99 | assert res == "value" 100 | 101 | async def test_query_querying_with_function_handler(self) -> None: 102 | query_dispatcher = QueryDispatcherImpl() 103 | query_dispatcher.register_handler(CollectUserDataQuery, handle_update_user) 104 | 105 | res = await query_dispatcher.query(CollectUserDataQuery(1, "Sam"), additional_data="value") 106 | assert res == "value" 107 | 108 | async def test_query_querying_with_middlewares_and_function_handler(self) -> None: 109 | middleware1 = DataAdderMiddlewareMock(middleware_data="data", additional_data="value") 110 | middleware2 = DataRemoverMiddlewareMock("middleware_data") 111 | query_dispatcher = QueryDispatcherImpl(middlewares=[middleware1, middleware2]) 112 | 113 | query_dispatcher.register_handler(CollectUserDataQuery, handle_update_user) 114 | 115 | res = await query_dispatcher.query(CollectUserDataQuery(1, "Sam")) 116 | assert res == "value" 117 | -------------------------------------------------------------------------------- /tests/unit/test_dispatcher.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | import pytest 4 | 5 | from didiator.interface.handlers import CommandHandler 6 | from didiator.interface.entities.command import Command 7 | 8 | from didiator.dispatchers.command import CommandDispatcherImpl 9 | from didiator.interface.exceptions import CommandHandlerNotFound 10 | from tests.mocks.middlewares import DataAdderMiddlewareMock, DataRemoverMiddlewareMock 11 | 12 | 13 | @dataclass 14 | class CreateUserCommand(Command[int]): 15 | user_id: int 16 | username: str 17 | 18 | 19 | @dataclass 20 | class UpdateUserCommand(Command[str]): 21 | user_id: int 22 | username: str 23 | 24 | 25 | class NotCommand: 26 | pass 27 | 28 | 29 | class UserId(int): 30 | pass 31 | 32 | 33 | class CreateUserHandler(CommandHandler[CreateUserCommand, int]): 34 | async def __call__(self, command: CreateUserCommand) -> int: 35 | return command.user_id 36 | 37 | 38 | class ExtendedCreateUserHandler(CommandHandler[CreateUserCommand, UserId]): 39 | async def __call__(self, command: CreateUserCommand) -> UserId: 40 | return UserId(command.user_id) 41 | 42 | 43 | class UpdateUserHandler(CommandHandler[UpdateUserCommand, str]): 44 | async def __call__(self, command: UpdateUserCommand, additional_data: str = "") -> str: 45 | return additional_data 46 | 47 | 48 | async def handle_update_user(command: UpdateUserCommand, additional_data: str = "") -> str: 49 | return additional_data 50 | 51 | 52 | class TestCommandDispatcher: 53 | def test_init(self) -> None: 54 | command_dispatcher: CommandDispatcherImpl = CommandDispatcherImpl() 55 | 56 | assert isinstance(command_dispatcher, CommandDispatcherImpl) 57 | assert command_dispatcher.handlers == {} 58 | assert command_dispatcher.middlewares == () 59 | 60 | async def test_command_handler_registration(self, command_dispatcher: CommandDispatcherImpl) -> None: 61 | command_dispatcher.register_handler(CreateUserCommand, CreateUserHandler) 62 | assert command_dispatcher.handlers == {CreateUserCommand: CreateUserHandler} 63 | 64 | command_dispatcher.register_handler(UpdateUserCommand, UpdateUserHandler) 65 | assert command_dispatcher.handlers == {CreateUserCommand: CreateUserHandler, UpdateUserCommand: UpdateUserHandler} 66 | 67 | command_dispatcher.register_handler(CreateUserCommand, ExtendedCreateUserHandler) 68 | assert command_dispatcher.handlers == {CreateUserCommand: ExtendedCreateUserHandler, UpdateUserCommand: UpdateUserHandler} 69 | 70 | async def test_initialization_with_middlewares(self) -> None: 71 | middleware1 = DataAdderMiddlewareMock() 72 | command_dispatcher = CommandDispatcherImpl(middlewares=(middleware1,)) 73 | assert command_dispatcher.middlewares == (middleware1,) 74 | 75 | middleware2 = DataRemoverMiddlewareMock() 76 | command_dispatcher = CommandDispatcherImpl(middlewares=[middleware1, middleware2]) 77 | assert command_dispatcher.middlewares == (middleware1, middleware2) 78 | 79 | async def test_command_sending(self, command_dispatcher: CommandDispatcherImpl) -> None: 80 | command_dispatcher.register_handler(CreateUserCommand, CreateUserHandler) 81 | 82 | res = await command_dispatcher.send(CreateUserCommand(1, "Jon")) 83 | assert res == 1 84 | 85 | async def test_sending_not_registered_command(self, command_dispatcher: CommandDispatcherImpl) -> None: 86 | command_dispatcher.register_handler(CreateUserCommand, CreateUserHandler) 87 | 88 | with pytest.raises(CommandHandlerNotFound): 89 | await command_dispatcher.send(UpdateUserCommand(1, "Jon")) 90 | 91 | async def test_command_sending_with_middlewares(self) -> None: 92 | middleware1 = DataAdderMiddlewareMock(middleware_data="data", additional_data="value") 93 | middleware2 = DataRemoverMiddlewareMock("middleware_data") 94 | command_dispatcher = CommandDispatcherImpl(middlewares=[middleware1, middleware2]) 95 | 96 | command_dispatcher.register_handler(UpdateUserCommand, UpdateUserHandler) 97 | 98 | res = await command_dispatcher.send(UpdateUserCommand(1, "Sam")) 99 | assert res == "value" 100 | 101 | async def test_command_sending_with_function_handler(self) -> None: 102 | command_dispatcher = CommandDispatcherImpl() 103 | command_dispatcher.register_handler(UpdateUserCommand, handle_update_user) 104 | 105 | res = await command_dispatcher.send(UpdateUserCommand(1, "Sam"), additional_data="value") 106 | assert res == "value" 107 | 108 | async def test_command_sending_with_middlewares_and_function_handler(self) -> None: 109 | middleware1 = DataAdderMiddlewareMock(middleware_data="data", additional_data="value") 110 | middleware2 = DataRemoverMiddlewareMock("middleware_data") 111 | command_dispatcher = CommandDispatcherImpl(middlewares=[middleware1, middleware2]) 112 | 113 | command_dispatcher.register_handler(UpdateUserCommand, handle_update_user) 114 | 115 | res = await command_dispatcher.send(UpdateUserCommand(1, "Sam")) 116 | assert res == "value" 117 | -------------------------------------------------------------------------------- /tests/unit/test_mediator.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from didiator.interface.handlers import CommandHandler, QueryHandler 4 | from didiator.interface.entities.command import Command 5 | from didiator.dispatchers.command import CommandDispatcherImpl 6 | from didiator.interface.mediator import CommandMediator, Mediator, QueryMediator 7 | from didiator.mediator import MediatorImpl 8 | from didiator.interface.entities.query import Query 9 | from didiator.dispatchers.query import QueryDispatcherImpl 10 | from tests.mocks.middlewares import DataRemoverMiddlewareMock 11 | 12 | 13 | @dataclass 14 | class CommandMock(Command[str]): 15 | result: str 16 | 17 | 18 | @dataclass 19 | class QueryMock(Query[str]): 20 | result: str 21 | 22 | 23 | class CommandHandlerMock(CommandHandler[CommandMock, str]): 24 | def __init__(self, *args, **kwargs): 25 | self._excluded_args = args 26 | self._expected_kwargs = kwargs 27 | 28 | async def __call__(self, command: CommandMock, *args, **kwargs) -> str: 29 | assert not set(self._excluded_args) & set(kwargs) 30 | 31 | for key, val in self._expected_kwargs.items(): 32 | assert kwargs[key] == val 33 | 34 | return command.result 35 | 36 | 37 | class QueryHandlerMock(QueryHandler[QueryMock, str]): 38 | def __init__(self, *args, **kwargs): 39 | self._excluded_args = args 40 | self._expected_kwargs = kwargs 41 | 42 | async def __call__(self, query: QueryMock, *args, **kwargs) -> str: 43 | assert not set(self._excluded_args) & set(kwargs) 44 | 45 | for key, val in self._expected_kwargs.items(): 46 | assert kwargs[key] == val 47 | 48 | return query.result 49 | 50 | 51 | class TestMediator: 52 | def test_init(self) -> None: 53 | mediator: Mediator = MediatorImpl() 54 | 55 | assert isinstance(mediator, MediatorImpl) 56 | assert isinstance(MediatorImpl(CommandDispatcherImpl(), QueryDispatcherImpl()), MediatorImpl) 57 | assert isinstance(MediatorImpl(CommandDispatcherImpl()), MediatorImpl) 58 | assert isinstance(MediatorImpl( 59 | command_dispatcher=CommandDispatcherImpl(), query_dispatcher=QueryDispatcherImpl(), 60 | ), MediatorImpl) 61 | 62 | async def test_sending_command_through_mediator(self) -> None: 63 | command_dispatcher = CommandDispatcherImpl() 64 | command_dispatcher.register_handler(CommandMock, CommandHandlerMock) 65 | 66 | mediator: CommandMediator = MediatorImpl(command_dispatcher) 67 | 68 | assert await mediator.send(CommandMock("data")) == "data" 69 | 70 | async def test_query_queries_through_mediator(self) -> None: 71 | query_dispatcher = QueryDispatcherImpl() 72 | query_dispatcher.register_handler(QueryMock, CommandHandlerMock) 73 | 74 | mediator: QueryMediator = MediatorImpl(query_dispatcher=query_dispatcher) 75 | 76 | assert await mediator.query(QueryMock("data")) == "data" 77 | 78 | async def test_sending_command_through_mediator_with_extra_args(self) -> None: 79 | command_dispatcher = CommandDispatcherImpl((DataRemoverMiddlewareMock("middleware_data"),)) 80 | command_dispatcher.register_handler(CommandMock, CommandHandlerMock(additional_data="arg")) 81 | 82 | mediator = MediatorImpl(command_dispatcher) 83 | 84 | assert await mediator.send(CommandMock("data"), middleware_data="value", additional_data="arg") == "data" 85 | 86 | async def test_extra_data_binding(self) -> None: 87 | mediator_without_extra_data = MediatorImpl() 88 | assert mediator_without_extra_data.extra_data == {} 89 | 90 | mediator = MediatorImpl(extra_data={"additional_data": "arg"}) 91 | assert mediator.extra_data == {"additional_data": "arg"} 92 | 93 | mediator2 = mediator.bind(middleware_data="value", some_data=1) 94 | assert mediator.extra_data == {"additional_data": "arg"} 95 | assert mediator2.extra_data == {"additional_data": "arg", "middleware_data": "value", "some_data": 1} 96 | 97 | mediator3 = mediator2.unbind("some_data", "additional_data") 98 | assert mediator.extra_data == {"additional_data": "arg"} 99 | assert mediator2.extra_data == {"additional_data": "arg", "middleware_data": "value", "some_data": 1} 100 | assert mediator3.extra_data == {"middleware_data": "value"} 101 | 102 | mediator4 = mediator2.bind(another_data=None, some_data=2) 103 | assert mediator.extra_data == {"additional_data": "arg"} 104 | assert mediator2.extra_data == {"additional_data": "arg", "middleware_data": "value", "some_data": 1} 105 | assert mediator3.extra_data == {"middleware_data": "value"} 106 | assert mediator4.extra_data == { 107 | "additional_data": "arg", "middleware_data": "value", "some_data": 2, "another_data": None, 108 | } 109 | 110 | async def test_sending_command_through_mediator_with_extra_data(self) -> None: 111 | command_dispatcher = CommandDispatcherImpl() 112 | command_dispatcher.register_handler(CommandMock, CommandHandlerMock(additional_data="arg")) 113 | 114 | mediator = MediatorImpl(command_dispatcher, extra_data={"additional_data": "arg"}) 115 | mediator2 = mediator.bind(middleware_data="value") 116 | assert await mediator2.send(CommandMock("data")) == "data" 117 | -------------------------------------------------------------------------------- /tests/unit/test_di_middleware.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Protocol 3 | 4 | from di import bind_by_type, Container 5 | from di.dependent import Dependent 6 | from di.executors import AsyncExecutor 7 | 8 | from didiator import Command, CommandHandler, Mediator, Query, QueryDispatcherImpl, QueryHandler 9 | from didiator.dispatchers.command import CommandDispatcherImpl 10 | from didiator.mediator import MediatorImpl 11 | from didiator.middlewares.di import DiMiddleware, DiScopes 12 | from didiator.utils.di_builder import DiBuilderImpl 13 | 14 | 15 | @dataclass 16 | class User: 17 | user_id: int 18 | username: str 19 | 20 | 21 | class Session(Protocol): 22 | async def commit(self) -> None: 23 | ... 24 | 25 | 26 | class UserRepo(Protocol): 27 | async def add_user(self, user: User) -> None: 28 | ... 29 | 30 | async def update_user(self, user: User) -> None: 31 | ... 32 | 33 | async def get_user_by_id(self, user_id: int) -> User: 34 | ... 35 | 36 | 37 | class UnitOfWork(Protocol): 38 | user: UserRepo 39 | 40 | async def commit(self) -> None: 41 | ... 42 | 43 | 44 | @dataclass 45 | class GetUserById(Query[User]): 46 | user_id: int 47 | 48 | 49 | class GetUserByIdHandler(QueryHandler[GetUserById, User]): 50 | def __init__(self, uow: UnitOfWork): 51 | print("init get handler") 52 | 53 | self._uow = uow 54 | 55 | async def __call__(self, query: GetUserById) -> User: 56 | return await self._uow.user.get_user_by_id(query.user_id) 57 | 58 | 59 | @dataclass 60 | class CreateUser(Command[int]): 61 | user_id: int 62 | username: str 63 | 64 | 65 | class CreateUserHandler(CommandHandler[CreateUser, int]): 66 | def __init__(self, uow: UnitOfWork): 67 | print("init create handler") 68 | self._uow = uow 69 | 70 | async def __call__(self, command: CreateUser) -> int: 71 | user = User(command.user_id, command.username) 72 | await self._uow.user.add_user(user) 73 | return user.user_id 74 | 75 | 76 | @dataclass 77 | class UpdateUser(Command[bool]): 78 | user_id: int 79 | username: str 80 | 81 | 82 | async def handle_update_user(command: UpdateUser, uow: UnitOfWork) -> bool: 83 | print("Handling update user:", command, uow) 84 | user = User(command.user_id, command.username) 85 | await uow.user.update_user(user) 86 | return True 87 | 88 | 89 | class UnitOfWorkImpl: 90 | def __init__(self, session: Session, user_repo: UserRepo): 91 | self._session = session 92 | self.user = user_repo 93 | 94 | async def commit(self) -> None: 95 | await self._session.commit() 96 | 97 | 98 | class SessionMock: 99 | async def commit(self) -> None: 100 | """A standard commit method mock""" 101 | pass 102 | 103 | 104 | class UserRepoMock: 105 | def __init__(self, session: Session): 106 | self._session = session 107 | self._db_mock: dict[int, User] = {} 108 | 109 | async def add_user(self, user: User) -> None: 110 | self._db_mock[user.user_id] = user 111 | 112 | async def update_user(self, user: User) -> None: 113 | self._db_mock[user.user_id] = user 114 | 115 | async def get_user_by_id(self, user_id: int) -> User: 116 | if user_id not in self._db_mock: 117 | raise ValueError("User with given id doesn't exist") 118 | return self._db_mock[user_id] 119 | 120 | 121 | class UserController: 122 | def __init__(self, mediator: Mediator): 123 | self._mediator = mediator 124 | 125 | async def interact_with_user(self) -> None: 126 | assert await self._mediator.send(CreateUser(1, "Jon")) == 1 127 | assert await self._mediator.query(GetUserById(1)) == User(1, "Jon") 128 | assert await self._mediator.send(UpdateUser(1, "Nick")) is True 129 | assert await self._mediator.send(UpdateUser(1, "Sam")) is True 130 | assert await self._mediator.query(GetUserById(1)) == User(1, "Sam") 131 | 132 | 133 | class TestDiMiddleware: 134 | async def test_di_middleware_with_mediator(self) -> None: 135 | di_container = Container() 136 | di_executor = AsyncExecutor() 137 | 138 | di_container.bind(bind_by_type(Dependent(UnitOfWorkImpl, scope="mediator"), UnitOfWork)) 139 | di_container.bind(bind_by_type(Dependent(UserRepoMock, scope="mediator"), UserRepo)) 140 | di_container.bind(bind_by_type(Dependent(SessionMock, scope="mediator"), Session)) 141 | 142 | di_scopes = ["mediator", "mediator_request"] 143 | command_dispatcher = CommandDispatcherImpl(middlewares=( 144 | DiMiddleware(DiBuilderImpl(di_container, di_executor, di_scopes), scopes=DiScopes("mediator")), 145 | )) 146 | query_dispatcher = QueryDispatcherImpl(middlewares=( 147 | DiMiddleware(DiBuilderImpl(di_container, di_executor, di_scopes), scopes=DiScopes("mediator")), 148 | )) 149 | 150 | command_dispatcher.register_handler(CreateUser, CreateUserHandler) 151 | command_dispatcher.register_handler(UpdateUser, handle_update_user) 152 | query_dispatcher.register_handler(GetUserById, GetUserByIdHandler) 153 | 154 | mediator = MediatorImpl(command_dispatcher, query_dispatcher) 155 | async with di_container.enter_scope("mediator") as di_state: 156 | scoped_mediator = mediator.bind(di_state=di_state) 157 | 158 | user_controller = UserController(scoped_mediator) 159 | await user_controller.interact_with_user() 160 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Didiator 3 | ======== 4 | 5 | ``didiator`` is an asynchronous library that implements the Mediator pattern and 6 | uses the `DI `_ library to help you to inject dependencies to called handlers 7 | 8 | This library is inspired by the `MediatR `_ used in C#, 9 | follows CQRS principles and implements event publishing 10 | 11 | Installation 12 | ============ 13 | 14 | Didiator is available on pypi: https://pypi.org/project/didiator 15 | 16 | .. code-block:: bash 17 | 18 | pip install -U "didiator[di]" 19 | 20 | It will install ``didiator`` with its optional DI dependency that is necessary to use ``DiMiddleware`` and ``DiBuilderImpl`` 21 | 22 | Examples 23 | ======== 24 | 25 | You can find more examples in `this folder `_ 26 | 27 | Create Commands and Queries with handlers for them 28 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 29 | 30 | .. code-block:: python 31 | 32 | @dataclass 33 | class CreateUser(Command[int]): 34 | user_id: int 35 | username: str 36 | 37 | class CreateUserHandler(CommandHandler[CreateUser, int]): 38 | def __init__(self, user_repo: UserRepo) -> None: 39 | self._user_repo = user_repo 40 | 41 | async def __call__(self, command: CreateUser) -> int: 42 | user = User(id=command.user_id, username=command.username) 43 | await self._user_repo.add_user(user) 44 | await self._user_repo.commit() 45 | return user.id 46 | 47 | You can use functions as handlers 48 | 49 | .. code-block:: python 50 | 51 | @dataclass 52 | class GetUserById(Query[User]): 53 | user_id: int 54 | 55 | async def handle_get_user_by_id(query: GetUserById, user_repo: UserRepo) -> User: 56 | user = await user_repo.get_user_by_id(query.user_id) 57 | return user 58 | 59 | Create DiBuilder 60 | ~~~~~~~~~~~~~~~~ 61 | 62 | ``DiBuilderImpl`` is a facade for Container from DI with caching of `solving `_ 63 | 64 | ``di_scopes`` is a list with the order of `scopes `_ 65 | 66 | ``di_builder.bind(...)`` will `bind `_ ``UserRepoImpl`` type to ``UserRepo`` protocol 67 | 68 | .. code-block:: python 69 | 70 | di_scopes = ["request"] 71 | di_builder = DiBuilderImpl(Container(), AsyncExecutor(), di_scopes) 72 | di_builder.bind(bind_by_type(Dependent(UserRepoImpl, scope="request"), UserRepo)) 73 | 74 | Create Mediator 75 | ~~~~~~~~~~~~~~~ 76 | 77 | Create dispatchers with their middlewares and use them to initialize the ``MediatorImpl`` 78 | 79 | ``cls_scope`` is a scope that will be used to bind class Command/Query handlers initialized during request handling 80 | 81 | .. code-block:: python 82 | 83 | middlewares = (LoggingMiddleware(), DiMiddleware(di_builder, scopes=DiScopes("request"))) 84 | command_dispatcher = CommandDispatcherImpl(middlewares=middlewares) 85 | query_dispatcher = QueryDispatcherImpl(middlewares=middlewares) 86 | 87 | mediator = MediatorImpl(command_dispatcher, query_dispatcher) 88 | 89 | Register handlers 90 | ~~~~~~~~~~~~~~~~~ 91 | 92 | .. code-block:: python 93 | 94 | # CreateUserHandler is not initialized during registration 95 | mediator.register_command_handler(CreateUser, CreateUserHandler) 96 | mediator.register_query_handler(GetUserById, handle_get_user_by_id) 97 | 98 | Main usage 99 | ~~~~~~~~~~ 100 | 101 | Enter the ``"request"`` scope that was registered earlier and create a new Mediator with ``di_state`` bound 102 | 103 | Use ``mediator.send(...)`` for commands and ``mediator.query(...)`` for queries 104 | 105 | .. code-block:: python 106 | 107 | async with di_builder.enter_scope("request") as di_state: 108 | scoped_mediator = mediator.bind(di_state=di_state) 109 | 110 | # It will call CreateUserHandler(UserRepoImpl()).__call__(command) 111 | # UserRepoImpl() created and injected automatically 112 | user_id = await scoped_mediator.send(CreateUser(1, "Jon")) 113 | 114 | # It will call handle_get_user_by_id(query, user_repo) 115 | # UserRepoImpl created earlier will be reused in this scope 116 | user = await scoped_mediator.query(GetUserById(user_id)) 117 | print("User:", user) 118 | # Session of UserRepoImpl will be closed after exiting the "request" scope 119 | 120 | Events publishing 121 | ~~~~~~~~~~~~~~~~~ 122 | 123 | You can register and publish events using ``Mediator`` and its ``EventObserver``. 124 | Unlike dispatchers, ``EventObserver`` publishes events to multiple event handlers subscribed to it 125 | and doesn't return their result. 126 | All middlewares also work with ``EventObserver``, as in in the case with Dispatchers. 127 | 128 | Define event and its handlers 129 | ----------------------------- 130 | 131 | .. code-block:: python 132 | 133 | class UserCreated(Event): 134 | user_id: int 135 | username: str 136 | 137 | async def on_user_created1(event: UserCreated, logger: Logger) -> None: 138 | logger.info("User created1: id=%s, username=%s", event.user_id, event.username) 139 | 140 | async def on_user_created2(event: UserCreated, logger: Logger) -> None: 141 | logger.info("User created2: id=%s, username=%s", event.user_id, event.username) 142 | 143 | Create EventObserver and use it for Mediator 144 | -------------------------------------------- 145 | 146 | .. code-block:: python 147 | 148 | middlewares = (LoggingMiddleware(), DiMiddleware(di_builder, scopes=DiScopes("request"))) 149 | event_observer = EventObserver(middlewares=middlewares) 150 | 151 | mediator = MediatorImpl(command_dispatcher, query_dispatcher, event_observer) 152 | 153 | Register event handlers 154 | ----------------------- 155 | 156 | You can register multiple event handlers for one event 157 | 158 | .. code-block:: python 159 | 160 | mediator.register_event_handler(UserCreated, on_user_created1) 161 | mediator.register_event_handler(UserCreated, on_user_created2) 162 | 163 | Publish event 164 | ------------- 165 | 166 | Event handlers will be executed sequentially 167 | 168 | .. code-block:: python 169 | 170 | await mediator.publish(UserCreated(1, "Jon")) 171 | # User created1: id=1, username="Jon" 172 | # User created2: id=1, username="Jon" 173 | 174 | await mediator.publish([UserCreated(2, "Sam"), UserCreated(3, "Nick")]) 175 | # User created1: id=2, username="Sam" 176 | # User created2: id=2, username="Sam" 177 | # User created1: id=3, username="Nick" 178 | # User created2: id=3, username="Nick" 179 | 180 | ⚠️ **Attention: this is a beta version of** ``didiator`` **that depends on** ``DI``, **which is also in beta. Both of them can change their API!** 181 | 182 | CQRS 183 | ==== 184 | 185 | CQRS stands for "`Command Query Responsibility Segregation `_". 186 | Its idea about splitting the responsibility of commands (writing) and queries (reading) into different models. 187 | 188 | ``didiator`` have segregated ``.send(command)``, ``.query(query)`` and ``.publish(events)`` methods in its ``Mediator`` and 189 | assumes that you will separate its handlers. 190 | Use ``CommandMediator``, ``QueryMediator`` and ``EventMediator`` protocols to explicitly define which method you need in ``YourController`` 191 | 192 | .. code-block:: mermaid 193 | 194 | graph LR; 195 | YourController-- Query -->Mediator; 196 | YourController-- Command -->Mediator; 197 | Mediator-. Query .->QueryDispatcher-.->di2[DiMiddleware]-.->QueryHandler; 198 | Mediator-. Command .->CommandDispatcher-.->di1[DiMiddleware]-.->CommandHandler; 199 | CommandHandler-- Event -->Mediator; 200 | Mediator-. Event .->EventObserver-.->di3[DiMiddleware]-.->EventHandler1; 201 | EventObserver-.->di4[DiMiddleware]-.->EventHandler2; 202 | 203 | ``DiMiddleware`` initializes handlers and injects dependencies for them, you can just send a command with the data you need 204 | 205 | Why ``didiator``? 206 | ================= 207 | 208 | - Easy dependency injection to your business logic 209 | - Separating dependencies from your controllers. They can just parse external requests and interact with the ``Mediator`` 210 | - CQRS 211 | - Event publishing 212 | - Flexible configuration 213 | - Middlewares support 214 | 215 | Why not? 216 | ======== 217 | 218 | - You don't need it 219 | - Maybe too low coupling: navigation becomes more difficult 220 | - Didiator is in beta now 221 | - No support for synchronous handlers 222 | 223 | -------------------------------------------------------------------------------- /examples/aiohttp_with_sqlalchemy.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | from collections.abc import AsyncGenerator, Callable 4 | from dataclasses import dataclass 5 | from typing import Awaitable, Protocol 6 | import logging 7 | from uuid import UUID, uuid4 8 | 9 | from aiohttp import web 10 | from aiohttp.web_request import Request, StreamResponse 11 | from di import bind_by_type, Container, ScopeState 12 | from di.dependent import Dependent 13 | from di.executors import AsyncExecutor 14 | from sqlalchemy.exc import IntegrityError 15 | from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncEngine, AsyncSession, create_async_engine 16 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column 17 | 18 | from didiator import ( 19 | Command, CommandHandler, Event, EventObserverImpl, Mediator, Query, QueryDispatcherImpl, 20 | QueryHandler, 21 | ) 22 | from didiator.dispatchers.command import CommandDispatcherImpl 23 | from didiator.interface.handlers.event import EventHandler 24 | from didiator.interface.utils.di_builder import DiBuilder 25 | from didiator.mediator import MediatorImpl 26 | from didiator.middlewares.di import DiMiddleware, DiScopes 27 | from didiator.middlewares.logging import LoggingMiddleware 28 | from didiator.utils.di_builder import DiBuilderImpl 29 | 30 | logger = logging.getLogger(__name__) 31 | 32 | # This is an example of usage didiator with DI, aiogram, and SQLAlchemy. 33 | # To run this script, install them by running this command: 34 | # pip install -U aiohttp SQLAlchemy DI 35 | # and set the value of TG_BOT_TOKEN to your token 36 | 37 | 38 | # User entity 39 | @dataclass 40 | class User: 41 | id: UUID 42 | username: str 43 | 44 | 45 | class UserRepo(Protocol): 46 | def add_user(self, user: User) -> None: 47 | ... 48 | 49 | async def get_user_by_id(self, user_id: UUID) -> User: 50 | ... 51 | 52 | async def commit(self) -> None: 53 | ... 54 | 55 | async def rollback(self) -> None: 56 | ... 57 | 58 | 59 | class TgUpdate: 60 | update_id: UUID 61 | 62 | 63 | # User created event and its handler 64 | @dataclass(frozen=True) 65 | class UserCreated(Event): 66 | user_id: UUID 67 | username: str 68 | 69 | 70 | class UserCreatedHandler(EventHandler[UserCreated]): 71 | def __init__(self, update: TgUpdate): 72 | self._update = update 73 | 74 | async def __call__(self, event: UserCreated) -> None: 75 | logger.info("User registered") 76 | 77 | 78 | # Create user command and its handler 79 | @dataclass(frozen=True) 80 | class CreateUser(Command[int]): 81 | username: str 82 | 83 | 84 | class UserAlreadyExists(RuntimeError): 85 | pass 86 | 87 | 88 | class CreateUserHandler(CommandHandler[CreateUser, UUID]): 89 | def __init__(self, mediator: Mediator, user_repo: UserRepo): 90 | self._mediator = mediator 91 | self._user_repo = user_repo 92 | 93 | async def __call__(self, command: CreateUser) -> UUID: 94 | user = User(id=uuid4(), username=command.username) 95 | self._user_repo.add_user(user) 96 | await self._mediator.publish(UserCreated(user.id, user.username)) 97 | 98 | try: 99 | await self._user_repo.commit() 100 | except IntegrityError: 101 | await self._user_repo.rollback() 102 | raise UserAlreadyExists 103 | 104 | return user.id 105 | 106 | 107 | # Get user query and its handler 108 | @dataclass(frozen=True) 109 | class GetUserById(Query[User]): 110 | user_id: UUID 111 | 112 | 113 | class GetUserByIdHandler(QueryHandler[GetUserById, User]): 114 | def __init__(self, user_repo: UserRepo) -> None: 115 | self._user_repo = user_repo 116 | 117 | async def __call__(self, query) -> User: 118 | user = await self._user_repo.get_user_by_id(query.user_id) 119 | return user 120 | 121 | 122 | # SQLAlchemy declaration 123 | 124 | class BaseModel(DeclarativeBase): 125 | pass 126 | 127 | 128 | class UserModel(BaseModel): 129 | __tablename__ = "users" 130 | 131 | id: Mapped[UUID] = mapped_column(primary_key=True) 132 | username: Mapped[str] = mapped_column(unique=True) 133 | 134 | 135 | class UserRepoImpl(UserRepo): 136 | def __init__(self, session: AsyncSession): 137 | self._session = session 138 | 139 | def add_user(self, user: User) -> None: 140 | self._session.add(UserModel(id=user.id, username=user.username)) 141 | 142 | async def get_user_by_id(self, user_id: UUID) -> User: 143 | user_model = await self._session.get(UserModel, user_id) 144 | return User(user_model.id, user_model.username) 145 | 146 | async def commit(self) -> None: 147 | await self._session.commit() 148 | 149 | async def rollback(self) -> None: 150 | await self._session.rollback() 151 | 152 | 153 | @dataclass(frozen=True) 154 | class Config: 155 | db_uri: str 156 | 157 | 158 | def build_config() -> Config: 159 | return Config( 160 | db_uri="sqlite+aiosqlite://", 161 | ) 162 | 163 | 164 | async def build_sa_engine(config: Config) -> AsyncGenerator[AsyncEngine, None]: 165 | engine = create_async_engine(config.db_uri, future=True) 166 | async with engine.begin() as conn: 167 | await conn.run_sync(BaseModel.metadata.create_all) 168 | 169 | yield engine 170 | 171 | await engine.dispose() 172 | 173 | 174 | def build_sa_session_factory(engine: AsyncEngine) -> async_sessionmaker[AsyncSession]: 175 | return async_sessionmaker(bind=engine, expire_on_commit=False, autoflush=False) 176 | 177 | 178 | async def build_sa_session(session_factory: async_sessionmaker[AsyncSession]) -> AsyncGenerator[AsyncSession, None]: 179 | async with session_factory() as session: 180 | yield session 181 | 182 | 183 | def build_mediator(di_builder: DiBuilder) -> Mediator: 184 | middlewares = (LoggingMiddleware(level=logging.INFO), DiMiddleware(di_builder, scopes=DiScopes("request"))) 185 | command_dispatcher = CommandDispatcherImpl(middlewares=middlewares) 186 | query_dispatcher = QueryDispatcherImpl(middlewares=middlewares) 187 | event_observer = EventObserverImpl(middlewares=middlewares) 188 | 189 | mediator = MediatorImpl(command_dispatcher, query_dispatcher, event_observer) 190 | mediator.register_command_handler(CreateUser, CreateUserHandler) 191 | mediator.register_query_handler(GetUserById, GetUserByIdHandler) 192 | mediator.register_event_handler(UserCreated, UserCreatedHandler) 193 | 194 | return mediator 195 | 196 | 197 | def get_mediator_builder(mediator: Mediator): 198 | def _build_mediator(di_state: ScopeState) -> Mediator: 199 | return mediator.bind(di_state=di_state) 200 | return _build_mediator 201 | 202 | 203 | def setup_di_builder() -> DiBuilderImpl: 204 | di_container = Container() 205 | di_executor = AsyncExecutor() 206 | di_scopes = ["app", "request"] 207 | di_builder = DiBuilderImpl(di_container, di_executor, di_scopes=di_scopes) 208 | 209 | di_builder.bind(bind_by_type(Dependent(lambda *args: di_builder, scope="app"), DiBuilder)) 210 | di_builder.bind(bind_by_type(Dependent(build_config, scope="app"), Config)) 211 | di_builder.bind(bind_by_type(Dependent(build_sa_engine, scope="app"), AsyncEngine)) 212 | di_builder.bind(bind_by_type(Dependent(build_sa_session_factory, scope="app"), async_sessionmaker[AsyncSession])) 213 | di_builder.bind(bind_by_type(Dependent(build_sa_session, scope="request"), AsyncSession)) 214 | di_builder.bind(bind_by_type(Dependent(UserRepoImpl, scope="request"), UserRepo)) 215 | return di_builder 216 | 217 | 218 | Controller = Callable[..., Awaitable[StreamResponse]] 219 | 220 | 221 | @web.middleware 222 | class DiWebMiddleware: 223 | def __init__( 224 | self, di_builder: DiBuilder, di_state: ScopeState | None = None, 225 | ) -> None: 226 | self._di_builder = di_builder 227 | self._di_state = di_state 228 | 229 | async def __call__( 230 | self, 231 | request: Request, 232 | handler: Controller, 233 | ) -> Awaitable[StreamResponse]: 234 | async with self._di_builder.enter_scope("request", self._di_state) as di_state: 235 | return await self._di_builder.execute(handler, "request", state=di_state, values={ 236 | Request: request, ScopeState: di_state, 237 | }) 238 | 239 | 240 | async def create_user_handler(request: Request, mediator: Mediator) -> web.Response: 241 | logger.info("Request received: %s", request) 242 | data = await request.json() 243 | username = data.get("username") 244 | if not isinstance(username, str): 245 | return web.Response(status=400, text="Body has to contain username") 246 | 247 | try: 248 | # It will call CreateUserHandler(UserRepoImpl()).__call__(command) 249 | # UserRepoImpl() created and injected automatically 250 | user_id = await mediator.send(CreateUser(username)) 251 | except UserAlreadyExists: 252 | logger.info("User already exists") 253 | return web.Response(status=409, text="Username already exist") 254 | 255 | # It will call GetUserByIdHandler(user_repo).__call__(query) 256 | # UserRepoImpl created earlier will be reused in this scope 257 | user = await mediator.query(GetUserById(user_id)) 258 | return web.Response(status=201, text=json.dumps(({"user_id": str(user.id), "username": user.username}))) 259 | 260 | 261 | async def main() -> None: 262 | logging.basicConfig( 263 | level=logging.INFO, 264 | format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", 265 | ) 266 | di_builder = setup_di_builder() 267 | 268 | async with di_builder.enter_scope("app") as di_state: 269 | mediator = await di_builder.execute(build_mediator, "app", state=di_state) 270 | di_builder.bind(bind_by_type(Dependent(get_mediator_builder(mediator), scope="request"), Mediator)) 271 | 272 | app = web.Application(middlewares=(DiWebMiddleware(di_builder, di_state),)) 273 | app.add_routes([web.route("POST", "/users/", create_user_handler)]) 274 | 275 | await web._run_app(app, host="127.0.0.1", port=5000) # noqa 276 | 277 | 278 | if __name__ == "__main__": 279 | asyncio.run(main()) 280 | -------------------------------------------------------------------------------- /examples/aiogram_with_sqlalchemy.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | from collections.abc import AsyncGenerator, Callable 4 | from dataclasses import dataclass 5 | from typing import Any, Awaitable, Protocol 6 | import logging 7 | 8 | import aiogram 9 | import aiogram.filters 10 | import aiogram.types as tg 11 | from di import bind_by_type, Container, ScopeState 12 | from di.dependent import Dependent 13 | from di.executors import AsyncExecutor 14 | from sqlalchemy.exc import IntegrityError 15 | from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncEngine, AsyncSession, create_async_engine 16 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column 17 | 18 | from didiator import Command, CommandHandler, Event, EventObserverImpl, Mediator, Query, QueryDispatcherImpl 19 | from didiator.dispatchers.command import CommandDispatcherImpl 20 | from didiator.interface.handlers.event import EventHandler 21 | from didiator.interface.utils.di_builder import DiBuilder 22 | from didiator.mediator import MediatorImpl 23 | from didiator.middlewares.di import DiMiddleware, DiScopes 24 | from didiator.middlewares.logging import LoggingMiddleware 25 | from didiator.utils.di_builder import DiBuilderImpl 26 | 27 | logger = logging.getLogger(__name__) 28 | 29 | # This is an example of usage didiator with DI, aiogram, and SQLAlchemy. 30 | # To run this script, install them by running this command: 31 | # pip install -U --pre aiogram SQLAlchemy DI 32 | # and set the value of TG_BOT_TOKEN to your token 33 | 34 | TG_BOT_TOKEN = os.getenv("TG_BOT_TOKEN", "") 35 | 36 | # SQLAlchemy declaration 37 | 38 | 39 | # User entity 40 | @dataclass 41 | class User: 42 | id: int 43 | username: str 44 | 45 | 46 | class UserRepo(Protocol): 47 | def add_user(self, user: User) -> None: 48 | ... 49 | 50 | async def get_user_by_id(self, user_id: int) -> User: 51 | ... 52 | 53 | async def commit(self) -> None: 54 | ... 55 | 56 | async def rollback(self) -> None: 57 | ... 58 | 59 | 60 | class TgUpdate: 61 | update_id: int 62 | 63 | 64 | # User created event and its handler 65 | @dataclass(frozen=True) 66 | class UserCreated(Event): 67 | user_id: int 68 | username: str 69 | 70 | 71 | class UserCreatedHandler(EventHandler[UserCreated]): 72 | def __init__(self, bot: aiogram.Bot, update: TgUpdate): 73 | self._bot = bot 74 | self._update = update 75 | 76 | async def __call__(self, event: UserCreated) -> None: 77 | logger.info("User registered, %s", self._bot) 78 | await self._bot.send_message( 79 | event.user_id, f"{event.username}, you're registered by update: {self._update.update_id}") 80 | 81 | 82 | # Create user command and its handler 83 | @dataclass(frozen=True) 84 | class CreateUser(Command[int]): 85 | user_id: int 86 | username: str 87 | 88 | 89 | class UserAlreadyExists(RuntimeError): 90 | pass 91 | 92 | 93 | class CreateUserHandler(CommandHandler[CreateUser, int]): 94 | def __init__(self, mediator: Mediator, user_repo: UserRepo): 95 | self._mediator = mediator 96 | self._user_repo = user_repo 97 | 98 | async def __call__(self, command: CreateUser) -> int: 99 | user = User(id=command.user_id, username=command.username) 100 | self._user_repo.add_user(user) 101 | await self._mediator.publish(UserCreated(user.id, user.username)) 102 | 103 | try: 104 | await self._user_repo.commit() 105 | except IntegrityError: 106 | await self._user_repo.rollback() 107 | raise UserAlreadyExists 108 | 109 | return user.id 110 | 111 | 112 | # Get user query and its handler 113 | @dataclass(frozen=True) 114 | class GetUserById(Query[User]): 115 | user_id: int 116 | 117 | 118 | async def handle_get_user_by_id(query: GetUserById, user_repo: UserRepo) -> User: 119 | user = await user_repo.get_user_by_id(query.user_id) 120 | return user 121 | 122 | 123 | class BaseModel(DeclarativeBase): 124 | pass 125 | 126 | 127 | class UserModel(BaseModel): 128 | __tablename__ = "users" 129 | 130 | id: Mapped[int] = mapped_column(primary_key=True, autoincrement=False) 131 | username: Mapped[str] 132 | 133 | 134 | class UserRepoImpl(UserRepo): 135 | def __init__(self, session: AsyncSession): 136 | self._session = session 137 | 138 | def add_user(self, user: User) -> None: 139 | self._session.add(UserModel(id=user.id, username=user.username)) 140 | 141 | async def get_user_by_id(self, user_id: int) -> User: 142 | user_model = await self._session.get(UserModel, user_id) 143 | return User(user_model.id, user_model.username) 144 | 145 | async def commit(self) -> None: 146 | await self._session.commit() 147 | 148 | async def rollback(self) -> None: 149 | await self._session.rollback() 150 | 151 | 152 | @dataclass(frozen=True) 153 | class Config: 154 | db_uri: str 155 | bot_token: str 156 | 157 | 158 | def build_config() -> Config: 159 | return Config( 160 | db_uri="sqlite+aiosqlite://", 161 | bot_token=TG_BOT_TOKEN, 162 | ) 163 | 164 | 165 | async def build_sa_engine(config: Config) -> AsyncGenerator[AsyncEngine, None]: 166 | engine = create_async_engine(config.db_uri, future=True) 167 | async with engine.begin() as conn: 168 | await conn.run_sync(BaseModel.metadata.create_all) 169 | 170 | yield engine 171 | 172 | await engine.dispose() 173 | 174 | 175 | def build_sa_session_factory(engine: AsyncEngine) -> async_sessionmaker[AsyncSession]: 176 | return async_sessionmaker(bind=engine, expire_on_commit=False, autoflush=False) 177 | 178 | 179 | async def build_sa_session(session_factory: async_sessionmaker[AsyncSession]) -> AsyncGenerator[AsyncSession, None]: 180 | async with session_factory() as session: 181 | yield session 182 | 183 | 184 | def build_repo(session: AsyncSession) -> UserRepoImpl: 185 | return UserRepoImpl(session) 186 | 187 | 188 | def build_tg_bot(config: Config) -> aiogram.Bot: 189 | return aiogram.Bot(config.bot_token) 190 | 191 | 192 | def build_tg_dispatcher() -> aiogram.Dispatcher: 193 | return aiogram.Dispatcher() 194 | 195 | 196 | def build_mediator(di_builder: DiBuilder) -> Mediator: 197 | middlewares = (LoggingMiddleware(level=logging.INFO), DiMiddleware(di_builder, scopes=DiScopes("tg_update"))) 198 | command_dispatcher = CommandDispatcherImpl(middlewares=middlewares) 199 | query_dispatcher = QueryDispatcherImpl(middlewares=middlewares) 200 | event_observer = EventObserverImpl(middlewares=middlewares) 201 | 202 | mediator = MediatorImpl(command_dispatcher, query_dispatcher, event_observer) 203 | mediator.register_command_handler(CreateUser, CreateUserHandler) 204 | mediator.register_query_handler(GetUserById, handle_get_user_by_id) 205 | mediator.register_event_handler(UserCreated, UserCreatedHandler) 206 | 207 | return mediator 208 | 209 | 210 | def setup_di_builder() -> DiBuilderImpl: 211 | di_container = Container() 212 | di_executor = AsyncExecutor() 213 | di_scopes = ["app", "tg_update"] 214 | di_builder = DiBuilderImpl(di_container, di_executor, di_scopes=di_scopes) 215 | 216 | di_builder.bind(bind_by_type(Dependent(lambda *args: di_builder, scope="app"), DiBuilder)) 217 | di_builder.bind(bind_by_type(Dependent(build_config, scope="app"), Config)) 218 | di_builder.bind(bind_by_type(Dependent(build_tg_bot, scope="app"), aiogram.Bot)) 219 | di_builder.bind(bind_by_type(Dependent(build_sa_engine, scope="app"), AsyncEngine)) 220 | di_builder.bind(bind_by_type(Dependent(build_sa_session_factory, scope="app"), async_sessionmaker[AsyncSession])) 221 | di_builder.bind(bind_by_type(Dependent(build_sa_session, scope="tg_update"), AsyncSession)) 222 | di_builder.bind(bind_by_type(Dependent(build_repo, scope="tg_update"), UserRepo)) 223 | return di_builder 224 | 225 | 226 | class MediatorMiddleware(aiogram.BaseMiddleware): 227 | def __init__( 228 | self, mediator: Mediator, di_builder: DiBuilder, di_state: ScopeState | None = None, 229 | ) -> None: 230 | self._mediator = mediator 231 | self._di_builder = di_builder 232 | self._di_state = di_state 233 | 234 | async def __call__( 235 | self, 236 | handler: Callable[[tg.TelegramObject, dict[str, Any]], Awaitable[Any]], 237 | event: tg.TelegramObject, 238 | data: dict[str, Any], 239 | ) -> None: 240 | async with self._di_builder.enter_scope("tg_update", self._di_state) as di_state: 241 | copied_di_builder = self._di_builder.copy() 242 | di_values = {TgUpdate: event} 243 | mediator = self._mediator.bind(di_state=di_state, di_builder=copied_di_builder, di_values=di_values) 244 | di_values[Mediator] = mediator 245 | data["mediator"] = mediator 246 | result = await handler(event, data) 247 | del data["mediator"] 248 | return result 249 | 250 | 251 | async def echo_handler(message: tg.Message, mediator: Mediator) -> None: 252 | logger.info(f"Message received: message_id={message.message_id}, text={message.text}") 253 | try: 254 | # It will call CreateUserHandler(UserRepoImpl()).__call__(command) 255 | # UserRepoImpl() created and injected automatically 256 | await mediator.send(CreateUser(message.from_user.id, message.from_user.username)) 257 | except UserAlreadyExists: 258 | logger.info("User already exists") 259 | 260 | # It will call GetUserByIdHandler(user_repo).__call__(query) 261 | # UserRepoImpl created earlier will be reused in this scope 262 | user = await mediator.query(GetUserById(message.from_user.id)) 263 | await message.answer(f"Hello, {user=}") 264 | logger.info("Message sent") 265 | 266 | 267 | async def main() -> None: 268 | logging.basicConfig( 269 | level=logging.INFO, 270 | format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", 271 | ) 272 | di_builder = setup_di_builder() 273 | 274 | async with di_builder.enter_scope("app") as di_state: 275 | mediator = await di_builder.execute(build_mediator, "app", state=di_state) 276 | 277 | dp = await di_builder.execute(aiogram.Dispatcher, "app", state=di_state) 278 | dp.update.outer_middleware(MediatorMiddleware(mediator, di_builder, di_state)) 279 | dp.message.register(echo_handler) 280 | 281 | bot = await di_builder.execute(aiogram.Bot, "app", state=di_state) 282 | await dp.start_polling(bot) 283 | 284 | 285 | if __name__ == "__main__": 286 | asyncio.run(main()) 287 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MAIN] 2 | 3 | # Analyse import fallback blocks. This can be used to support both Python 2 and 4 | # 3 compatible code, which means that the block might have code that exists 5 | # only in one or another interpreter, leading to false positives when analysed. 6 | analyse-fallback-blocks=no 7 | 8 | # Load and enable all available extensions. Use --list-extensions to see a list 9 | # all available extensions. 10 | #enable-all-extensions= 11 | 12 | # In error mode, messages with a category besides ERROR or FATAL are 13 | # suppressed, and no reports are done by default. Error mode is compatible with 14 | # disabling specific errors. 15 | #errors-only= 16 | 17 | # Always return a 0 (non-error) status code, even if lint errors are found. 18 | # This is primarily useful in continuous integration scripts. 19 | #exit-zero= 20 | 21 | # A comma-separated list of package or module names from where C extensions may 22 | # be loaded. Extensions are loading into the active Python interpreter and may 23 | # run arbitrary code. 24 | extension-pkg-allow-list= 25 | 26 | # A comma-separated list of package or module names from where C extensions may 27 | # be loaded. Extensions are loading into the active Python interpreter and may 28 | # run arbitrary code. (This is an alternative name to extension-pkg-allow-list 29 | # for backward compatibility.) 30 | extension-pkg-whitelist= 31 | 32 | # Return non-zero exit code if any of these messages/categories are detected, 33 | # even if score is above --fail-under value. Syntax same as enable. Messages 34 | # specified are enabled, while categories only check already-enabled messages. 35 | fail-on= 36 | 37 | # Specify a score threshold under which the program will exit with error. 38 | fail-under=10 39 | 40 | # Interpret the stdin as a python script, whose filename needs to be passed as 41 | # the module_or_package argument. 42 | #from-stdin= 43 | 44 | # Files or directories to be skipped. They should be base names, not paths. 45 | ignore=CVS 46 | 47 | # Add files or directories matching the regular expressions patterns to the 48 | # ignore-list. The regex matches against paths and can be in Posix or Windows 49 | # format. Because '\' represents the directory delimiter on Windows systems, it 50 | # can't be used as an escape character. 51 | ignore-paths= 52 | 53 | # Files or directories matching the regular expression patterns are skipped. 54 | # The regex matches against base names, not paths. The default value ignores 55 | # Emacs file locks 56 | ignore-patterns=^\.# 57 | 58 | # List of module names for which member attributes should not be checked 59 | # (useful for modules/projects where namespaces are manipulated during runtime 60 | # and thus existing member attributes cannot be deduced by static analysis). It 61 | # supports qualified module names, as well as Unix pattern matching. 62 | ignored-modules= 63 | 64 | # Python code to execute, usually for sys.path manipulation such as 65 | # pygtk.require(). 66 | #init-hook= 67 | 68 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 69 | # number of processors available to use, and will cap the count on Windows to 70 | # avoid hangs. 71 | jobs=1 72 | 73 | # Control the amount of potential inferred values when inferring a single 74 | # object. This can help the performance when dealing with large functions or 75 | # complex, nested conditions. 76 | limit-inference-results=100 77 | 78 | # List of plugins (as comma separated values of python module names) to load, 79 | # usually to register additional checkers. 80 | load-plugins= 81 | 82 | # Pickle collected data for later comparisons. 83 | persistent=yes 84 | 85 | # Minimum Python version to use for version dependent checks. Will default to 86 | # the version used to run pylint. 87 | py-version=3.10 88 | 89 | # Discover python modules and packages in the file system subtree. 90 | recursive=no 91 | 92 | # When enabled, pylint would attempt to guess common misconfiguration and emit 93 | # user-friendly hints instead of false-positive error messages. 94 | suggestion-mode=yes 95 | 96 | # Allow loading of arbitrary C extensions. Extensions are imported into the 97 | # active Python interpreter and may run arbitrary code. 98 | unsafe-load-any-extension=no 99 | 100 | # In verbose mode, extra non-checker-related info will be displayed. 101 | #verbose= 102 | 103 | 104 | [BASIC] 105 | 106 | # Naming style matching correct argument names. 107 | argument-naming-style=snake_case 108 | 109 | # Regular expression matching correct argument names. Overrides argument- 110 | # naming-style. If left empty, argument names will be checked with the set 111 | # naming style. 112 | #argument-rgx= 113 | 114 | # Naming style matching correct attribute names. 115 | attr-naming-style=snake_case 116 | 117 | # Regular expression matching correct attribute names. Overrides attr-naming- 118 | # style. If left empty, attribute names will be checked with the set naming 119 | # style. 120 | #attr-rgx= 121 | 122 | # Bad variable names which should always be refused, separated by a comma. 123 | bad-names=foo, 124 | bar, 125 | baz, 126 | toto, 127 | tutu, 128 | tata 129 | 130 | # Bad variable names regexes, separated by a comma. If names match any regex, 131 | # they will always be refused 132 | bad-names-rgxs= 133 | 134 | # Naming style matching correct class attribute names. 135 | class-attribute-naming-style=any 136 | 137 | # Regular expression matching correct class attribute names. Overrides class- 138 | # attribute-naming-style. If left empty, class attribute names will be checked 139 | # with the set naming style. 140 | #class-attribute-rgx= 141 | 142 | # Naming style matching correct class constant names. 143 | class-const-naming-style=UPPER_CASE 144 | 145 | # Regular expression matching correct class constant names. Overrides class- 146 | # const-naming-style. If left empty, class constant names will be checked with 147 | # the set naming style. 148 | #class-const-rgx= 149 | 150 | # Naming style matching correct class names. 151 | class-naming-style=PascalCase 152 | 153 | # Regular expression matching correct class names. Overrides class-naming- 154 | # style. If left empty, class names will be checked with the set naming style. 155 | #class-rgx= 156 | 157 | # Naming style matching correct constant names. 158 | const-naming-style=UPPER_CASE 159 | 160 | # Regular expression matching correct constant names. Overrides const-naming- 161 | # style. If left empty, constant names will be checked with the set naming 162 | # style. 163 | #const-rgx= 164 | 165 | # Minimum line length for functions/classes that require docstrings, shorter 166 | # ones are exempt. 167 | docstring-min-length=-1 168 | 169 | # Naming style matching correct function names. 170 | function-naming-style=snake_case 171 | 172 | # Regular expression matching correct function names. Overrides function- 173 | # naming-style. If left empty, function names will be checked with the set 174 | # naming style. 175 | #function-rgx= 176 | 177 | # Good variable names which should always be accepted, separated by a comma. 178 | good-names=i, 179 | j, 180 | k, 181 | ex, 182 | Run, 183 | _ 184 | 185 | # Good variable names regexes, separated by a comma. If names match any regex, 186 | # they will always be accepted 187 | good-names-rgxs= 188 | 189 | # Include a hint for the correct naming format with invalid-name. 190 | include-naming-hint=no 191 | 192 | # Naming style matching correct inline iteration names. 193 | inlinevar-naming-style=any 194 | 195 | # Regular expression matching correct inline iteration names. Overrides 196 | # inlinevar-naming-style. If left empty, inline iteration names will be checked 197 | # with the set naming style. 198 | #inlinevar-rgx= 199 | 200 | # Naming style matching correct method names. 201 | method-naming-style=snake_case 202 | 203 | # Regular expression matching correct method names. Overrides method-naming- 204 | # style. If left empty, method names will be checked with the set naming style. 205 | #method-rgx= 206 | 207 | # Naming style matching correct module names. 208 | module-naming-style=snake_case 209 | 210 | # Regular expression matching correct module names. Overrides module-naming- 211 | # style. If left empty, module names will be checked with the set naming style. 212 | #module-rgx= 213 | 214 | # Colon-delimited sets of names that determine each other's naming style when 215 | # the name regexes allow several styles. 216 | name-group= 217 | 218 | # Regular expression which should only match function or class names that do 219 | # not require a docstring. 220 | no-docstring-rgx=^_ 221 | 222 | # List of decorators that produce properties, such as abc.abstractproperty. Add 223 | # to this list to register other decorators that produce valid properties. 224 | # These decorators are taken in consideration only for invalid-name. 225 | property-classes=abc.abstractproperty 226 | 227 | # Regular expression matching correct type variable names. If left empty, type 228 | # variable names will be checked with the set naming style. 229 | #typevar-rgx= 230 | 231 | # Naming style matching correct variable names. 232 | variable-naming-style=snake_case 233 | 234 | # Regular expression matching correct variable names. Overrides variable- 235 | # naming-style. If left empty, variable names will be checked with the set 236 | # naming style. 237 | #variable-rgx= 238 | 239 | 240 | [CLASSES] 241 | 242 | # Warn about protected attribute access inside special methods 243 | check-protected-access-in-special-methods=no 244 | 245 | # List of method names used to declare (i.e. assign) instance attributes. 246 | defining-attr-methods=__init__, 247 | __new__, 248 | setUp, 249 | __post_init__ 250 | 251 | # List of member names, which should be excluded from the protected access 252 | # warning. 253 | exclude-protected=_asdict, 254 | _fields, 255 | _replace, 256 | _source, 257 | _make 258 | 259 | # List of valid names for the first argument in a class method. 260 | valid-classmethod-first-arg=cls 261 | 262 | # List of valid names for the first argument in a metaclass class method. 263 | valid-metaclass-classmethod-first-arg=cls 264 | 265 | 266 | [DESIGN] 267 | 268 | # List of regular expressions of class ancestor names to ignore when counting 269 | # public methods (see R0903) 270 | exclude-too-few-public-methods= 271 | 272 | # List of qualified class names to ignore when counting class parents (see 273 | # R0901) 274 | ignored-parents= 275 | 276 | # Maximum number of arguments for function / method. 277 | max-args=5 278 | 279 | # Maximum number of attributes for a class (see R0902). 280 | max-attributes=7 281 | 282 | # Maximum number of boolean expressions in an if statement (see R0916). 283 | max-bool-expr=5 284 | 285 | # Maximum number of branch for function / method body. 286 | max-branches=12 287 | 288 | # Maximum number of locals for function / method body. 289 | max-locals=15 290 | 291 | # Maximum number of parents for a class (see R0901). 292 | max-parents=7 293 | 294 | # Maximum number of public methods for a class (see R0904). 295 | max-public-methods=20 296 | 297 | # Maximum number of return / yield for function / method body. 298 | max-returns=6 299 | 300 | # Maximum number of statements in function / method body. 301 | max-statements=50 302 | 303 | # Minimum number of public methods for a class (see R0903). 304 | min-public-methods=2 305 | 306 | 307 | [EXCEPTIONS] 308 | 309 | # Exceptions that will emit a warning when caught. 310 | overgeneral-exceptions=BaseException, 311 | Exception 312 | 313 | 314 | [FORMAT] 315 | 316 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 317 | expected-line-ending-format= 318 | 319 | # Regexp for a line that is allowed to be longer than the limit. 320 | ignore-long-lines=^\s*(# )??$ 321 | 322 | # Number of spaces of indent required inside a hanging or continued line. 323 | indent-after-paren=4 324 | 325 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 326 | # tab). 327 | indent-string=' ' 328 | 329 | # Maximum number of characters on a single line. 330 | max-line-length=120 331 | 332 | # Maximum number of lines in a module. 333 | max-module-lines=1000 334 | 335 | # Allow the body of a class to be on the same line as the declaration if body 336 | # contains single statement. 337 | single-line-class-stmt=no 338 | 339 | # Allow the body of an if to be on the same line as the test if there is no 340 | # else. 341 | single-line-if-stmt=no 342 | 343 | 344 | [IMPORTS] 345 | 346 | # List of modules that can be imported at any level, not just the top level 347 | # one. 348 | allow-any-import-level= 349 | 350 | # Allow wildcard imports from modules that define __all__. 351 | allow-wildcard-with-all=no 352 | 353 | # Deprecated modules which should not be used, separated by a comma. 354 | deprecated-modules= 355 | 356 | # Output a graph (.gv or any supported image format) of external dependencies 357 | # to the given file (report RP0402 must not be disabled). 358 | ext-import-graph= 359 | 360 | # Output a graph (.gv or any supported image format) of all (i.e. internal and 361 | # external) dependencies to the given file (report RP0402 must not be 362 | # disabled). 363 | import-graph= 364 | 365 | # Output a graph (.gv or any supported image format) of internal dependencies 366 | # to the given file (report RP0402 must not be disabled). 367 | int-import-graph= 368 | 369 | # Force import order to recognize a module as part of the standard 370 | # compatibility libraries. 371 | known-standard-library= 372 | 373 | # Force import order to recognize a module as part of a third party library. 374 | known-third-party=enchant 375 | 376 | # Couples of modules and preferred modules, separated by a comma. 377 | preferred-modules= 378 | 379 | 380 | [LOGGING] 381 | 382 | # The type of string formatting that logging methods do. `old` means using % 383 | # formatting, `new` is for `{}` formatting. 384 | logging-format-style=old 385 | 386 | # Logging modules to check that the string format arguments are in logging 387 | # function parameter format. 388 | logging-modules=logging 389 | 390 | 391 | [MESSAGES CONTROL] 392 | 393 | # Only show warnings with the listed confidence levels. Leave empty to show 394 | # all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, 395 | # UNDEFINED. 396 | confidence=HIGH, 397 | CONTROL_FLOW, 398 | INFERENCE, 399 | INFERENCE_FAILURE, 400 | UNDEFINED 401 | 402 | # Disable the message, report, category or checker with the given id(s). You 403 | # can either give multiple identifiers separated by comma (,) or put this 404 | # option multiple times (only on the command line, not in the configuration 405 | # file where it should appear only once). You can also use "--disable=all" to 406 | # disable everything first and then re-enable specific checks. For example, if 407 | # you want to run only the similarities checker, you can use "--disable=all 408 | # --enable=similarities". If you want to run only the classes checker, but have 409 | # no Warning level messages displayed, use "--disable=all --enable=classes 410 | # --disable=W". 411 | disable=too-few-public-methods, 412 | missing-module-docstring, 413 | missing-class-docstring, 414 | missing-function-docstring 415 | 416 | 417 | # Enable the message, report, category or checker with the given id(s). You can 418 | # either give multiple identifier separated by comma (,) or put this option 419 | # multiple time (only on the command line, not in the configuration file where 420 | # it should appear only once). See also the "--disable" option for examples. 421 | enable=c-extension-no-member 422 | 423 | 424 | [METHOD_ARGS] 425 | 426 | # List of qualified names (i.e., library.method) which require a timeout 427 | # parameter e.g. 'requests.api.get,requests.api.post' 428 | timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request 429 | 430 | 431 | [MISCELLANEOUS] 432 | 433 | # List of note tags to take in consideration, separated by a comma. 434 | notes=FIXME, 435 | XXX, 436 | TODO 437 | 438 | # Regular expression of note tags to take in consideration. 439 | notes-rgx= 440 | 441 | 442 | [REFACTORING] 443 | 444 | # Maximum number of nested blocks for function / method body 445 | max-nested-blocks=5 446 | 447 | # Complete name of functions that never returns. When checking for 448 | # inconsistent-return-statements if a never returning function is called then 449 | # it will be considered as an explicit return statement and no message will be 450 | # printed. 451 | never-returning-functions=sys.exit,argparse.parse_error 452 | 453 | 454 | [REPORTS] 455 | 456 | # Python expression which should return a score less than or equal to 10. You 457 | # have access to the variables 'fatal', 'error', 'warning', 'refactor', 458 | # 'convention', and 'info' which contain the number of messages in each 459 | # category, as well as 'statement' which is the total number of statements 460 | # analyzed. This score is used by the global evaluation report (RP0004). 461 | evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) 462 | 463 | # Template used to display messages. This is a python new-style format string 464 | # used to format the message information. See doc for all details. 465 | msg-template= 466 | 467 | # Set the output format. Available formats are text, parseable, colorized, json 468 | # and msvs (visual studio). You can also give a reporter class, e.g. 469 | # mypackage.mymodule.MyReporterClass. 470 | #output-format= 471 | 472 | # Tells whether to display a full report or only the messages. 473 | reports=no 474 | 475 | # Activate the evaluation score. 476 | score=yes 477 | 478 | 479 | [SIMILARITIES] 480 | 481 | # Comments are removed from the similarity computation 482 | ignore-comments=yes 483 | 484 | # Docstrings are removed from the similarity computation 485 | ignore-docstrings=yes 486 | 487 | # Imports are removed from the similarity computation 488 | ignore-imports=yes 489 | 490 | # Signatures are removed from the similarity computation 491 | ignore-signatures=yes 492 | 493 | # Minimum lines number of a similarity. 494 | min-similarity-lines=4 495 | 496 | 497 | [SPELLING] 498 | 499 | # Limits count of emitted suggestions for spelling mistakes. 500 | max-spelling-suggestions=4 501 | 502 | # Spelling dictionary name. Available dictionaries: none. To make it work, 503 | # install the 'python-enchant' package. 504 | spelling-dict= 505 | 506 | # List of comma separated words that should be considered directives if they 507 | # appear at the beginning of a comment and should not be checked. 508 | spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: 509 | 510 | # List of comma separated words that should not be checked. 511 | spelling-ignore-words= 512 | 513 | # A path to a file that contains the private dictionary; one word per line. 514 | spelling-private-dict-file= 515 | 516 | # Tells whether to store unknown words to the private dictionary (see the 517 | # --spelling-private-dict-file option) instead of raising a message. 518 | spelling-store-unknown-words=no 519 | 520 | 521 | [STRING] 522 | 523 | # This flag controls whether inconsistent-quotes generates a warning when the 524 | # character used as a quote delimiter is used inconsistently within a module. 525 | check-quote-consistency=no 526 | 527 | # This flag controls whether the implicit-str-concat should generate a warning 528 | # on implicit string concatenation in sequences defined over several lines. 529 | check-str-concat-over-line-jumps=no 530 | 531 | 532 | [TYPECHECK] 533 | 534 | # List of decorators that produce context managers, such as 535 | # contextlib.contextmanager. Add to this list to register other decorators that 536 | # produce valid context managers. 537 | contextmanager-decorators=contextlib.contextmanager 538 | 539 | # List of members which are set dynamically and missed by pylint inference 540 | # system, and so shouldn't trigger E1101 when accessed. Python regular 541 | # expressions are accepted. 542 | generated-members= 543 | 544 | # Tells whether to warn about missing members when the owner of the attribute 545 | # is inferred to be None. 546 | ignore-none=yes 547 | 548 | # This flag controls whether pylint should warn about no-member and similar 549 | # checks whenever an opaque object is returned when inferring. The inference 550 | # can return multiple potential results while evaluating a Python object, but 551 | # some branches might not be evaluated, which results in partial inference. In 552 | # that case, it might be useful to still emit no-member and other checks for 553 | # the rest of the inferred objects. 554 | ignore-on-opaque-inference=yes 555 | 556 | # List of symbolic message names to ignore for Mixin members. 557 | ignored-checks-for-mixins=no-member, 558 | not-async-context-manager, 559 | not-context-manager, 560 | attribute-defined-outside-init 561 | 562 | # List of class names for which member attributes should not be checked (useful 563 | # for classes with dynamically set attributes). This supports the use of 564 | # qualified names. 565 | ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace 566 | 567 | # Show a hint with possible names when a member name was not found. The aspect 568 | # of finding the hint is based on edit distance. 569 | missing-member-hint=yes 570 | 571 | # The minimum edit distance a name should have in order to be considered a 572 | # similar match for a missing member name. 573 | missing-member-hint-distance=1 574 | 575 | # The total number of similar names that should be taken in consideration when 576 | # showing a hint for a missing member. 577 | missing-member-max-choices=1 578 | 579 | # Regex pattern to define which classes are considered mixins. 580 | mixin-class-rgx=.*[Mm]ixin 581 | 582 | # List of decorators that change the signature of a decorated function. 583 | signature-mutators= 584 | 585 | 586 | [VARIABLES] 587 | 588 | # List of additional names supposed to be defined in builtins. Remember that 589 | # you should avoid defining new builtins when possible. 590 | additional-builtins= 591 | 592 | # Tells whether unused global variables should be treated as a violation. 593 | allow-global-unused-variables=yes 594 | 595 | # List of names allowed to shadow builtins 596 | allowed-redefined-builtins= 597 | 598 | # List of strings which can identify a callback function by name. A callback 599 | # name must start or end with one of those strings. 600 | callbacks=cb_, 601 | _cb 602 | 603 | # A regular expression matching the name of dummy variables (i.e. expected to 604 | # not be used). 605 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 606 | 607 | # Argument names that match this expression will be ignored. 608 | ignored-argument-names=_.*|^ignored_|^unused_ 609 | 610 | # Tells whether we should check for unused import in __init__ files. 611 | init-import=no 612 | 613 | # List of qualified module names which can have objects that can redefine 614 | # builtins. 615 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 616 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "anyio" 5 | version = "4.6.0" 6 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 7 | optional = true 8 | python-versions = ">=3.9" 9 | files = [ 10 | {file = "anyio-4.6.0-py3-none-any.whl", hash = "sha256:c7d2e9d63e31599eeb636c8c5c03a7e108d73b345f064f1c19fdc87b79036a9a"}, 11 | {file = "anyio-4.6.0.tar.gz", hash = "sha256:137b4559cbb034c477165047febb6ff83f390fc3b20bf181c1fc0a728cb8beeb"}, 12 | ] 13 | 14 | [package.dependencies] 15 | idna = ">=2.8" 16 | sniffio = ">=1.1" 17 | 18 | [package.extras] 19 | doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 20 | test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.21.0b1)"] 21 | trio = ["trio (>=0.26.1)"] 22 | 23 | [[package]] 24 | name = "astroid" 25 | version = "2.15.8" 26 | description = "An abstract syntax tree for Python with inference support." 27 | optional = false 28 | python-versions = ">=3.7.2" 29 | files = [ 30 | {file = "astroid-2.15.8-py3-none-any.whl", hash = "sha256:1aa149fc5c6589e3d0ece885b4491acd80af4f087baafa3fb5203b113e68cd3c"}, 31 | {file = "astroid-2.15.8.tar.gz", hash = "sha256:6c107453dffee9055899705de3c9ead36e74119cee151e5a9aaf7f0b0e020a6a"}, 32 | ] 33 | 34 | [package.dependencies] 35 | lazy-object-proxy = ">=1.4.0" 36 | wrapt = {version = ">=1.14,<2", markers = "python_version >= \"3.11\""} 37 | 38 | [[package]] 39 | name = "colorama" 40 | version = "0.4.6" 41 | description = "Cross-platform colored terminal text." 42 | optional = false 43 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 44 | files = [ 45 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 46 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 47 | ] 48 | 49 | [[package]] 50 | name = "di" 51 | version = "0.79.2" 52 | description = "Dependency injection toolkit" 53 | optional = true 54 | python-versions = ">=3.8,<4" 55 | files = [ 56 | {file = "di-0.79.2-py3-none-any.whl", hash = "sha256:4b2ac7c46d4d9e941ca47d37c2029ba739c1f8a0e19e5288731224870f00d6e6"}, 57 | {file = "di-0.79.2.tar.gz", hash = "sha256:0c65b9ccb984252dadbdcdb39743eeddef0c1f167f791c59fcd70e97bb0d3af8"}, 58 | ] 59 | 60 | [package.dependencies] 61 | anyio = {version = ">=3.5.0", optional = true, markers = "extra == \"anyio\""} 62 | graphlib2 = ">=0.4.1,<0.5.0" 63 | 64 | [package.extras] 65 | anyio = ["anyio (>=3.5.0)"] 66 | 67 | [[package]] 68 | name = "dill" 69 | version = "0.3.8" 70 | description = "serialize all of Python" 71 | optional = false 72 | python-versions = ">=3.8" 73 | files = [ 74 | {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, 75 | {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, 76 | ] 77 | 78 | [package.extras] 79 | graph = ["objgraph (>=1.7.2)"] 80 | profile = ["gprof2dot (>=2022.7.29)"] 81 | 82 | [[package]] 83 | name = "dishka" 84 | version = "1.3.0" 85 | description = "Cute DI framework with scopes and agreeable API" 86 | optional = false 87 | python-versions = ">=3.10" 88 | files = [ 89 | {file = "dishka-1.3.0-py3-none-any.whl", hash = "sha256:37d534f9d4cb60df1f3ea19a193d60242a509433abb3d94dbefe62b38804ff7a"}, 90 | {file = "dishka-1.3.0.tar.gz", hash = "sha256:07c12cf87517f86d55e113701a4ceb816af4f46ae97929b395d92a0e4b9d6aec"}, 91 | ] 92 | 93 | [[package]] 94 | name = "flake8" 95 | version = "6.1.0" 96 | description = "the modular source code checker: pep8 pyflakes and co" 97 | optional = false 98 | python-versions = ">=3.8.1" 99 | files = [ 100 | {file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"}, 101 | {file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"}, 102 | ] 103 | 104 | [package.dependencies] 105 | mccabe = ">=0.7.0,<0.8.0" 106 | pycodestyle = ">=2.11.0,<2.12.0" 107 | pyflakes = ">=3.1.0,<3.2.0" 108 | 109 | [[package]] 110 | name = "graphlib2" 111 | version = "0.4.7" 112 | description = "Rust port of the Python stdlib graphlib modules" 113 | optional = true 114 | python-versions = ">=3.7" 115 | files = [ 116 | {file = "graphlib2-0.4.7-cp37-abi3-macosx_10_7_x86_64.whl", hash = "sha256:483710733215783cdc76452ccde1247af8f697685c9c1dfd9bb9ff4f52d990ee"}, 117 | {file = "graphlib2-0.4.7-cp37-abi3-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:3619c7d3c5aca95e6cbbfc283aa6bf42ffa5b59d7f39c8d0ad615bce65dc406f"}, 118 | {file = "graphlib2-0.4.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b19f1b91d0f22ca3d1cfb2965478db98cf5916a5c6cea5fdc7caf4bf1bfbc33"}, 119 | {file = "graphlib2-0.4.7-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:624020f6808ee21ffbb2e455f8dd4196bbb37032a35aa3327f0f5b65fb6a35d1"}, 120 | {file = "graphlib2-0.4.7-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6efc6a197a619a97f1b105aea14b202101241c1db9014bd100ad19cf29288cbf"}, 121 | {file = "graphlib2-0.4.7-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d7cc38b68775cb2cdfc487bbaca2f7991da0d76d42a68f412c2ca61461e6e026"}, 122 | {file = "graphlib2-0.4.7-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b06bed98d42f4e10adfe2a8332efdca06b5bac6e7c86dd1d22a4dea4de9b275a"}, 123 | {file = "graphlib2-0.4.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c9ec3a5645bdf020d8bd9196b2665e26090d60e523fd498df29628f2c5fbecc"}, 124 | {file = "graphlib2-0.4.7-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:824df87f767471febfd785a05a2cc77c0c973e0112a548df827763ca0aa8c126"}, 125 | {file = "graphlib2-0.4.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2de5e32ca5c0b06d442d2be4b378cc0bc335c5fcbc14a7d531a621eb8294d019"}, 126 | {file = "graphlib2-0.4.7-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:13a23fcf07c7bef8a5ad0e04ab826d3a2a2bcb493197005300c68b4ea7b8f581"}, 127 | {file = "graphlib2-0.4.7-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:15a8a6daa28c1fb5c518d387879f3bbe313264fbbc2fab5635b718bc71a24913"}, 128 | {file = "graphlib2-0.4.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:0cb6c4449834077972c3cea4602f86513b4b75fcf2d40b12e4fe4bf1aa5c8da2"}, 129 | {file = "graphlib2-0.4.7-cp37-abi3-win32.whl", hash = "sha256:31b40cea537845d80b69403ae306d7c6a68716b76f5171f68daed1804aadefec"}, 130 | {file = "graphlib2-0.4.7-cp37-abi3-win_amd64.whl", hash = "sha256:d40935a9da81a046ebcaa0216ad593ef504ae8a5425a59bdbd254c0462adedc8"}, 131 | {file = "graphlib2-0.4.7-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:9cef08a50632e75a9e11355e68fa1f8c9371d0734642855f8b5c4ead1b058e6f"}, 132 | {file = "graphlib2-0.4.7-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aeecb604d70317c20ca6bc3556f7f5c40146ad1f0ded837e978b2fe6edf3e567"}, 133 | {file = "graphlib2-0.4.7-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb4ae9df7ed895c6557619049c9f73e1c2e6d1fbed568010fd5d4af94e2f0692"}, 134 | {file = "graphlib2-0.4.7-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:3ee3a99fc39df948fef340b01254709cc603263f8b176f72ed26f1eea44070a4"}, 135 | {file = "graphlib2-0.4.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5873480df8991273bd1585122df232acd0f946c401c254bd9f0d661c72589dcf"}, 136 | {file = "graphlib2-0.4.7-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297c817229501255cd3a744c62c8f91e5139ee79bc550488f5bc765ffa33f7c5"}, 137 | {file = "graphlib2-0.4.7-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:853ef22df8e9f695706e0b8556cda9342d4d617f7d7bd02803e824bcc0c30b20"}, 138 | {file = "graphlib2-0.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee62ff1042fde980adf668e30393eca79aee8f1fa1274ab3b98d69091c70c5e8"}, 139 | {file = "graphlib2-0.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b16e21e70938132d4160c2591fed59f79b5f8b702e4860c8933111b5fedb55c2"}, 140 | {file = "graphlib2-0.4.7.tar.gz", hash = "sha256:a951c18cb4c2c2834eec898b4c75d3f930d6f08beb37496f0e0ce56eb3f571f5"}, 141 | ] 142 | 143 | [[package]] 144 | name = "idna" 145 | version = "3.10" 146 | description = "Internationalized Domain Names in Applications (IDNA)" 147 | optional = true 148 | python-versions = ">=3.6" 149 | files = [ 150 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 151 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 152 | ] 153 | 154 | [package.extras] 155 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 156 | 157 | [[package]] 158 | name = "iniconfig" 159 | version = "2.0.0" 160 | description = "brain-dead simple config-ini parsing" 161 | optional = false 162 | python-versions = ">=3.7" 163 | files = [ 164 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 165 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 166 | ] 167 | 168 | [[package]] 169 | name = "isort" 170 | version = "5.13.2" 171 | description = "A Python utility / library to sort Python imports." 172 | optional = false 173 | python-versions = ">=3.8.0" 174 | files = [ 175 | {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, 176 | {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, 177 | ] 178 | 179 | [package.extras] 180 | colors = ["colorama (>=0.4.6)"] 181 | 182 | [[package]] 183 | name = "lazy-object-proxy" 184 | version = "1.10.0" 185 | description = "A fast and thorough lazy object proxy." 186 | optional = false 187 | python-versions = ">=3.8" 188 | files = [ 189 | {file = "lazy-object-proxy-1.10.0.tar.gz", hash = "sha256:78247b6d45f43a52ef35c25b5581459e85117225408a4128a3daf8bf9648ac69"}, 190 | {file = "lazy_object_proxy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:855e068b0358ab916454464a884779c7ffa312b8925c6f7401e952dcf3b89977"}, 191 | {file = "lazy_object_proxy-1.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab7004cf2e59f7c2e4345604a3e6ea0d92ac44e1c2375527d56492014e690c3"}, 192 | {file = "lazy_object_proxy-1.10.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc0d2fc424e54c70c4bc06787e4072c4f3b1aa2f897dfdc34ce1013cf3ceef05"}, 193 | {file = "lazy_object_proxy-1.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e2adb09778797da09d2b5ebdbceebf7dd32e2c96f79da9052b2e87b6ea495895"}, 194 | {file = "lazy_object_proxy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b1f711e2c6dcd4edd372cf5dec5c5a30d23bba06ee012093267b3376c079ec83"}, 195 | {file = "lazy_object_proxy-1.10.0-cp310-cp310-win32.whl", hash = "sha256:76a095cfe6045c7d0ca77db9934e8f7b71b14645f0094ffcd842349ada5c5fb9"}, 196 | {file = "lazy_object_proxy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:b4f87d4ed9064b2628da63830986c3d2dca7501e6018347798313fcf028e2fd4"}, 197 | {file = "lazy_object_proxy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fec03caabbc6b59ea4a638bee5fce7117be8e99a4103d9d5ad77f15d6f81020c"}, 198 | {file = "lazy_object_proxy-1.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02c83f957782cbbe8136bee26416686a6ae998c7b6191711a04da776dc9e47d4"}, 199 | {file = "lazy_object_proxy-1.10.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009e6bb1f1935a62889ddc8541514b6a9e1fcf302667dcb049a0be5c8f613e56"}, 200 | {file = "lazy_object_proxy-1.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75fc59fc450050b1b3c203c35020bc41bd2695ed692a392924c6ce180c6f1dc9"}, 201 | {file = "lazy_object_proxy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:782e2c9b2aab1708ffb07d4bf377d12901d7a1d99e5e410d648d892f8967ab1f"}, 202 | {file = "lazy_object_proxy-1.10.0-cp311-cp311-win32.whl", hash = "sha256:edb45bb8278574710e68a6b021599a10ce730d156e5b254941754a9cc0b17d03"}, 203 | {file = "lazy_object_proxy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:e271058822765ad5e3bca7f05f2ace0de58a3f4e62045a8c90a0dfd2f8ad8cc6"}, 204 | {file = "lazy_object_proxy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e98c8af98d5707dcdecc9ab0863c0ea6e88545d42ca7c3feffb6b4d1e370c7ba"}, 205 | {file = "lazy_object_proxy-1.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:952c81d415b9b80ea261d2372d2a4a2332a3890c2b83e0535f263ddfe43f0d43"}, 206 | {file = "lazy_object_proxy-1.10.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80b39d3a151309efc8cc48675918891b865bdf742a8616a337cb0090791a0de9"}, 207 | {file = "lazy_object_proxy-1.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e221060b701e2aa2ea991542900dd13907a5c90fa80e199dbf5a03359019e7a3"}, 208 | {file = "lazy_object_proxy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:92f09ff65ecff3108e56526f9e2481b8116c0b9e1425325e13245abfd79bdb1b"}, 209 | {file = "lazy_object_proxy-1.10.0-cp312-cp312-win32.whl", hash = "sha256:3ad54b9ddbe20ae9f7c1b29e52f123120772b06dbb18ec6be9101369d63a4074"}, 210 | {file = "lazy_object_proxy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:127a789c75151db6af398b8972178afe6bda7d6f68730c057fbbc2e96b08d282"}, 211 | {file = "lazy_object_proxy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e4ed0518a14dd26092614412936920ad081a424bdcb54cc13349a8e2c6d106a"}, 212 | {file = "lazy_object_proxy-1.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ad9e6ed739285919aa9661a5bbed0aaf410aa60231373c5579c6b4801bd883c"}, 213 | {file = "lazy_object_proxy-1.10.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fc0a92c02fa1ca1e84fc60fa258458e5bf89d90a1ddaeb8ed9cc3147f417255"}, 214 | {file = "lazy_object_proxy-1.10.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0aefc7591920bbd360d57ea03c995cebc204b424524a5bd78406f6e1b8b2a5d8"}, 215 | {file = "lazy_object_proxy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5faf03a7d8942bb4476e3b62fd0f4cf94eaf4618e304a19865abf89a35c0bbee"}, 216 | {file = "lazy_object_proxy-1.10.0-cp38-cp38-win32.whl", hash = "sha256:e333e2324307a7b5d86adfa835bb500ee70bfcd1447384a822e96495796b0ca4"}, 217 | {file = "lazy_object_proxy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:cb73507defd385b7705c599a94474b1d5222a508e502553ef94114a143ec6696"}, 218 | {file = "lazy_object_proxy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:366c32fe5355ef5fc8a232c5436f4cc66e9d3e8967c01fb2e6302fd6627e3d94"}, 219 | {file = "lazy_object_proxy-1.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2297f08f08a2bb0d32a4265e98a006643cd7233fb7983032bd61ac7a02956b3b"}, 220 | {file = "lazy_object_proxy-1.10.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18dd842b49456aaa9a7cf535b04ca4571a302ff72ed8740d06b5adcd41fe0757"}, 221 | {file = "lazy_object_proxy-1.10.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:217138197c170a2a74ca0e05bddcd5f1796c735c37d0eee33e43259b192aa424"}, 222 | {file = "lazy_object_proxy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9a3a87cf1e133e5b1994144c12ca4aa3d9698517fe1e2ca82977781b16955658"}, 223 | {file = "lazy_object_proxy-1.10.0-cp39-cp39-win32.whl", hash = "sha256:30b339b2a743c5288405aa79a69e706a06e02958eab31859f7f3c04980853b70"}, 224 | {file = "lazy_object_proxy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:a899b10e17743683b293a729d3a11f2f399e8a90c73b089e29f5d0fe3509f0dd"}, 225 | {file = "lazy_object_proxy-1.10.0-pp310.pp311.pp312.pp38.pp39-none-any.whl", hash = "sha256:80fa48bd89c8f2f456fc0765c11c23bf5af827febacd2f523ca5bc1893fcc09d"}, 226 | ] 227 | 228 | [[package]] 229 | name = "mccabe" 230 | version = "0.7.0" 231 | description = "McCabe checker, plugin for flake8" 232 | optional = false 233 | python-versions = ">=3.6" 234 | files = [ 235 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 236 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 237 | ] 238 | 239 | [[package]] 240 | name = "mypy" 241 | version = "0.991" 242 | description = "Optional static typing for Python" 243 | optional = false 244 | python-versions = ">=3.7" 245 | files = [ 246 | {file = "mypy-0.991-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab"}, 247 | {file = "mypy-0.991-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d"}, 248 | {file = "mypy-0.991-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6"}, 249 | {file = "mypy-0.991-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb"}, 250 | {file = "mypy-0.991-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305"}, 251 | {file = "mypy-0.991-cp310-cp310-win_amd64.whl", hash = "sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c"}, 252 | {file = "mypy-0.991-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372"}, 253 | {file = "mypy-0.991-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f"}, 254 | {file = "mypy-0.991-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33"}, 255 | {file = "mypy-0.991-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05"}, 256 | {file = "mypy-0.991-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad"}, 257 | {file = "mypy-0.991-cp311-cp311-win_amd64.whl", hash = "sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297"}, 258 | {file = "mypy-0.991-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813"}, 259 | {file = "mypy-0.991-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711"}, 260 | {file = "mypy-0.991-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd"}, 261 | {file = "mypy-0.991-cp37-cp37m-win_amd64.whl", hash = "sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef"}, 262 | {file = "mypy-0.991-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a"}, 263 | {file = "mypy-0.991-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93"}, 264 | {file = "mypy-0.991-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf"}, 265 | {file = "mypy-0.991-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135"}, 266 | {file = "mypy-0.991-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70"}, 267 | {file = "mypy-0.991-cp38-cp38-win_amd64.whl", hash = "sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243"}, 268 | {file = "mypy-0.991-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d"}, 269 | {file = "mypy-0.991-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5"}, 270 | {file = "mypy-0.991-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3"}, 271 | {file = "mypy-0.991-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648"}, 272 | {file = "mypy-0.991-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476"}, 273 | {file = "mypy-0.991-cp39-cp39-win_amd64.whl", hash = "sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461"}, 274 | {file = "mypy-0.991-py3-none-any.whl", hash = "sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb"}, 275 | {file = "mypy-0.991.tar.gz", hash = "sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06"}, 276 | ] 277 | 278 | [package.dependencies] 279 | mypy-extensions = ">=0.4.3" 280 | typing-extensions = ">=3.10" 281 | 282 | [package.extras] 283 | dmypy = ["psutil (>=4.0)"] 284 | install-types = ["pip"] 285 | python2 = ["typed-ast (>=1.4.0,<2)"] 286 | reports = ["lxml"] 287 | 288 | [[package]] 289 | name = "mypy-extensions" 290 | version = "1.0.0" 291 | description = "Type system extensions for programs checked with the mypy type checker." 292 | optional = false 293 | python-versions = ">=3.5" 294 | files = [ 295 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 296 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 297 | ] 298 | 299 | [[package]] 300 | name = "packaging" 301 | version = "24.1" 302 | description = "Core utilities for Python packages" 303 | optional = false 304 | python-versions = ">=3.8" 305 | files = [ 306 | {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, 307 | {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, 308 | ] 309 | 310 | [[package]] 311 | name = "platformdirs" 312 | version = "4.3.6" 313 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 314 | optional = false 315 | python-versions = ">=3.8" 316 | files = [ 317 | {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, 318 | {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, 319 | ] 320 | 321 | [package.extras] 322 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] 323 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] 324 | type = ["mypy (>=1.11.2)"] 325 | 326 | [[package]] 327 | name = "pluggy" 328 | version = "1.5.0" 329 | description = "plugin and hook calling mechanisms for python" 330 | optional = false 331 | python-versions = ">=3.8" 332 | files = [ 333 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 334 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 335 | ] 336 | 337 | [package.extras] 338 | dev = ["pre-commit", "tox"] 339 | testing = ["pytest", "pytest-benchmark"] 340 | 341 | [[package]] 342 | name = "pycodestyle" 343 | version = "2.11.1" 344 | description = "Python style guide checker" 345 | optional = false 346 | python-versions = ">=3.8" 347 | files = [ 348 | {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, 349 | {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, 350 | ] 351 | 352 | [[package]] 353 | name = "pyflakes" 354 | version = "3.1.0" 355 | description = "passive checker of Python programs" 356 | optional = false 357 | python-versions = ">=3.8" 358 | files = [ 359 | {file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"}, 360 | {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, 361 | ] 362 | 363 | [[package]] 364 | name = "pylint" 365 | version = "2.17.7" 366 | description = "python code static checker" 367 | optional = false 368 | python-versions = ">=3.7.2" 369 | files = [ 370 | {file = "pylint-2.17.7-py3-none-any.whl", hash = "sha256:27a8d4c7ddc8c2f8c18aa0050148f89ffc09838142193fdbe98f172781a3ff87"}, 371 | {file = "pylint-2.17.7.tar.gz", hash = "sha256:f4fcac7ae74cfe36bc8451e931d8438e4a476c20314b1101c458ad0f05191fad"}, 372 | ] 373 | 374 | [package.dependencies] 375 | astroid = ">=2.15.8,<=2.17.0-dev0" 376 | colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} 377 | dill = {version = ">=0.3.6", markers = "python_version >= \"3.11\""} 378 | isort = ">=4.2.5,<6" 379 | mccabe = ">=0.6,<0.8" 380 | platformdirs = ">=2.2.0" 381 | tomlkit = ">=0.10.1" 382 | 383 | [package.extras] 384 | spelling = ["pyenchant (>=3.2,<4.0)"] 385 | testutils = ["gitpython (>3)"] 386 | 387 | [[package]] 388 | name = "pytest" 389 | version = "7.4.4" 390 | description = "pytest: simple powerful testing with Python" 391 | optional = false 392 | python-versions = ">=3.7" 393 | files = [ 394 | {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, 395 | {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, 396 | ] 397 | 398 | [package.dependencies] 399 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 400 | iniconfig = "*" 401 | packaging = "*" 402 | pluggy = ">=0.12,<2.0" 403 | 404 | [package.extras] 405 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 406 | 407 | [[package]] 408 | name = "pytest-asyncio" 409 | version = "0.20.3" 410 | description = "Pytest support for asyncio" 411 | optional = false 412 | python-versions = ">=3.7" 413 | files = [ 414 | {file = "pytest-asyncio-0.20.3.tar.gz", hash = "sha256:83cbf01169ce3e8eb71c6c278ccb0574d1a7a3bb8eaaf5e50e0ad342afb33b36"}, 415 | {file = "pytest_asyncio-0.20.3-py3-none-any.whl", hash = "sha256:f129998b209d04fcc65c96fc85c11e5316738358909a8399e93be553d7656442"}, 416 | ] 417 | 418 | [package.dependencies] 419 | pytest = ">=6.1.0" 420 | 421 | [package.extras] 422 | docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] 423 | testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] 424 | 425 | [[package]] 426 | name = "sniffio" 427 | version = "1.3.1" 428 | description = "Sniff out which async library your code is running under" 429 | optional = true 430 | python-versions = ">=3.7" 431 | files = [ 432 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 433 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 434 | ] 435 | 436 | [[package]] 437 | name = "tomlkit" 438 | version = "0.13.2" 439 | description = "Style preserving TOML library" 440 | optional = false 441 | python-versions = ">=3.8" 442 | files = [ 443 | {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, 444 | {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, 445 | ] 446 | 447 | [[package]] 448 | name = "typing-extensions" 449 | version = "4.12.2" 450 | description = "Backported and Experimental Type Hints for Python 3.8+" 451 | optional = false 452 | python-versions = ">=3.8" 453 | files = [ 454 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 455 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 456 | ] 457 | 458 | [[package]] 459 | name = "wrapt" 460 | version = "1.16.0" 461 | description = "Module for decorators, wrappers and monkey patching." 462 | optional = false 463 | python-versions = ">=3.6" 464 | files = [ 465 | {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, 466 | {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, 467 | {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, 468 | {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, 469 | {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, 470 | {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, 471 | {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, 472 | {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, 473 | {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, 474 | {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, 475 | {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, 476 | {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, 477 | {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, 478 | {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, 479 | {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, 480 | {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, 481 | {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, 482 | {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, 483 | {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, 484 | {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, 485 | {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, 486 | {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, 487 | {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, 488 | {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, 489 | {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, 490 | {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, 491 | {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, 492 | {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, 493 | {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, 494 | {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, 495 | {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, 496 | {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, 497 | {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, 498 | {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, 499 | {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, 500 | {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, 501 | {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, 502 | {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, 503 | {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, 504 | {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, 505 | {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, 506 | {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, 507 | {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, 508 | {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, 509 | {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, 510 | {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, 511 | {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, 512 | {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, 513 | {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, 514 | {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, 515 | {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, 516 | {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, 517 | {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, 518 | {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, 519 | {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, 520 | {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, 521 | {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, 522 | {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, 523 | {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, 524 | {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, 525 | {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, 526 | {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, 527 | {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, 528 | {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, 529 | {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, 530 | {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, 531 | {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, 532 | {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, 533 | {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, 534 | {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, 535 | ] 536 | 537 | [extras] 538 | di = ["di"] 539 | dishka = [] 540 | 541 | [metadata] 542 | lock-version = "2.0" 543 | python-versions = "^3.11,<4" 544 | content-hash = "b003dea8560a3684ce04b6abc20352d6bbe959478ba31fbff2c62ebe31047d95" 545 | --------------------------------------------------------------------------------