explicit-python 2.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,100 @@
1
+ """Шина сообщений: обработка команд на обновление данных."""
2
+ from abc import ABC
3
+ from typing import Any
4
+ from typing import Dict
5
+ from typing import Protocol
6
+ from typing import Tuple
7
+ from typing import Type
8
+
9
+ from pydantic.main import BaseModel
10
+
11
+ from explicit.domain import HasUnsetValues
12
+
13
+
14
+ class Command(HasUnsetValues, BaseModel):
15
+
16
+ """Объект запроса на обновление данных.
17
+
18
+ .. code-block:: python
19
+
20
+ class RegisterStudent(Command):
21
+ last_name: str
22
+ first_name: str
23
+
24
+ """
25
+
26
+ def dict_by_prefix(
27
+ self, prefix: str, *, removeprefix=False, exclude=None
28
+ ) -> dict:
29
+ assert prefix, f'prefix not set: "{prefix}"'
30
+
31
+ if exclude is None:
32
+ exclude = ()
33
+
34
+ def removeprefix_(k: str) -> str:
35
+ if removeprefix:
36
+ return k.removeprefix(prefix)
37
+ return k
38
+
39
+ fields = (
40
+ (removeprefix_(k), v)
41
+ for k, v in self.dict().items()
42
+ if k.startswith(prefix)
43
+ )
44
+
45
+ return dict(
46
+ (k, v) for k, v in fields
47
+ if k not in exclude
48
+ )
49
+
50
+
51
+ class CommandHandler(Protocol):
52
+
53
+ """Обработчик команды.
54
+
55
+ .. code-block:: python
56
+
57
+ def register_student(command: RegisterStudent, uow: UnitOfWork) -> None::
58
+ assert isinstance(command, Command)
59
+ student = factory.create(last_name=command.last_name, first_name=command.first_name)
60
+ repository.add(student)
61
+ """
62
+
63
+ def __call__(self, command: Command, **kwargs) -> Any:
64
+ """Обработка запроса на обновление данных."""
65
+
66
+
67
+ class CommandBusMixin(ABC):
68
+
69
+ """Примесь к шине сообщений, реализует обработку команд на обновление данных."""
70
+
71
+ _command_handlers_value: Dict[Type[Command], CommandHandler]
72
+
73
+ @property
74
+ def _command_handlers(self) -> Dict[Type[Command], CommandHandler]:
75
+ if not hasattr(self, '_command_handlers_value'):
76
+ self._command_handlers_value = {}
77
+ return self._command_handlers_value
78
+
79
+ def add_command_handler(self, command: Type[Command], handler: CommandHandler) -> None:
80
+ """Регистрация обработчика команды."""
81
+ assert issubclass(command, Command)
82
+ assert callable(handler)
83
+ self._command_handlers[command] = handler
84
+
85
+ def add_command_handlers(
86
+ self,
87
+ *handlers: Tuple[Type[Command], CommandHandler]
88
+ ) -> None:
89
+ """Регистрация обработчиков команд."""
90
+ for command, handler in handlers:
91
+ self.add_command_handler(command, handler)
92
+
93
+ def handle_command(self, command: Command):
94
+ """Передача запроса обработчику типа данного команды."""
95
+
96
+ assert isinstance(command, Command)
97
+ try:
98
+ return self._command_handlers[type(command)](command)
99
+ except KeyError as e:
100
+ raise ValueError(f'No handler for {type(command)}') from e
@@ -0,0 +1,48 @@
1
+ """Шина сообщений: внедрение зависимостей."""
2
+ from functools import partial
3
+ from typing import Any
4
+ from typing import Callable
5
+ from typing import Dict
6
+ from typing import Optional
7
+ import inspect
8
+ import logging
9
+
10
+
11
+ logger = logging.getLogger(__name__)
12
+ logger.setLevel(logging.INFO)
13
+
14
+
15
+ class UnknownDependencyError(Exception):
16
+ """Ожидаемая обработчиком зависимость не зарегистрирована."""
17
+
18
+
19
+ class DependencyInjectionMixin:
20
+
21
+ """Примесь к шине сообщений, реализует управление зависимостями и их внедрение."""
22
+
23
+ _dependencies: Dict[str, Callable]
24
+
25
+ def _set_dependencies(self, **dependencies: Any):
26
+ """Заменяет внутреннее сопоставление зависимостей."""
27
+ self._dependencies = dependencies
28
+
29
+ def _get_dependency(self, alias: str) -> Callable:
30
+ """Определяет и возвращает зависимость по имени."""
31
+ try:
32
+ return self._dependencies[alias]
33
+ except KeyError as e:
34
+ raise UnknownDependencyError(f'Зависимость для аргумента "{alias}" не найдена') from e
35
+
36
+ def _inject_dependencies(self, target: Callable, exclude: Optional[set[str]] = None) -> Callable:
37
+ """Внедряет зависимости в target."""
38
+ exclude = exclude or set()
39
+
40
+ target_signature = inspect.signature(target)
41
+ # определяем зависимости, которые ожидает обработчик
42
+ deps = {
43
+ name: self._get_dependency(name)
44
+ for name in target_signature.parameters.keys()
45
+ if name not in exclude
46
+ }
47
+ logger.debug('В обработчик %s внедрены зависимости %s', target, deps)
48
+ return partial(target, **deps)
@@ -0,0 +1,121 @@
1
+ """Шина сообщений: обработка событий."""
2
+ from abc import ABC
3
+ from typing import TYPE_CHECKING
4
+ from typing import Any
5
+ from typing import Callable
6
+ from typing import Dict
7
+ from typing import List
8
+ from typing import Optional
9
+ from typing import Type
10
+ from uuid import uuid4
11
+ import json
12
+
13
+ from more_itertools.recipes import consume
14
+ from pydantic.fields import Field
15
+ from pydantic.json import pydantic_encoder
16
+
17
+ from explicit.domain.model import asdict
18
+
19
+
20
+ if TYPE_CHECKING:
21
+ from dataclasses import dataclass # noqa
22
+
23
+ from explicit.unit_of_work import AbstractUnitOfWork # noqa
24
+ else:
25
+ from pydantic.dataclasses import dataclass # noqa
26
+
27
+
28
+ @dataclass
29
+ class EventMeta:
30
+ id: str = Field(default_factory=lambda: str(uuid4()))
31
+ transaction_id: Optional[str] = None
32
+ payload: Dict = Field(default_factory=dict)
33
+
34
+
35
+ @dataclass
36
+ class Event:
37
+ """Событие."""
38
+ meta: EventMeta = Field(default_factory=EventMeta)
39
+
40
+ def set_transaction_id(self, transaction_id: str):
41
+ self.meta.transaction_id = transaction_id
42
+
43
+ def get_transaction_id(self) -> Optional[str]:
44
+ return self.meta.transaction_id
45
+
46
+ def set_payload(self, payload: Dict):
47
+ assert isinstance(payload, dict)
48
+ self.meta.payload = payload
49
+
50
+ def get_payload(self):
51
+ return self.meta.payload
52
+
53
+ @classmethod
54
+ def from_event(cls, event: 'Event', **kwargs) -> 'Event':
55
+ return cls(
56
+ meta=EventMeta(transaction_id=event.meta.transaction_id, payload=event.meta.payload),
57
+ **kwargs
58
+ )
59
+
60
+ def configure_meta(
61
+ self, transaction_id: Optional[str] = None, payload: Optional[Dict] = None
62
+ ) -> None:
63
+ self.meta.transaction_id = transaction_id
64
+ if payload:
65
+ assert isinstance(payload, dict)
66
+ self.meta.payload = payload
67
+
68
+ def dump(self) -> str:
69
+ return json.dumps(
70
+ {
71
+ 'type': type(self).__name__,
72
+ 'body': asdict(self)
73
+ },
74
+ default=pydantic_encoder
75
+ )
76
+
77
+
78
+ EventHandler = Callable[[Event], Any]
79
+ """Обработчик события.
80
+
81
+ .. code-block:: python
82
+
83
+ def student_registered(event: StudentRegistered, uow: UnitOfWork) -> None:
84
+ assert isinstance(event, Event)
85
+ ...
86
+ """
87
+
88
+
89
+ class EventBusMixin(ABC):
90
+
91
+ """Примесь к шине сообщений, реализует обработку событий."""
92
+
93
+ _event_handlers_value: Dict[Type[Event], List[EventHandler]]
94
+
95
+ @property
96
+ def _event_handlers(self) -> Dict[Type[Event], List[EventHandler]]:
97
+ if not hasattr(self, '_event_handlers_value'):
98
+ self._event_handlers_value = {}
99
+ return self._event_handlers_value
100
+
101
+ def add_event_handler(
102
+ self, event: Type[Event], handler: EventHandler
103
+ ) -> None:
104
+ """Регистрация обработчика события."""
105
+
106
+ assert issubclass(event, Event)
107
+ assert callable(handler)
108
+ self._event_handlers.setdefault(event, []).append(handler)
109
+
110
+ def handle_event(self, event: Event):
111
+ """Передача события списку обработчиков типа данного события."""
112
+
113
+ assert isinstance(event, Event)
114
+
115
+ try:
116
+ consume(
117
+ handler(event)
118
+ for handler in self._event_handlers[type(event)]
119
+ )
120
+ except KeyError as e:
121
+ raise ValueError(f'No handler for {type(event)}') from e
@@ -0,0 +1,90 @@
1
+ """Шина сообщений."""
2
+ from typing import Type
3
+ import threading
4
+
5
+ from explicit.unit_of_work import AbstractUnitOfWork
6
+
7
+ from .commands import Command
8
+ from .commands import CommandBusMixin
9
+ from .commands import CommandHandler
10
+ from .dependency_injection import DependencyInjectionMixin
11
+ from .events import Event
12
+ from .events import EventBusMixin
13
+ from .events import EventHandler
14
+ from .queries import Query
15
+ from .queries import QueryBusMixin
16
+
17
+
18
+ class MessageBus(DependencyInjectionMixin, QueryBusMixin, CommandBusMixin, EventBusMixin):
19
+
20
+ """Шина.
21
+
22
+ Обеспечивает доставку команды соответствующему обработчику.
23
+
24
+ .. code-block:: python
25
+
26
+ from core import bus
27
+
28
+ bus.handle(Command(...))
29
+
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ uow: AbstractUnitOfWork,
35
+ **dependencies,
36
+ ):
37
+ self._thread_local = threading.local()
38
+ self._uow = uow
39
+ self._set_dependencies(uow=uow, **dependencies)
40
+
41
+ def add_command_handler(self, command: Type[Command], handler: CommandHandler) -> None:
42
+ """Внедрение зависимостей в обработчик и регистрация его обычным образом."""
43
+ super().add_command_handler(
44
+ command=command, handler=self._inject_dependencies(target=handler, exclude={'command'})
45
+ )
46
+
47
+ def add_event_handler(self, event: Type[Event], handler: EventHandler) -> None:
48
+ """Внедрение зависимостей в обработчик и регистрация его обычным образом."""
49
+ super().add_event_handler(event=event, handler=self._inject_dependencies(target=handler, exclude={'event'}))
50
+
51
+ def get_uow(self) -> AbstractUnitOfWork:
52
+ """Возвращает используемый UnitOfWork.
53
+
54
+ Может использоваться, например, для динамической регистрации репозиториев.
55
+ """
56
+ return self._uow
57
+
58
+ @property
59
+ def _message_queue(self):
60
+ if not hasattr(self._thread_local, 'message_bus_queue'):
61
+ self._thread_local.message_bus_queue = []
62
+ return self._thread_local.message_bus_queue
63
+
64
+ def _collect_events(self):
65
+ self._message_queue.extend(self._uow.collect_events())
66
+
67
+ def handle(self, message):
68
+ result = None
69
+
70
+ self._message_queue[:] = [message]
71
+
72
+ while self._message_queue:
73
+ message = self._message_queue.pop(0)
74
+
75
+ if isinstance(message, Query):
76
+ result = self.handle_query(message)
77
+
78
+ elif isinstance(message, Command):
79
+ result = self.handle_command(message)
80
+
81
+ elif isinstance(message, Event):
82
+ # результат обработки события нас не интересует.
83
+ self.handle_event(message)
84
+
85
+ else:
86
+ raise ValueError(f'Unknown message type: {message}')
87
+
88
+ self._collect_events()
89
+
90
+ return result
@@ -0,0 +1,51 @@
1
+ """Шина сообщений: обработка запросов на чтение данных."""
2
+ from abc import ABC
3
+ from abc import abstractmethod
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+ from typing import Dict
7
+ from typing import Type
8
+
9
+
10
+ @dataclass
11
+ class Query(ABC):
12
+ """Объект запроса на чтение данных."""
13
+
14
+
15
+ class QueryHandler(ABC):
16
+
17
+ @abstractmethod
18
+ def handle(self, query: Query) -> Any:
19
+ """Обработка запроса на чтение данных."""
20
+ assert isinstance(query, Query)
21
+
22
+
23
+ class QueryBusMixin(ABC):
24
+
25
+ """Примесь к шине сообщений, реализует обработку запросов на чтение данных."""
26
+
27
+ _query_handlers_value: Dict[Type[Query], QueryHandler]
28
+
29
+ @property
30
+ def _query_handlers(self) -> Dict[Type[Query], QueryHandler]:
31
+ if not hasattr(self, "_query_handlers_value"):
32
+ self._query_handlers_value = {}
33
+ return self._query_handlers_value
34
+
35
+ def add_query_handler(
36
+ self, query: Type[Query], handler: QueryHandler
37
+ ) -> None:
38
+ """Регистрация обработчика запроса."""
39
+
40
+ assert issubclass(query, Query)
41
+ assert isinstance(handler, QueryHandler)
42
+ self._query_handlers[query] = handler
43
+
44
+ def handle_query(
45
+ self,
46
+ query: Query,
47
+ ):
48
+ """Передача запроса обработчику типа данного запроса."""
49
+
50
+ assert isinstance(query, Query)
51
+ return self._query_handlers[type(query)].handle(query)
explicit/py.typed ADDED
File without changes
@@ -0,0 +1,184 @@
1
+ """Абстракции "Единицы работы"."""
2
+ from abc import ABC
3
+ from abc import abstractmethod
4
+ from queue import Queue
5
+ from typing import Dict
6
+ from typing import Optional
7
+ from typing import Tuple
8
+ from typing import Type
9
+ import threading
10
+
11
+ from explicit.adapters.db import AbstractRepository
12
+
13
+
14
+ class RepositoryRegistry:
15
+
16
+ """Реестр репозиториев."""
17
+
18
+ __repositories: Dict[str, AbstractRepository] # pylint: disable=invalid-name
19
+
20
+ def __init__(self, uow: 'AbstractUnitOfWork'):
21
+ self.__repositories = {}
22
+ self.__uow = uow
23
+
24
+ def register(
25
+ self, *repositories: Tuple[str, AbstractRepository]
26
+ ):
27
+ for name, repository in repositories:
28
+ self.__repositories[name] = repository
29
+ setattr(self.__uow, name, repository)
30
+
31
+ def get(self, name: str) -> AbstractRepository:
32
+ return self.__repositories[name]
33
+
34
+ def __iter__(self):
35
+ yield from self.__repositories.values()
36
+
37
+
38
+ # данные уровня модуля, локальные для текущего потока
39
+ _thread_local = threading.local()
40
+
41
+
42
+ class AbstractUnitOfWork(ABC):
43
+
44
+ """Единица работы, логическая бизнес-транзакция.
45
+
46
+ Обеспечивает атомарность выполняемых операций.
47
+
48
+ .. code-block:: python
49
+
50
+ class UnitOfWork(AbstractUnitOfWork):
51
+
52
+ units: UnitsRepository
53
+ users: UsersRepository
54
+ persons: PersonsRepository
55
+ students: StudentsRepository
56
+
57
+ def handler(...):
58
+ with UnitOfWork().wraps():
59
+ ...
60
+
61
+ """
62
+
63
+ _registry: RepositoryRegistry
64
+
65
+ strategy: 'AbstractTransactionStrategy'
66
+
67
+ def __init__(self, *args, **kwargs) -> None: # pylint: disable=unused-argument
68
+ self._registry = RepositoryRegistry(self)
69
+ # данные уровня экземпляра, локальные для текущего потока
70
+ self._thread_local = threading.local()
71
+
72
+ def register_repositories(
73
+ self, *repositories: Tuple[str, AbstractRepository]
74
+ ):
75
+ """Регистрация репозиториев.
76
+
77
+ Пример:
78
+
79
+ .. code-block::python
80
+
81
+ class AppConfig(AppConfigBase):
82
+ def ready(self):
83
+ self._register_repositories()
84
+
85
+ def _register_repositories(self):
86
+ from app.core import bus
87
+ from .adapters.db import repository
88
+
89
+ bus.get_uow().register_repositories(('persons', repository))
90
+
91
+ """
92
+ self._registry.register(*repositories)
93
+
94
+ @property
95
+ def _wrapping(self) -> bool:
96
+ self._init_per_thread_state()
97
+ return self._thread_local.uow_wrapping
98
+
99
+ @_wrapping.setter
100
+ def _wrapping(self, value):
101
+ self._init_per_thread_state()
102
+ self._thread_local.uow_wrapping = value
103
+
104
+ @property
105
+ def _event_queue(self):
106
+ if not hasattr(_thread_local, 'event_queue'):
107
+ _thread_local.event_queue = Queue()
108
+ return _thread_local.event_queue
109
+
110
+ def _init_per_thread_state(self):
111
+ """Инициализирует локальные для потока атрибуты значениями по-умолчанию."""
112
+ if not hasattr(self._thread_local, 'uow_init'):
113
+ self._thread_local.uow_init = True
114
+ self._thread_local.uow_wrapping = False
115
+
116
+ @abstractmethod
117
+ def _get_new_self(self) -> 'AbstractUnitOfWork':
118
+ """Возвращает экземпляр UnitOfWork для вложенных транзакций."""
119
+
120
+ def commit(self):
121
+ """Зафиксировать транзакцию."""
122
+ self.strategy.commit()
123
+
124
+ def rollback(self):
125
+ """Откатить транзакцию."""
126
+ self.strategy.rollback()
127
+
128
+ def wrap(self) -> 'AbstractUnitOfWork':
129
+ """Обернуть логику в бизнес-транзакцию."""
130
+ if not self._wrapping:
131
+ self._wrapping = True
132
+ return self
133
+
134
+ return self._get_new_self()
135
+
136
+ def add_event(self, event):
137
+ self._event_queue.put(event)
138
+
139
+ def collect_events(self):
140
+ while not self._event_queue.empty():
141
+ yield self._event_queue.get_nowait()
142
+
143
+ def __enter__(self) -> 'AbstractUnitOfWork':
144
+ """Начало транзакции."""
145
+
146
+ self.strategy.__enter__()
147
+ return self
148
+
149
+ def __exit__(
150
+ self,
151
+ exc_type: Optional[Type[Exception]],
152
+ *args: Tuple
153
+ ) -> None:
154
+ """Окончание транзакции."""
155
+
156
+ try:
157
+ if exc_type is not None:
158
+ self.rollback()
159
+ else:
160
+ self.commit()
161
+ except Exception as e:
162
+ raise e
163
+ finally:
164
+ self._wrapping = False
165
+ self.strategy.__exit__()
166
+
167
+
168
+ class AbstractTransactionStrategy(ABC):
169
+
170
+ """Абстрактная транзакционная стратегия."""
171
+
172
+ def __enter__(self):
173
+ """Действия при входе в транзакцию."""
174
+
175
+ def __exit__(self, *_):
176
+ """Действия при выходе из транзакции."""
177
+
178
+ @abstractmethod
179
+ def commit(self):
180
+ """Зафиксировать транзакцию."""
181
+
182
+ @abstractmethod
183
+ def rollback(self):
184
+ """Откатить транзакцию."""