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.
- explicit/adapters/__init__.py +0 -0
- explicit/adapters/db.py +63 -0
- explicit/adapters/fs.py +40 -0
- explicit/adapters/messaging.py +14 -0
- explicit/contrib/__init__.py +0 -0
- explicit/contrib/domain/__init__.py +0 -0
- explicit/contrib/domain/model/__init__.py +9 -0
- explicit/contrib/domain/model/fields.py +31 -0
- explicit/contrib/messagebus/__init__.py +0 -0
- explicit/contrib/messagebus/event_registry.py +56 -0
- explicit/domain/__init__.py +28 -0
- explicit/domain/factories.py +37 -0
- explicit/domain/fields.py +9 -0
- explicit/domain/model.py +152 -0
- explicit/domain/types.py +36 -0
- explicit/domain/validation/__init__.py +0 -0
- explicit/domain/validation/exceptions.py +148 -0
- explicit/messagebus/__init__.py +8 -0
- explicit/messagebus/commands.py +100 -0
- explicit/messagebus/dependency_injection.py +48 -0
- explicit/messagebus/events.py +121 -0
- explicit/messagebus/messagebus.py +90 -0
- explicit/messagebus/queries.py +51 -0
- explicit/py.typed +0 -0
- explicit/unit_of_work.py +184 -0
- explicit_python-2.2.1.dist-info/METADATA +298 -0
- explicit_python-2.2.1.dist-info/RECORD +30 -0
- explicit_python-2.2.1.dist-info/WHEEL +5 -0
- explicit_python-2.2.1.dist-info/dependency_links.txt +1 -0
- explicit_python-2.2.1.dist-info/top_level.txt +1 -0
|
@@ -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
|
explicit/unit_of_work.py
ADDED
|
@@ -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
|
+
"""Откатить транзакцию."""
|