python-cqrs 0.0.2__tar.gz → 0.0.5__tar.gz
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.
- {python_cqrs-0.0.2/src/python_cqrs.egg-info → python_cqrs-0.0.5}/PKG-INFO +15 -16
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/pyproject.toml +13 -15
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/compressors/protocol.py +2 -0
- python_cqrs-0.0.5/src/cqrs/container/di.py +29 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/container/protocol.py +3 -3
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/dispatcher/dispatcher.py +22 -8
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/events/bootstrap.py +12 -15
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/events/event.py +9 -5
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/events/event_emitter.py +11 -6
- python_cqrs-0.0.5/src/cqrs/events/map.py +28 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/mediator.py +17 -11
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/middlewares/base.py +1 -2
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/middlewares/logging.py +9 -10
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/outbox/producer.py +32 -7
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/outbox/protocol.py +13 -1
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/outbox/sqlalchemy.py +87 -29
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/requests/bootstrap.py +18 -20
- python_cqrs-0.0.5/src/cqrs/requests/map.py +27 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5/src/python_cqrs.egg-info}/PKG-INFO +15 -16
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/python_cqrs.egg-info/SOURCES.txt +0 -1
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/python_cqrs.egg-info/requires.txt +6 -11
- python_cqrs-0.0.2/src/cqrs/container/di.py +0 -19
- python_cqrs-0.0.2/src/cqrs/events/map.py +0 -27
- python_cqrs-0.0.2/src/cqrs/registry.py +0 -29
- python_cqrs-0.0.2/src/cqrs/requests/map.py +0 -30
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/LICENSE +0 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/README.md +0 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/setup.cfg +0 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/__init__.py +0 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/adapters/__init__.py +0 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/adapters/amqp.py +0 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/adapters/kafka.py +0 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/compressors/__init__.py +0 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/compressors/zlib.py +0 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/container/__init__.py +0 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/dispatcher/__init__.py +0 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/events/__init__.py +0 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/events/event_handler.py +0 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/message_brokers/__init__.py +0 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/message_brokers/amqp.py +0 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/message_brokers/devnull.py +0 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/message_brokers/kafka.py +0 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/message_brokers/protocol.py +0 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/middlewares/__init__.py +0 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/outbox/__init__.py +0 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/outbox/repository.py +0 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/requests/__init__.py +0 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/requests/request.py +0 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/requests/request_handler.py +0 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/response.py +0 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/python_cqrs.egg-info/dependency_links.txt +0 -0
- {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/python_cqrs.egg-info/top_level.txt +0 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-cqrs
|
|
3
|
-
Version: 0.0.
|
|
4
|
-
|
|
3
|
+
Version: 0.0.5
|
|
4
|
+
Summary: Python CQRS pattern implementation
|
|
5
|
+
Author: Nikita Kunov
|
|
6
|
+
Author-email: Vadim Kozyrevskiy <vadikko2@mail.ru>, Dmitriy Kutlubaev <kutlubaev00@mail.ru>
|
|
5
7
|
Description-Content-Type: text/markdown
|
|
6
8
|
License-File: LICENSE
|
|
7
9
|
Requires-Dist: pydantic==2.*
|
|
@@ -10,22 +12,19 @@ Requires-Dist: aio-pika==9.3.0
|
|
|
10
12
|
Requires-Dist: di[anyio]==0.79.2
|
|
11
13
|
Requires-Dist: sqlalchemy[asyncio]==2.0.*
|
|
12
14
|
Requires-Dist: retry-async==0.1.4
|
|
13
|
-
Provides-Extra:
|
|
14
|
-
Requires-Dist: pre-commit==3.8.0; extra == "
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pre-commit==3.8.0; extra == "dev"
|
|
17
|
+
Requires-Dist: pyright==1.1.377; extra == "dev"
|
|
18
|
+
Requires-Dist: ruff==0.6.2; extra == "dev"
|
|
19
|
+
Requires-Dist: aiokafka==0.10.0; extra == "dev"
|
|
20
|
+
Requires-Dist: pytest~=7.4.2; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-asyncio~=0.21.1; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-env==0.6.2; extra == "dev"
|
|
23
|
+
Requires-Dist: python-dotenv==1.0.1; extra == "dev"
|
|
24
|
+
Requires-Dist: cryptography==42.0.2; extra == "dev"
|
|
25
|
+
Requires-Dist: asyncmy==0.2.9; extra == "dev"
|
|
15
26
|
Provides-Extra: kafka
|
|
16
27
|
Requires-Dist: aiokafka==0.10.0; extra == "kafka"
|
|
17
|
-
Provides-Extra: lint
|
|
18
|
-
Requires-Dist: flake8==7.0.0; extra == "lint"
|
|
19
|
-
Requires-Dist: flake8-pytest; extra == "lint"
|
|
20
|
-
Requires-Dist: black; extra == "lint"
|
|
21
|
-
Provides-Extra: tests
|
|
22
|
-
Requires-Dist: aiokafka==0.10.0; extra == "tests"
|
|
23
|
-
Requires-Dist: pytest~=7.4.2; extra == "tests"
|
|
24
|
-
Requires-Dist: pytest-asyncio~=0.21.1; extra == "tests"
|
|
25
|
-
Requires-Dist: pytest-env==0.6.2; extra == "tests"
|
|
26
|
-
Requires-Dist: python-dotenv==1.0.1; extra == "tests"
|
|
27
|
-
Requires-Dist: cryptography==42.0.2; extra == "tests"
|
|
28
|
-
Requires-Dist: asyncmy==0.2.9; extra == "tests"
|
|
29
28
|
|
|
30
29
|
# CQRS
|
|
31
30
|
|
|
@@ -4,7 +4,9 @@ requires = ["setuptools>=42", "wheel"]
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
authors = [
|
|
7
|
-
{name = "Vadim Kozyrevskiy"}
|
|
7
|
+
{name = "Vadim Kozyrevskiy", email = "vadikko2@mail.ru"},
|
|
8
|
+
{name = "Dmitriy Kutlubaev", email = "kutlubaev00@mail.ru"},
|
|
9
|
+
{name = "Nikita Kunov"}
|
|
8
10
|
]
|
|
9
11
|
dependencies = [
|
|
10
12
|
"pydantic==2.*",
|
|
@@ -14,24 +16,17 @@ dependencies = [
|
|
|
14
16
|
"sqlalchemy[asyncio]==2.0.*",
|
|
15
17
|
"retry-async==0.1.4"
|
|
16
18
|
]
|
|
17
|
-
description = ""
|
|
19
|
+
description = "Python CQRS pattern implementation"
|
|
18
20
|
name = "python-cqrs"
|
|
19
21
|
readme = "README.md"
|
|
20
|
-
version = "0.0.
|
|
22
|
+
version = "0.0.5"
|
|
21
23
|
|
|
22
24
|
[project.optional-dependencies]
|
|
23
|
-
|
|
24
|
-
"pre-commit==3.8.0
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
]
|
|
29
|
-
lint = [
|
|
30
|
-
"flake8==7.0.0",
|
|
31
|
-
"flake8-pytest",
|
|
32
|
-
"black"
|
|
33
|
-
]
|
|
34
|
-
tests = [
|
|
25
|
+
dev = [
|
|
26
|
+
"pre-commit==3.8.0",
|
|
27
|
+
"pyright==1.1.377",
|
|
28
|
+
"ruff==0.6.2",
|
|
29
|
+
# Tests
|
|
35
30
|
"aiokafka==0.10.0",
|
|
36
31
|
"pytest~=7.4.2",
|
|
37
32
|
"pytest-asyncio~=0.21.1",
|
|
@@ -40,6 +35,9 @@ tests = [
|
|
|
40
35
|
"cryptography==42.0.2",
|
|
41
36
|
"asyncmy==0.2.9"
|
|
42
37
|
]
|
|
38
|
+
kafka = [
|
|
39
|
+
"aiokafka==0.10.0"
|
|
40
|
+
]
|
|
43
41
|
|
|
44
42
|
[tool.pytest.ini_options]
|
|
45
43
|
addopts = "--junit-xml=report.xml"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
import di
|
|
4
|
+
from di import dependent, executors
|
|
5
|
+
|
|
6
|
+
from cqrs import container as cqrs_container
|
|
7
|
+
|
|
8
|
+
T = typing.TypeVar("T")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DIContainer(cqrs_container.Container[di.Container]):
|
|
12
|
+
def __init__(self, external_container: di.Container) -> None:
|
|
13
|
+
self._external_container = external_container
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def external_container(self) -> di.Container:
|
|
17
|
+
return self._external_container
|
|
18
|
+
|
|
19
|
+
def attach_external_container(self, container: di.Container) -> None:
|
|
20
|
+
self._external_container = container
|
|
21
|
+
|
|
22
|
+
async def resolve(self, type_: typing.Type[T]) -> T:
|
|
23
|
+
executor = executors.AsyncExecutor()
|
|
24
|
+
solved = self._external_container.solve(
|
|
25
|
+
dependent.Dependent(type_, scope="request"),
|
|
26
|
+
scopes=["request"],
|
|
27
|
+
)
|
|
28
|
+
with self._external_container.enter_scope("request") as state:
|
|
29
|
+
return await solved.execute_async(executor=executor, state=state)
|
|
@@ -11,10 +11,10 @@ class Container(typing.Protocol[C]):
|
|
|
11
11
|
|
|
12
12
|
@property
|
|
13
13
|
def external_container(self) -> C:
|
|
14
|
-
|
|
14
|
+
raise NotImplementedError
|
|
15
15
|
|
|
16
16
|
def attach_external_container(self, container: C) -> None:
|
|
17
|
-
|
|
17
|
+
raise NotImplementedError
|
|
18
18
|
|
|
19
19
|
async def resolve(self, type_: typing.Type[T]) -> T:
|
|
20
|
-
|
|
20
|
+
raise NotImplementedError
|
|
@@ -3,14 +3,20 @@ import typing
|
|
|
3
3
|
|
|
4
4
|
import pydantic
|
|
5
5
|
|
|
6
|
-
from cqrs import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
from cqrs import (
|
|
7
|
+
container as di_container,
|
|
8
|
+
events as cqrs_events,
|
|
9
|
+
middlewares,
|
|
10
|
+
requests,
|
|
11
|
+
response as res,
|
|
12
|
+
)
|
|
10
13
|
|
|
11
14
|
logger = logging.getLogger("cqrs")
|
|
12
15
|
|
|
13
16
|
|
|
17
|
+
class RequestHandlerDoesNotExist(Exception): ...
|
|
18
|
+
|
|
19
|
+
|
|
14
20
|
class RequestDispatchResult(pydantic.BaseModel):
|
|
15
21
|
response: res.Response | None = pydantic.Field(default=None)
|
|
16
22
|
events: typing.List[cqrs_events.Event] = pydantic.Field(default_factory=list)
|
|
@@ -28,7 +34,11 @@ class RequestDispatcher:
|
|
|
28
34
|
self._middleware_chain = middleware_chain or middlewares.MiddlewareChain()
|
|
29
35
|
|
|
30
36
|
async def dispatch(self, request: requests.Request) -> RequestDispatchResult:
|
|
31
|
-
handler_type = self._request_map.get(type(request))
|
|
37
|
+
handler_type = self._request_map.get(type(request), None)
|
|
38
|
+
if handler_type is None:
|
|
39
|
+
raise RequestHandlerDoesNotExist(
|
|
40
|
+
f"RequestHandler not found matching Request type {type(request)}",
|
|
41
|
+
)
|
|
32
42
|
handler = await self._container.resolve(handler_type)
|
|
33
43
|
wrapped_handle = self._middleware_chain.wrap(handler.handle)
|
|
34
44
|
response = await wrapped_handle(request)
|
|
@@ -49,12 +59,16 @@ class EventDispatcher:
|
|
|
49
59
|
self._container = container
|
|
50
60
|
self._middleware_chain = middleware_chain or middlewares.MiddlewareChain()
|
|
51
61
|
|
|
52
|
-
async def _handle_event(
|
|
62
|
+
async def _handle_event(
|
|
63
|
+
self,
|
|
64
|
+
event: cqrs_events.Event,
|
|
65
|
+
handle_type: typing.Type[cqrs_events.EventHandler[cqrs_events.Event]],
|
|
66
|
+
):
|
|
53
67
|
handler = await self._container.resolve(handle_type)
|
|
54
68
|
await handler.handle(event)
|
|
55
69
|
|
|
56
|
-
async def dispatch(self, event:
|
|
57
|
-
handler_types = self._event_map.get(type(event))
|
|
70
|
+
async def dispatch(self, event: cqrs_events.Event) -> None:
|
|
71
|
+
handler_types = self._event_map.get(type(event), [])
|
|
58
72
|
if not handler_types:
|
|
59
73
|
logger.warning(
|
|
60
74
|
"Handlers for event %s not found",
|
|
@@ -5,23 +5,19 @@ import di
|
|
|
5
5
|
import cqrs
|
|
6
6
|
from cqrs import events
|
|
7
7
|
from cqrs.container import di as ed_di_container
|
|
8
|
-
from cqrs.middlewares import base as mediator_middlewares
|
|
9
|
-
from cqrs.middlewares import logging as logging_middleware
|
|
8
|
+
from cqrs.middlewares import base as mediator_middlewares, logging as logging_middleware
|
|
10
9
|
|
|
11
10
|
|
|
12
11
|
def setup_mediator(
|
|
13
|
-
container: ed_di_container.DIContainer
|
|
14
|
-
middlewares: typing.Iterable[mediator_middlewares.Middleware]
|
|
15
|
-
events_mapper: typing.Callable[[events.EventMap], None] = None,
|
|
12
|
+
container: ed_di_container.DIContainer,
|
|
13
|
+
middlewares: typing.Iterable[mediator_middlewares.Middleware],
|
|
14
|
+
events_mapper: typing.Callable[[events.EventMap], None] | None = None,
|
|
16
15
|
) -> cqrs.EventMediator:
|
|
17
|
-
|
|
18
16
|
_events_mapper = events.EventMap()
|
|
19
|
-
if events_mapper:
|
|
17
|
+
if events_mapper is not None:
|
|
20
18
|
events_mapper(_events_mapper)
|
|
21
19
|
|
|
22
20
|
middleware_chain = mediator_middlewares.MiddlewareChain()
|
|
23
|
-
if middlewares is None:
|
|
24
|
-
middlewares = []
|
|
25
21
|
|
|
26
22
|
for middleware in middlewares:
|
|
27
23
|
middleware_chain.add(middleware)
|
|
@@ -34,9 +30,9 @@ def setup_mediator(
|
|
|
34
30
|
|
|
35
31
|
|
|
36
32
|
def bootstrap(
|
|
37
|
-
di_container: di.Container
|
|
38
|
-
middlewares: typing.
|
|
39
|
-
events_mapper: typing.Callable[[events.EventMap], None] = None,
|
|
33
|
+
di_container: di.Container,
|
|
34
|
+
middlewares: typing.Sequence[mediator_middlewares.Middleware] | None = None,
|
|
35
|
+
events_mapper: typing.Callable[[events.EventMap], None] | None = None,
|
|
40
36
|
on_startup: typing.List[typing.Callable[[], None]] | None = None,
|
|
41
37
|
) -> cqrs.EventMediator:
|
|
42
38
|
if on_startup is None:
|
|
@@ -45,11 +41,12 @@ def bootstrap(
|
|
|
45
41
|
for fun in on_startup:
|
|
46
42
|
fun()
|
|
47
43
|
|
|
48
|
-
if middlewares is None:
|
|
49
|
-
middlewares = []
|
|
50
44
|
container = ed_di_container.DIContainer(di_container)
|
|
45
|
+
middlewares_list: typing.List[mediator_middlewares.Middleware] = list(
|
|
46
|
+
middlewares or [],
|
|
47
|
+
)
|
|
51
48
|
return setup_mediator(
|
|
52
49
|
container,
|
|
53
50
|
events_mapper=events_mapper,
|
|
54
|
-
middlewares=
|
|
51
|
+
middlewares=middlewares_list + [logging_middleware.LoggingMiddleware()],
|
|
55
52
|
)
|
|
@@ -9,7 +9,7 @@ class Event(pydantic.BaseModel, frozen=True):
|
|
|
9
9
|
"""The base class for events"""
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
class DomainEvent(Event):
|
|
12
|
+
class DomainEvent(Event, frozen=True):
|
|
13
13
|
"""
|
|
14
14
|
The base class for domain events.
|
|
15
15
|
"""
|
|
@@ -18,7 +18,7 @@ class DomainEvent(Event):
|
|
|
18
18
|
_P = typing.TypeVar("_P")
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
class NotificationEvent(Event):
|
|
21
|
+
class NotificationEvent(Event, frozen=True):
|
|
22
22
|
"""
|
|
23
23
|
The base class for notification events.
|
|
24
24
|
|
|
@@ -38,7 +38,9 @@ class NotificationEvent(Event):
|
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
40
|
event_id: uuid.UUID = pydantic.Field(default_factory=uuid.uuid4)
|
|
41
|
-
event_timestamp: datetime.datetime = pydantic.Field(
|
|
41
|
+
event_timestamp: datetime.datetime = pydantic.Field(
|
|
42
|
+
default_factory=datetime.datetime.now,
|
|
43
|
+
)
|
|
42
44
|
event_name: typing.Text
|
|
43
45
|
event_type: typing.ClassVar[typing.Text] = "notification_event"
|
|
44
46
|
|
|
@@ -50,7 +52,7 @@ class NotificationEvent(Event):
|
|
|
50
52
|
return hash(self.event_id)
|
|
51
53
|
|
|
52
54
|
|
|
53
|
-
class ECSTEvent(Event, typing.Generic[_P]):
|
|
55
|
+
class ECSTEvent(Event, typing.Generic[_P], frozen=True):
|
|
54
56
|
"""
|
|
55
57
|
Base class for ECST events.
|
|
56
58
|
|
|
@@ -74,7 +76,9 @@ class ECSTEvent(Event, typing.Generic[_P]):
|
|
|
74
76
|
"""
|
|
75
77
|
|
|
76
78
|
event_id: uuid.UUID = pydantic.Field(default_factory=uuid.uuid4)
|
|
77
|
-
event_timestamp: datetime.datetime = pydantic.Field(
|
|
79
|
+
event_timestamp: datetime.datetime = pydantic.Field(
|
|
80
|
+
default_factory=datetime.datetime.now,
|
|
81
|
+
)
|
|
78
82
|
event_name: typing.Text
|
|
79
83
|
event_type: typing.ClassVar = "ecst_event"
|
|
80
84
|
|
|
@@ -24,12 +24,11 @@ class EventEmitter:
|
|
|
24
24
|
self._message_broker = message_broker
|
|
25
25
|
|
|
26
26
|
@functools.singledispatchmethod
|
|
27
|
-
async def emit(self, event: event.Event) -> None:
|
|
28
|
-
...
|
|
27
|
+
async def emit(self, event: event.Event) -> None: ...
|
|
29
28
|
|
|
30
29
|
@emit.register
|
|
31
30
|
async def _(self, event: event.DomainEvent) -> None:
|
|
32
|
-
handlers_types = self._event_map.get(type(event))
|
|
31
|
+
handlers_types = self._event_map.get(type(event), [])
|
|
33
32
|
if not handlers_types:
|
|
34
33
|
logger.warning(
|
|
35
34
|
"Handlers for domain event %s not found",
|
|
@@ -47,7 +46,9 @@ class EventEmitter:
|
|
|
47
46
|
@emit.register
|
|
48
47
|
async def _(self, event: event.NotificationEvent) -> None:
|
|
49
48
|
if not self._message_broker:
|
|
50
|
-
raise RuntimeError(
|
|
49
|
+
raise RuntimeError(
|
|
50
|
+
"To use NotificationEvent, message_broker argument must be specified.",
|
|
51
|
+
)
|
|
51
52
|
|
|
52
53
|
message = _build_message(event)
|
|
53
54
|
|
|
@@ -62,7 +63,9 @@ class EventEmitter:
|
|
|
62
63
|
@emit.register
|
|
63
64
|
async def _(self, event: event.ECSTEvent) -> None:
|
|
64
65
|
if not self._message_broker:
|
|
65
|
-
raise RuntimeError(
|
|
66
|
+
raise RuntimeError(
|
|
67
|
+
"To use ECSTEvent, message_broker argument must be specified.",
|
|
68
|
+
)
|
|
66
69
|
|
|
67
70
|
message = _build_message(event)
|
|
68
71
|
|
|
@@ -75,7 +78,9 @@ class EventEmitter:
|
|
|
75
78
|
await self._message_broker.send_message(message)
|
|
76
79
|
|
|
77
80
|
|
|
78
|
-
def _build_message(
|
|
81
|
+
def _build_message(
|
|
82
|
+
event: event.NotificationEvent | event.ECSTEvent,
|
|
83
|
+
) -> message_brokers.Message:
|
|
79
84
|
payload = event.model_dump(mode="json")
|
|
80
85
|
|
|
81
86
|
return message_brokers.Message(
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from cqrs.events import event, event_handler
|
|
4
|
+
|
|
5
|
+
_KT = typing.TypeVar("_KT", bound=typing.Type[event.Event])
|
|
6
|
+
_VT = typing.List[typing.Type[event_handler.EventHandler[event.Event]]]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EventMap(typing.Dict[_KT, _VT]):
|
|
10
|
+
def bind(
|
|
11
|
+
self,
|
|
12
|
+
event_type: _KT,
|
|
13
|
+
handler_type: typing.Type[event_handler.EventHandler[event.Event]],
|
|
14
|
+
) -> None:
|
|
15
|
+
if event_type not in self:
|
|
16
|
+
self[event_type] = [handler_type]
|
|
17
|
+
else:
|
|
18
|
+
if handler_type in self[event_type]:
|
|
19
|
+
raise KeyError(f"{handler_type} already bind to {event_type}")
|
|
20
|
+
self[event_type].append(handler_type)
|
|
21
|
+
|
|
22
|
+
def __setitem__(self, __key: _KT, __value: _VT) -> None:
|
|
23
|
+
if __key in self:
|
|
24
|
+
raise KeyError(f"{__key} already exists in registry")
|
|
25
|
+
super().__setitem__(__key, __value)
|
|
26
|
+
|
|
27
|
+
def __delitem__(self, __key_: _KT):
|
|
28
|
+
raise TypeError(f"{self.__class__.__name__} has no delete method")
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import typing
|
|
2
2
|
|
|
3
|
-
from cqrs import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
from cqrs import (
|
|
4
|
+
container as di_container,
|
|
5
|
+
dispatcher,
|
|
6
|
+
events,
|
|
7
|
+
middlewares,
|
|
8
|
+
requests,
|
|
9
|
+
response,
|
|
10
|
+
)
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
class RequestMediator:
|
|
@@ -43,7 +45,9 @@ class RequestMediator:
|
|
|
43
45
|
event_emitter: events.EventEmitter | None = None,
|
|
44
46
|
middleware_chain: middlewares.MiddlewareChain | None = None,
|
|
45
47
|
*,
|
|
46
|
-
dispatcher_type: typing.Type[
|
|
48
|
+
dispatcher_type: typing.Type[
|
|
49
|
+
dispatcher.RequestDispatcher
|
|
50
|
+
] = dispatcher.RequestDispatcher,
|
|
47
51
|
) -> None:
|
|
48
52
|
self._event_emitter = event_emitter
|
|
49
53
|
self._dispatcher = dispatcher_type(
|
|
@@ -52,7 +56,7 @@ class RequestMediator:
|
|
|
52
56
|
middleware_chain=middleware_chain, # type: ignore
|
|
53
57
|
)
|
|
54
58
|
|
|
55
|
-
async def send(self, request:
|
|
59
|
+
async def send(self, request: requests.Request) -> response.Response | None:
|
|
56
60
|
dispatch_result = await self._dispatcher.dispatch(request)
|
|
57
61
|
|
|
58
62
|
if dispatch_result.events:
|
|
@@ -60,7 +64,7 @@ class RequestMediator:
|
|
|
60
64
|
|
|
61
65
|
return dispatch_result.response
|
|
62
66
|
|
|
63
|
-
async def _send_events(self, events: typing.List[
|
|
67
|
+
async def _send_events(self, events: typing.List[events.Event]) -> None:
|
|
64
68
|
if not self._event_emitter:
|
|
65
69
|
return
|
|
66
70
|
|
|
@@ -91,7 +95,9 @@ class EventMediator:
|
|
|
91
95
|
container: di_container.Container,
|
|
92
96
|
middleware_chain: middlewares.MiddlewareChain | None = None,
|
|
93
97
|
*,
|
|
94
|
-
dispatcher_type: typing.Type[
|
|
98
|
+
dispatcher_type: typing.Type[
|
|
99
|
+
dispatcher.EventDispatcher
|
|
100
|
+
] = dispatcher.EventDispatcher,
|
|
95
101
|
):
|
|
96
102
|
self._dispatcher = dispatcher_type(
|
|
97
103
|
event_map=event_map, # type: ignore
|
|
@@ -99,5 +105,5 @@ class EventMediator:
|
|
|
99
105
|
middleware_chain=middleware_chain, # type: ignore
|
|
100
106
|
)
|
|
101
107
|
|
|
102
|
-
async def send(self, event:
|
|
108
|
+
async def send(self, event: events.Event) -> None:
|
|
103
109
|
await self._dispatcher.dispatch(event)
|
|
@@ -9,8 +9,7 @@ HandleType = typing.Callable[[Req], typing.Awaitable[Res]]
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class Middleware(typing.Protocol):
|
|
12
|
-
async def __call__(self, request: requests.Request, handle: HandleType) -> Res:
|
|
13
|
-
...
|
|
12
|
+
async def __call__(self, request: requests.Request, handle: HandleType) -> Res: ...
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
class MiddlewareChain:
|
|
@@ -2,21 +2,18 @@ import logging
|
|
|
2
2
|
import typing
|
|
3
3
|
|
|
4
4
|
from cqrs import requests, response
|
|
5
|
+
from cqrs.middlewares import base
|
|
5
6
|
|
|
6
7
|
Req = typing.TypeVar("Req", bound=requests.Request, contravariant=True)
|
|
7
8
|
Res = typing.TypeVar("Res", response.Response, None, covariant=True)
|
|
8
9
|
HandleType = typing.Callable[[Req], typing.Awaitable[Res]]
|
|
9
10
|
|
|
11
|
+
logger = logging.getLogger("cqrs")
|
|
10
12
|
|
|
11
|
-
class LoggingMiddleware:
|
|
12
|
-
def __init__(
|
|
13
|
-
self,
|
|
14
|
-
logger: logging.Logger | None = None,
|
|
15
|
-
) -> None:
|
|
16
|
-
self._logger = logger or logging.getLogger("cqrs")
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
class LoggingMiddleware(base.Middleware):
|
|
15
|
+
async def __call__(self, request: requests.Request, handle: HandleType) -> Res:
|
|
16
|
+
logger.debug(
|
|
20
17
|
"Handle %s request",
|
|
21
18
|
type(request).__name__,
|
|
22
19
|
extra={
|
|
@@ -25,11 +22,13 @@ class LoggingMiddleware:
|
|
|
25
22
|
},
|
|
26
23
|
)
|
|
27
24
|
resp = await handle(request)
|
|
28
|
-
|
|
25
|
+
logger.debug(
|
|
29
26
|
"Request %s handled",
|
|
30
27
|
type(request).__name__,
|
|
31
28
|
extra={
|
|
32
|
-
"request_json_fields": {
|
|
29
|
+
"request_json_fields": {
|
|
30
|
+
"response": resp.model_dump(mode="json") if resp else {},
|
|
31
|
+
},
|
|
33
32
|
"to_mask": True,
|
|
34
33
|
},
|
|
35
34
|
)
|
|
@@ -13,7 +13,10 @@ logger = logging.getLogger("cqrs")
|
|
|
13
13
|
logger.setLevel(logging.DEBUG)
|
|
14
14
|
|
|
15
15
|
SessionFactory: typing.TypeAlias = typing.Callable[[], sql_session.AsyncSession]
|
|
16
|
-
Serializer: typing.TypeAlias = typing.Callable[
|
|
16
|
+
Serializer: typing.TypeAlias = typing.Callable[
|
|
17
|
+
[repository_protocol.Event],
|
|
18
|
+
typing.Awaitable[typing.Dict],
|
|
19
|
+
]
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
class EventProducer:
|
|
@@ -27,15 +30,27 @@ class EventProducer:
|
|
|
27
30
|
self.repository = repository
|
|
28
31
|
self.serializer = serializer
|
|
29
32
|
|
|
30
|
-
async def periodically_task(
|
|
33
|
+
async def periodically_task(
|
|
34
|
+
self,
|
|
35
|
+
batch_size: int = 100,
|
|
36
|
+
wait_ms: int = 500,
|
|
37
|
+
) -> None:
|
|
31
38
|
"""Calls produce periodically with specified delay"""
|
|
32
39
|
while True:
|
|
33
40
|
await asyncio.sleep(float(wait_ms) / 1000.0)
|
|
34
41
|
await self.produce_batch(batch_size)
|
|
35
42
|
|
|
36
|
-
async def send_message(
|
|
43
|
+
async def send_message(
|
|
44
|
+
self,
|
|
45
|
+
session: object,
|
|
46
|
+
event: cqrs.ECSTEvent | cqrs.NotificationEvent,
|
|
47
|
+
):
|
|
37
48
|
try:
|
|
38
|
-
serialized = (
|
|
49
|
+
serialized = (
|
|
50
|
+
(await self.serializer(event))
|
|
51
|
+
if self.serializer
|
|
52
|
+
else event.model_dump(mode="json")
|
|
53
|
+
)
|
|
39
54
|
await self.message_broker.send_message(
|
|
40
55
|
broker_protocol.Message(
|
|
41
56
|
message_type=event.event_type,
|
|
@@ -45,10 +60,20 @@ class EventProducer:
|
|
|
45
60
|
),
|
|
46
61
|
)
|
|
47
62
|
except Exception as e:
|
|
48
|
-
logger.error(
|
|
49
|
-
|
|
63
|
+
logger.error(
|
|
64
|
+
f"Error while producing event {event.event_id} to kafka broker: {e}",
|
|
65
|
+
)
|
|
66
|
+
await self.repository.update_status(
|
|
67
|
+
session,
|
|
68
|
+
event.event_id,
|
|
69
|
+
repository_protocol.EventStatus.NOT_PRODUCED,
|
|
70
|
+
)
|
|
50
71
|
else:
|
|
51
|
-
await self.repository.update_status(
|
|
72
|
+
await self.repository.update_status(
|
|
73
|
+
session,
|
|
74
|
+
event.event_id,
|
|
75
|
+
repository_protocol.EventStatus.PRODUCED,
|
|
76
|
+
)
|
|
52
77
|
|
|
53
78
|
async def produce_one(self, event_id: uuid.UUID) -> None:
|
|
54
79
|
async with self.repository as session:
|
|
@@ -11,33 +11,45 @@ Event: typing.TypeAlias = ev.NotificationEvent | ev.ECSTEvent
|
|
|
11
11
|
class Outbox(typing.Protocol):
|
|
12
12
|
def add(self, event: Event):
|
|
13
13
|
"""Adds event to outbox"""
|
|
14
|
+
raise NotImplementedError
|
|
14
15
|
|
|
15
16
|
async def save(self):
|
|
16
17
|
"""Commits events to the storage"""
|
|
18
|
+
raise NotImplementedError
|
|
17
19
|
|
|
18
20
|
async def get_events(self, batch_size: int = 100) -> typing.List[Event]:
|
|
19
21
|
"""Returns not produced events"""
|
|
22
|
+
raise NotImplementedError
|
|
20
23
|
|
|
21
24
|
async def get_event(self, event_id: uuid.UUID) -> Event | None:
|
|
22
25
|
"""Returns event by id"""
|
|
26
|
+
raise NotImplementedError
|
|
23
27
|
|
|
24
28
|
async def mark_as_produced(self, event_id: uuid.UUID) -> None:
|
|
25
29
|
"""Marks event as produced"""
|
|
30
|
+
raise NotImplementedError
|
|
26
31
|
|
|
27
32
|
async def mark_as_failure(self, event_id: uuid.UUID) -> None:
|
|
28
33
|
"""Marks event as not produced with failure"""
|
|
34
|
+
raise NotImplementedError
|
|
29
35
|
|
|
30
36
|
|
|
31
37
|
class EventProducer(abc.ABC):
|
|
32
38
|
@abc.abstractmethod
|
|
33
39
|
async def produce_one(self, event_id: uuid.UUID) -> None:
|
|
34
40
|
"""Produces event to broker"""
|
|
41
|
+
raise NotImplementedError
|
|
35
42
|
|
|
36
43
|
@abc.abstractmethod
|
|
37
44
|
async def produce_batch(self, batch_size: int = 100) -> None:
|
|
38
45
|
"""Produces events to broker"""
|
|
46
|
+
raise NotImplementedError
|
|
39
47
|
|
|
40
|
-
async def periodically_task(
|
|
48
|
+
async def periodically_task(
|
|
49
|
+
self,
|
|
50
|
+
batch_size: int = 100,
|
|
51
|
+
wait_ms: int = 500,
|
|
52
|
+
) -> None:
|
|
41
53
|
"""Calls produce periodically with specified delay"""
|
|
42
54
|
while True:
|
|
43
55
|
await asyncio.sleep(float(wait_ms) / 1000.0)
|
|
@@ -46,13 +46,21 @@ class OutboxModel(Base):
|
|
|
46
46
|
autoincrement=True,
|
|
47
47
|
comment="Identity",
|
|
48
48
|
)
|
|
49
|
-
event_id = sqlalchemy.Column(
|
|
49
|
+
event_id = sqlalchemy.Column(
|
|
50
|
+
sqlalchemy.Uuid,
|
|
51
|
+
nullable=False,
|
|
52
|
+
comment="Event idempotency id",
|
|
53
|
+
)
|
|
50
54
|
event_id_bin = sqlalchemy.Column(
|
|
51
55
|
sqlalchemy.BINARY(16),
|
|
52
56
|
nullable=False,
|
|
53
57
|
comment="Event idempotency id in 16 bit presentation",
|
|
54
58
|
)
|
|
55
|
-
event_type = sqlalchemy.Column(
|
|
59
|
+
event_type = sqlalchemy.Column(
|
|
60
|
+
sqlalchemy.Enum(EventType),
|
|
61
|
+
nullable=False,
|
|
62
|
+
comment="Event type",
|
|
63
|
+
)
|
|
56
64
|
event_status = sqlalchemy.Column(
|
|
57
65
|
sqlalchemy.Enum(repository.EventStatus),
|
|
58
66
|
nullable=False,
|
|
@@ -65,7 +73,11 @@ class OutboxModel(Base):
|
|
|
65
73
|
default=0,
|
|
66
74
|
comment="Event producing flush counter",
|
|
67
75
|
)
|
|
68
|
-
event_name = sqlalchemy.Column(
|
|
76
|
+
event_name = sqlalchemy.Column(
|
|
77
|
+
sqlalchemy.String(255),
|
|
78
|
+
nullable=False,
|
|
79
|
+
comment="Event name",
|
|
80
|
+
)
|
|
69
81
|
|
|
70
82
|
created_at = sqlalchemy.Column(
|
|
71
83
|
sqlalchemy.DateTime,
|
|
@@ -73,10 +85,17 @@ class OutboxModel(Base):
|
|
|
73
85
|
server_default=func.now(),
|
|
74
86
|
comment="Event creation timestamp",
|
|
75
87
|
)
|
|
76
|
-
payload = sqlalchemy.Column(
|
|
88
|
+
payload = sqlalchemy.Column(
|
|
89
|
+
mysql.BLOB,
|
|
90
|
+
nullable=False,
|
|
91
|
+
default={},
|
|
92
|
+
comment="Event payload",
|
|
93
|
+
)
|
|
77
94
|
|
|
78
95
|
def row_to_dict(self) -> typing.Dict[typing.Text, typing.Any]:
|
|
79
|
-
return {
|
|
96
|
+
return {
|
|
97
|
+
column.name: getattr(self, column.name) for column in self.__table__.columns
|
|
98
|
+
}
|
|
80
99
|
|
|
81
100
|
@classmethod
|
|
82
101
|
def get_batch_query(cls, size: int) -> sqlalchemy.Select:
|
|
@@ -85,7 +104,12 @@ class OutboxModel(Base):
|
|
|
85
104
|
.select_from(cls)
|
|
86
105
|
.where(
|
|
87
106
|
sqlalchemy.and_(
|
|
88
|
-
cls.event_status.in_(
|
|
107
|
+
cls.event_status.in_(
|
|
108
|
+
[
|
|
109
|
+
repository.EventStatus.NEW,
|
|
110
|
+
repository.EventStatus.NOT_PRODUCED,
|
|
111
|
+
],
|
|
112
|
+
),
|
|
89
113
|
cls.flush_counter < MAX_FLUSH_COUNTER_VALUE,
|
|
90
114
|
),
|
|
91
115
|
)
|
|
@@ -103,7 +127,12 @@ class OutboxModel(Base):
|
|
|
103
127
|
.where(
|
|
104
128
|
sqlalchemy.and_(
|
|
105
129
|
cls.event_id_bin == func.UUID_TO_BIN(event_id),
|
|
106
|
-
cls.event_status.in_(
|
|
130
|
+
cls.event_status.in_(
|
|
131
|
+
[
|
|
132
|
+
repository.EventStatus.NEW,
|
|
133
|
+
repository.EventStatus.NOT_PRODUCED,
|
|
134
|
+
],
|
|
135
|
+
),
|
|
107
136
|
cls.flush_counter < MAX_FLUSH_COUNTER_VALUE,
|
|
108
137
|
),
|
|
109
138
|
)
|
|
@@ -116,19 +145,23 @@ class OutboxModel(Base):
|
|
|
116
145
|
def update_status_query(
|
|
117
146
|
cls,
|
|
118
147
|
event_id: uuid.UUID,
|
|
119
|
-
status:
|
|
120
|
-
repository.EventStatus.PRODUCED,
|
|
121
|
-
repository.EventStatus.NOT_PRODUCED,
|
|
122
|
-
],
|
|
148
|
+
status: repository.EventStatus,
|
|
123
149
|
) -> sqlalchemy.Update:
|
|
124
|
-
values = {
|
|
150
|
+
values = {
|
|
151
|
+
"event_status": status,
|
|
152
|
+
"flush_counter": cls.flush_counter,
|
|
153
|
+
}
|
|
125
154
|
if status == repository.EventStatus.NOT_PRODUCED:
|
|
126
|
-
values["flush_counter"]
|
|
155
|
+
values["flush_counter"] += 1
|
|
127
156
|
|
|
128
|
-
return
|
|
157
|
+
return (
|
|
158
|
+
sqlalchemy.update(cls)
|
|
159
|
+
.where(cls.event_id_bin == func.UUID_TO_BIN(event_id))
|
|
160
|
+
.values(**values)
|
|
161
|
+
)
|
|
129
162
|
|
|
130
163
|
@classmethod
|
|
131
|
-
def status_sorting_case(cls) -> sqlalchemy.
|
|
164
|
+
def status_sorting_case(cls) -> sqlalchemy.Case:
|
|
132
165
|
return sqlalchemy.case(
|
|
133
166
|
{
|
|
134
167
|
repository.EventStatus.NEW: 1,
|
|
@@ -140,8 +173,12 @@ class OutboxModel(Base):
|
|
|
140
173
|
)
|
|
141
174
|
|
|
142
175
|
|
|
143
|
-
class SqlAlchemyOutboxedEventRepository(
|
|
144
|
-
|
|
176
|
+
class SqlAlchemyOutboxedEventRepository(
|
|
177
|
+
repository.OutboxedEventRepository[sql_session.AsyncSession],
|
|
178
|
+
):
|
|
179
|
+
EVENT_CLASS_MAPPING: typing.ClassVar[
|
|
180
|
+
typing.Dict[EventType, typing.Type[repository.Event]]
|
|
181
|
+
] = {
|
|
145
182
|
EventType.NOTIFICATION_EVENT: ev.NotificationEvent,
|
|
146
183
|
EventType.ECST_EVENT: ev.ECSTEvent,
|
|
147
184
|
}
|
|
@@ -164,26 +201,42 @@ class SqlAlchemyOutboxedEventRepository(repository.OutboxedEventRepository[sql_s
|
|
|
164
201
|
def _process_events(self, model: OutboxModel) -> repository.Event:
|
|
165
202
|
event_dict = model.row_to_dict()
|
|
166
203
|
event_dict["payload"] = orjson.loads(
|
|
167
|
-
self._compressor.decompress(event_dict["payload"])
|
|
204
|
+
self._compressor.decompress(event_dict["payload"])
|
|
205
|
+
if self._compressor
|
|
206
|
+
else event_dict["payload"],
|
|
207
|
+
)
|
|
208
|
+
return self.EVENT_CLASS_MAPPING[event_dict["event_type"]].model_validate(
|
|
209
|
+
event_dict,
|
|
168
210
|
)
|
|
169
|
-
return self.EVENT_CLASS_MAPPING[event_dict["event_type"]].model_validate(event_dict)
|
|
170
211
|
|
|
171
|
-
async def get_many(
|
|
212
|
+
async def get_many(
|
|
213
|
+
self,
|
|
214
|
+
session: sql_session.AsyncSession,
|
|
215
|
+
batch_size: int = 100,
|
|
216
|
+
) -> typing.List[repository.Event]:
|
|
172
217
|
events: typing.Sequence[OutboxModel] = (
|
|
173
|
-
(await session.execute(OutboxModel.get_batch_query(batch_size)))
|
|
218
|
+
(await session.execute(OutboxModel.get_batch_query(batch_size)))
|
|
219
|
+
.scalars()
|
|
220
|
+
.all()
|
|
174
221
|
)
|
|
175
222
|
|
|
176
223
|
tasks = []
|
|
177
224
|
for event in events:
|
|
178
|
-
if not self.EVENT_CLASS_MAPPING.get(event.event_type):
|
|
225
|
+
if not self.EVENT_CLASS_MAPPING.get(EventType(event.event_type)):
|
|
179
226
|
logger.warning(f"Unknown event type for {event}")
|
|
180
227
|
continue
|
|
181
228
|
tasks.append(asyncio.to_thread(self._process_events, event))
|
|
182
229
|
|
|
183
230
|
return await asyncio.gather(*tasks) # noqa
|
|
184
231
|
|
|
185
|
-
async def get_one(
|
|
186
|
-
|
|
232
|
+
async def get_one(
|
|
233
|
+
self,
|
|
234
|
+
session: sql_session.AsyncSession,
|
|
235
|
+
event_id: uuid.UUID,
|
|
236
|
+
) -> repository.Event | None:
|
|
237
|
+
event: OutboxModel | None = (
|
|
238
|
+
await session.execute(OutboxModel.get_event_query(event_id))
|
|
239
|
+
).scalar()
|
|
187
240
|
|
|
188
241
|
if event is None:
|
|
189
242
|
return
|
|
@@ -194,7 +247,9 @@ class SqlAlchemyOutboxedEventRepository(repository.OutboxedEventRepository[sql_s
|
|
|
194
247
|
|
|
195
248
|
event_dict = event.row_to_dict()
|
|
196
249
|
event_dict["payload"] = orjson.loads(
|
|
197
|
-
self._compressor.decompress(event_dict["payload"])
|
|
250
|
+
self._compressor.decompress(event_dict["payload"])
|
|
251
|
+
if self._compressor
|
|
252
|
+
else event_dict["payload"],
|
|
198
253
|
)
|
|
199
254
|
|
|
200
255
|
return self.EVENT_CLASS_MAPPING[event.event_type].model_validate(event_dict)
|
|
@@ -205,7 +260,9 @@ class SqlAlchemyOutboxedEventRepository(repository.OutboxedEventRepository[sql_s
|
|
|
205
260
|
event_id: uuid.UUID,
|
|
206
261
|
new_status: repository.EventStatus,
|
|
207
262
|
) -> None:
|
|
208
|
-
await session.execute(
|
|
263
|
+
await session.execute(
|
|
264
|
+
statement=OutboxModel.update_status_query(event_id, new_status),
|
|
265
|
+
)
|
|
209
266
|
|
|
210
267
|
async def commit(self, session: sql_session.AsyncSession):
|
|
211
268
|
await session.commit()
|
|
@@ -218,6 +275,7 @@ class SqlAlchemyOutboxedEventRepository(repository.OutboxedEventRepository[sql_s
|
|
|
218
275
|
return self.session
|
|
219
276
|
|
|
220
277
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
278
|
+
if self.session:
|
|
279
|
+
await self.session.rollback()
|
|
280
|
+
await self.session.close()
|
|
281
|
+
self.session = None
|
|
@@ -6,16 +6,15 @@ import cqrs
|
|
|
6
6
|
from cqrs import events, requests
|
|
7
7
|
from cqrs.container import di as ed_di_container
|
|
8
8
|
from cqrs.message_brokers import devnull, protocol
|
|
9
|
-
from cqrs.middlewares import base as mediator_middlewares
|
|
10
|
-
from cqrs.middlewares import logging as logging_middleware
|
|
9
|
+
from cqrs.middlewares import base as mediator_middlewares, logging as logging_middleware
|
|
11
10
|
|
|
12
11
|
DEFAULT_MESSAGE_BROKER = devnull.DevnullMessageBroker()
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
def setup_event_emitter(
|
|
16
|
-
container: ed_di_container.DIContainer
|
|
17
|
-
domain_events_mapper: typing.Callable[[events.EventMap], None] = None,
|
|
18
|
-
message_broker: protocol.MessageBroker = None,
|
|
15
|
+
container: ed_di_container.DIContainer,
|
|
16
|
+
domain_events_mapper: typing.Callable[[events.EventMap], None] | None = None,
|
|
17
|
+
message_broker: protocol.MessageBroker | None = None,
|
|
19
18
|
):
|
|
20
19
|
if message_broker is None:
|
|
21
20
|
message_broker = DEFAULT_MESSAGE_BROKER
|
|
@@ -33,12 +32,11 @@ def setup_event_emitter(
|
|
|
33
32
|
|
|
34
33
|
def setup_mediator(
|
|
35
34
|
event_emitter: events.EventEmitter,
|
|
36
|
-
container: ed_di_container.DIContainer
|
|
37
|
-
middlewares: typing.Iterable[mediator_middlewares.Middleware]
|
|
38
|
-
commands_mapper: typing.Callable[[requests.RequestMap], None] = None,
|
|
39
|
-
queries_mapper: typing.Callable[[requests.RequestMap], None] = None,
|
|
35
|
+
container: ed_di_container.DIContainer,
|
|
36
|
+
middlewares: typing.Iterable[mediator_middlewares.Middleware],
|
|
37
|
+
commands_mapper: typing.Callable[[requests.RequestMap], None] | None = None,
|
|
38
|
+
queries_mapper: typing.Callable[[requests.RequestMap], None] | None = None,
|
|
40
39
|
) -> cqrs.RequestMediator:
|
|
41
|
-
|
|
42
40
|
requests_mapper = requests.RequestMap()
|
|
43
41
|
if commands_mapper:
|
|
44
42
|
commands_mapper(requests_mapper)
|
|
@@ -46,8 +44,6 @@ def setup_mediator(
|
|
|
46
44
|
queries_mapper(requests_mapper)
|
|
47
45
|
|
|
48
46
|
middleware_chain = mediator_middlewares.MiddlewareChain()
|
|
49
|
-
if middlewares is None:
|
|
50
|
-
middlewares = []
|
|
51
47
|
|
|
52
48
|
for middleware in middlewares:
|
|
53
49
|
middleware_chain.add(middleware)
|
|
@@ -61,12 +57,12 @@ def setup_mediator(
|
|
|
61
57
|
|
|
62
58
|
|
|
63
59
|
def bootstrap(
|
|
60
|
+
di_container: di.Container,
|
|
64
61
|
message_broker: protocol.MessageBroker | None = None,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
queries_mapper: typing.Callable[[requests.RequestMap], None] = None,
|
|
62
|
+
middlewares: typing.Sequence[mediator_middlewares.Middleware] | None = None,
|
|
63
|
+
commands_mapper: typing.Callable[[requests.RequestMap], None] | None = None,
|
|
64
|
+
domain_events_mapper: typing.Callable[[events.EventMap], None] | None = None,
|
|
65
|
+
queries_mapper: typing.Callable[[requests.RequestMap], None] | None = None,
|
|
70
66
|
on_startup: typing.List[typing.Callable[[], None]] | None = None,
|
|
71
67
|
) -> cqrs.RequestMediator:
|
|
72
68
|
if message_broker is None:
|
|
@@ -77,18 +73,20 @@ def bootstrap(
|
|
|
77
73
|
for fun in on_startup:
|
|
78
74
|
fun()
|
|
79
75
|
|
|
80
|
-
if middlewares is None:
|
|
81
|
-
middlewares = []
|
|
82
76
|
container = ed_di_container.DIContainer(di_container)
|
|
77
|
+
|
|
83
78
|
event_emitter = setup_event_emitter(
|
|
84
79
|
container,
|
|
85
80
|
domain_events_mapper,
|
|
86
81
|
message_broker,
|
|
87
82
|
)
|
|
83
|
+
middlewares_list: typing.List[mediator_middlewares.Middleware] = list(
|
|
84
|
+
middlewares or [],
|
|
85
|
+
)
|
|
88
86
|
return setup_mediator(
|
|
89
87
|
event_emitter,
|
|
90
88
|
container,
|
|
91
|
-
middlewares=
|
|
89
|
+
middlewares=middlewares_list + [logging_middleware.LoggingMiddleware()],
|
|
92
90
|
commands_mapper=commands_mapper,
|
|
93
91
|
queries_mapper=queries_mapper,
|
|
94
92
|
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from cqrs import response
|
|
4
|
+
from cqrs.requests import request, request_handler
|
|
5
|
+
|
|
6
|
+
_KT = typing.TypeVar("_KT", bound=typing.Type[request.Request])
|
|
7
|
+
_VT = typing.TypeVar(
|
|
8
|
+
"_VT",
|
|
9
|
+
bound=typing.Type[
|
|
10
|
+
request_handler.RequestHandler[request.Request, response.Response]
|
|
11
|
+
],
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RequestMap(typing.Dict[_KT, _VT]):
|
|
16
|
+
_registry: typing.Dict[_KT, _VT]
|
|
17
|
+
|
|
18
|
+
def bind(self, request_type: _KT, handler_type: _VT) -> None:
|
|
19
|
+
self[request_type] = handler_type
|
|
20
|
+
|
|
21
|
+
def __setitem__(self, __key: _KT, __value: _VT) -> None:
|
|
22
|
+
if __key in self:
|
|
23
|
+
raise KeyError(f"{__key} already exists in registry")
|
|
24
|
+
super().__setitem__(__key, __value)
|
|
25
|
+
|
|
26
|
+
def __delitem__(self, __key):
|
|
27
|
+
raise TypeError(f"{self.__class__.__name__} has no delete method")
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-cqrs
|
|
3
|
-
Version: 0.0.
|
|
4
|
-
|
|
3
|
+
Version: 0.0.5
|
|
4
|
+
Summary: Python CQRS pattern implementation
|
|
5
|
+
Author: Nikita Kunov
|
|
6
|
+
Author-email: Vadim Kozyrevskiy <vadikko2@mail.ru>, Dmitriy Kutlubaev <kutlubaev00@mail.ru>
|
|
5
7
|
Description-Content-Type: text/markdown
|
|
6
8
|
License-File: LICENSE
|
|
7
9
|
Requires-Dist: pydantic==2.*
|
|
@@ -10,22 +12,19 @@ Requires-Dist: aio-pika==9.3.0
|
|
|
10
12
|
Requires-Dist: di[anyio]==0.79.2
|
|
11
13
|
Requires-Dist: sqlalchemy[asyncio]==2.0.*
|
|
12
14
|
Requires-Dist: retry-async==0.1.4
|
|
13
|
-
Provides-Extra:
|
|
14
|
-
Requires-Dist: pre-commit==3.8.0; extra == "
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pre-commit==3.8.0; extra == "dev"
|
|
17
|
+
Requires-Dist: pyright==1.1.377; extra == "dev"
|
|
18
|
+
Requires-Dist: ruff==0.6.2; extra == "dev"
|
|
19
|
+
Requires-Dist: aiokafka==0.10.0; extra == "dev"
|
|
20
|
+
Requires-Dist: pytest~=7.4.2; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-asyncio~=0.21.1; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-env==0.6.2; extra == "dev"
|
|
23
|
+
Requires-Dist: python-dotenv==1.0.1; extra == "dev"
|
|
24
|
+
Requires-Dist: cryptography==42.0.2; extra == "dev"
|
|
25
|
+
Requires-Dist: asyncmy==0.2.9; extra == "dev"
|
|
15
26
|
Provides-Extra: kafka
|
|
16
27
|
Requires-Dist: aiokafka==0.10.0; extra == "kafka"
|
|
17
|
-
Provides-Extra: lint
|
|
18
|
-
Requires-Dist: flake8==7.0.0; extra == "lint"
|
|
19
|
-
Requires-Dist: flake8-pytest; extra == "lint"
|
|
20
|
-
Requires-Dist: black; extra == "lint"
|
|
21
|
-
Provides-Extra: tests
|
|
22
|
-
Requires-Dist: aiokafka==0.10.0; extra == "tests"
|
|
23
|
-
Requires-Dist: pytest~=7.4.2; extra == "tests"
|
|
24
|
-
Requires-Dist: pytest-asyncio~=0.21.1; extra == "tests"
|
|
25
|
-
Requires-Dist: pytest-env==0.6.2; extra == "tests"
|
|
26
|
-
Requires-Dist: python-dotenv==1.0.1; extra == "tests"
|
|
27
|
-
Requires-Dist: cryptography==42.0.2; extra == "tests"
|
|
28
|
-
Requires-Dist: asyncmy==0.2.9; extra == "tests"
|
|
29
28
|
|
|
30
29
|
# CQRS
|
|
31
30
|
|
|
@@ -5,18 +5,10 @@ di[anyio]==0.79.2
|
|
|
5
5
|
sqlalchemy[asyncio]==2.0.*
|
|
6
6
|
retry-async==0.1.4
|
|
7
7
|
|
|
8
|
-
[
|
|
8
|
+
[dev]
|
|
9
9
|
pre-commit==3.8.0
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
aiokafka==0.10.0
|
|
13
|
-
|
|
14
|
-
[lint]
|
|
15
|
-
flake8==7.0.0
|
|
16
|
-
flake8-pytest
|
|
17
|
-
black
|
|
18
|
-
|
|
19
|
-
[tests]
|
|
10
|
+
pyright==1.1.377
|
|
11
|
+
ruff==0.6.2
|
|
20
12
|
aiokafka==0.10.0
|
|
21
13
|
pytest~=7.4.2
|
|
22
14
|
pytest-asyncio~=0.21.1
|
|
@@ -24,3 +16,6 @@ pytest-env==0.6.2
|
|
|
24
16
|
python-dotenv==1.0.1
|
|
25
17
|
cryptography==42.0.2
|
|
26
18
|
asyncmy==0.2.9
|
|
19
|
+
|
|
20
|
+
[kafka]
|
|
21
|
+
aiokafka==0.10.0
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import typing
|
|
2
|
-
|
|
3
|
-
import di
|
|
4
|
-
from di import dependent, executors
|
|
5
|
-
|
|
6
|
-
from cqrs import container
|
|
7
|
-
|
|
8
|
-
T = typing.TypeVar("T")
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class DIContainer(container.Container[di.Container]):
|
|
12
|
-
def __init__(self, external_container: di.Container | None = None) -> None:
|
|
13
|
-
self._external_container = external_container
|
|
14
|
-
|
|
15
|
-
async def resolve(self, type_: typing.Type[T]) -> T:
|
|
16
|
-
executor = executors.AsyncExecutor()
|
|
17
|
-
solved = self._external_container.solve(dependent.Dependent(type_, scope="request"), scopes=["request"])
|
|
18
|
-
with self._external_container.enter_scope("request") as state:
|
|
19
|
-
return await solved.execute_async(executor=executor, state=state)
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import collections
|
|
2
|
-
import typing
|
|
3
|
-
|
|
4
|
-
from cqrs import registry
|
|
5
|
-
from cqrs.events import event, event_handler
|
|
6
|
-
|
|
7
|
-
E = typing.TypeVar("E", bound=event.Event, contravariant=True)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class EventMap(registry.InMemoryRegistry[typing.Type[E]], typing.List[typing.Type[event_handler.EventHandler]]):
|
|
11
|
-
_registry: collections.defaultdict
|
|
12
|
-
|
|
13
|
-
def __init__(self) -> None:
|
|
14
|
-
super().__init__()
|
|
15
|
-
self._registry = collections.defaultdict(list)
|
|
16
|
-
|
|
17
|
-
def bind(self, event_type: typing.Type[E], handler_type: typing.Type[event_handler.EventHandler[E]]) -> None:
|
|
18
|
-
self[event_type].append(handler_type)
|
|
19
|
-
|
|
20
|
-
def get(self, event_type: typing.Type[E]) -> typing.List[typing.Type[event_handler.EventHandler[E]]]:
|
|
21
|
-
return self._registry[event_type]
|
|
22
|
-
|
|
23
|
-
def get_events(self) -> list[typing.Type[E]]:
|
|
24
|
-
return list(self.keys())
|
|
25
|
-
|
|
26
|
-
def __str__(self) -> str:
|
|
27
|
-
return str(self._registry)
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import typing
|
|
2
|
-
from collections import abc
|
|
3
|
-
|
|
4
|
-
_VT = typing.TypeVar("_VT")
|
|
5
|
-
_KT = typing.TypeVar("_KT")
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class InMemoryRegistry(abc.MutableMapping[_KT, _VT]):
|
|
9
|
-
_registry: typing.Dict[_KT, _VT]
|
|
10
|
-
|
|
11
|
-
def __init__(self):
|
|
12
|
-
self._registry = dict()
|
|
13
|
-
|
|
14
|
-
def __setitem__(self, __key: _KT, __value: _VT) -> None:
|
|
15
|
-
if __key in self._registry:
|
|
16
|
-
raise KeyError(f"{__key} already exists in registry")
|
|
17
|
-
self._registry[__key] = __value
|
|
18
|
-
|
|
19
|
-
def __delitem__(self, __key):
|
|
20
|
-
raise TypeError(f"{self.__class__.__name__} has no delete method")
|
|
21
|
-
|
|
22
|
-
def __getitem__(self, __key: _KT) -> _VT:
|
|
23
|
-
return self._registry[__key]
|
|
24
|
-
|
|
25
|
-
def __len__(self):
|
|
26
|
-
return len(self._registry.keys())
|
|
27
|
-
|
|
28
|
-
def __iter__(self):
|
|
29
|
-
return iter(self._registry.keys())
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import typing
|
|
2
|
-
|
|
3
|
-
from cqrs import registry
|
|
4
|
-
from cqrs.requests import request, request_handler
|
|
5
|
-
|
|
6
|
-
TReq = typing.TypeVar("TReq", bound=typing.Type[request.Request], contravariant=True)
|
|
7
|
-
TH = typing.TypeVar("TH", bound=typing.Type[request_handler.RequestHandler], contravariant=True)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class RequestMap(registry.InMemoryRegistry[TReq, TH]):
|
|
11
|
-
def bind(
|
|
12
|
-
self,
|
|
13
|
-
request_type: TReq,
|
|
14
|
-
handler_type: TH,
|
|
15
|
-
) -> None:
|
|
16
|
-
self._registry[request_type] = handler_type
|
|
17
|
-
|
|
18
|
-
def get(self, request_type: TReq) -> TH:
|
|
19
|
-
handler_type = self._registry.get(request_type)
|
|
20
|
-
if not handler_type:
|
|
21
|
-
raise RequestHandlerDoesNotExist("RequestHandler not found matching Request type.")
|
|
22
|
-
|
|
23
|
-
return handler_type
|
|
24
|
-
|
|
25
|
-
def __str__(self) -> str:
|
|
26
|
-
return str(self._registry)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class RequestHandlerDoesNotExist(Exception):
|
|
30
|
-
...
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|