python-cqrs 4.7.2__tar.gz → 4.8.0__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-4.7.2/src/python_cqrs.egg-info → python_cqrs-4.8.0}/PKG-INFO +8 -9
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/pyproject.toml +9 -12
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/dispatcher/event.py +4 -1
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/dispatcher/saga.py +9 -1
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/dispatcher/streaming.py +9 -4
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/events/__init__.py +12 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/events/bootstrap.py +56 -1
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/events/event.py +43 -5
- python_cqrs-4.8.0/src/cqrs/events/event_emitter.py +142 -0
- python_cqrs-4.8.0/src/cqrs/events/event_handler.py +59 -0
- python_cqrs-4.8.0/src/cqrs/events/event_processor.py +143 -0
- python_cqrs-4.8.0/src/cqrs/events/map.py +68 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/mediator.py +18 -5
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/requests/bootstrap.py +6 -2
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/requests/cor_request_handler.py +7 -2
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/requests/request_handler.py +21 -5
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/saga/bootstrap.py +4 -2
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/saga/step.py +4 -3
- {python_cqrs-4.7.2 → python_cqrs-4.8.0/src/python_cqrs.egg-info}/PKG-INFO +8 -9
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/python_cqrs.egg-info/requires.txt +6 -8
- python_cqrs-4.7.2/src/cqrs/events/event_emitter.py +0 -82
- python_cqrs-4.7.2/src/cqrs/events/event_handler.py +0 -26
- python_cqrs-4.7.2/src/cqrs/events/event_processor.py +0 -66
- python_cqrs-4.7.2/src/cqrs/events/map.py +0 -29
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/LICENSE +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/README.md +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/setup.cfg +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/__init__.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/adapters/__init__.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/adapters/amqp.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/adapters/circuit_breaker.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/adapters/kafka.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/adapters/protocol.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/compressors/__init__.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/compressors/protocol.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/compressors/zlib.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/container/__init__.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/container/dependency_injector.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/container/di.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/container/protocol.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/deserializers/__init__.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/deserializers/exceptions.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/deserializers/json.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/dispatcher/__init__.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/dispatcher/exceptions.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/dispatcher/models.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/dispatcher/request.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/message_brokers/__init__.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/message_brokers/amqp.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/message_brokers/devnull.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/message_brokers/kafka.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/message_brokers/protocol.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/middlewares/__init__.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/middlewares/base.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/middlewares/logging.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/outbox/__init__.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/outbox/map.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/outbox/mock.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/outbox/repository.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/outbox/sqlalchemy.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/producer.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/requests/__init__.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/requests/map.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/requests/mermaid.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/requests/request.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/response.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/saga/__init__.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/saga/circuit_breaker.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/saga/compensation.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/saga/execution.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/saga/fallback.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/saga/mermaid.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/saga/models.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/saga/recovery.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/saga/saga.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/saga/storage/__init__.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/saga/storage/enums.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/saga/storage/memory.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/saga/storage/models.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/saga/storage/protocol.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/saga/storage/sqlalchemy.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/saga/validation.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/serializers/__init__.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/serializers/default.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/cqrs/types.py +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/python_cqrs.egg-info/SOURCES.txt +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/python_cqrs.egg-info/dependency_links.txt +0 -0
- {python_cqrs-4.7.2 → python_cqrs-4.8.0}/src/python_cqrs.egg-info/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-cqrs
|
|
3
|
-
Version: 4.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 4.8.0
|
|
4
|
+
Summary: Event-Driven Architecture Framework for Distributed Systems
|
|
5
5
|
Author-email: Vadim Kozyrevskiy <vadikko2@mail.ru>, Dmitry Kutlubaev <kutlubaev00@mail.ru>
|
|
6
6
|
Maintainer-email: Vadim Kozyrevskiy <vadikko2@mail.ru>
|
|
7
7
|
Project-URL: Documentation, https://mkdocs.python-cqrs.dev/
|
|
@@ -17,14 +17,14 @@ Requires-Python: >=3.10
|
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
Requires-Dist: dataclass-wizard==0.*
|
|
20
|
-
Requires-Dist: di[anyio]==0
|
|
21
|
-
Requires-Dist: dependency-injector>=4.
|
|
20
|
+
Requires-Dist: di[anyio]==0.*
|
|
21
|
+
Requires-Dist: dependency-injector>=4.0
|
|
22
22
|
Requires-Dist: orjson==3.*
|
|
23
23
|
Requires-Dist: pydantic==2.*
|
|
24
|
-
Requires-Dist: python-dotenv==1
|
|
25
|
-
Requires-Dist: retry-async==0.1
|
|
24
|
+
Requires-Dist: python-dotenv==1.*
|
|
25
|
+
Requires-Dist: retry-async==0.1.*
|
|
26
26
|
Requires-Dist: sqlalchemy[asyncio]==2.0.*
|
|
27
|
-
Requires-Dist: typing-extensions>=4.0
|
|
27
|
+
Requires-Dist: typing-extensions>=4.0
|
|
28
28
|
Provides-Extra: aiobreaker
|
|
29
29
|
Requires-Dist: aiobreaker>=0.3.0; extra == "aiobreaker"
|
|
30
30
|
Provides-Extra: dev
|
|
@@ -51,10 +51,9 @@ Requires-Dist: faststream[kafka]==0.5.28; extra == "examples"
|
|
|
51
51
|
Requires-Dist: faker>=37.12.0; extra == "examples"
|
|
52
52
|
Requires-Dist: uvicorn==0.32.0; extra == "examples"
|
|
53
53
|
Requires-Dist: aiohttp==3.13.2; extra == "examples"
|
|
54
|
+
Requires-Dist: protobuf>=4.25.8; extra == "examples"
|
|
54
55
|
Provides-Extra: kafka
|
|
55
56
|
Requires-Dist: aiokafka==0.10.0; extra == "kafka"
|
|
56
|
-
Provides-Extra: protobuf
|
|
57
|
-
Requires-Dist: protobuf==4.25.5; extra == "protobuf"
|
|
58
57
|
Provides-Extra: rabbit
|
|
59
58
|
Requires-Dist: aio-pika==9.3.0; extra == "rabbit"
|
|
60
59
|
Dynamic: license-file
|
|
@@ -17,21 +17,21 @@ classifiers = [
|
|
|
17
17
|
]
|
|
18
18
|
dependencies = [
|
|
19
19
|
"dataclass-wizard==0.*",
|
|
20
|
-
"di[anyio]==0
|
|
21
|
-
"dependency-injector>=4.
|
|
20
|
+
"di[anyio]==0.*",
|
|
21
|
+
"dependency-injector>=4.0",
|
|
22
22
|
"orjson==3.*",
|
|
23
23
|
"pydantic==2.*",
|
|
24
|
-
"python-dotenv==1
|
|
25
|
-
"retry-async==0.1
|
|
24
|
+
"python-dotenv==1.*",
|
|
25
|
+
"retry-async==0.1.*",
|
|
26
26
|
"sqlalchemy[asyncio]==2.0.*",
|
|
27
|
-
"typing-extensions>=4.0
|
|
27
|
+
"typing-extensions>=4.0"
|
|
28
28
|
]
|
|
29
|
-
description = "
|
|
29
|
+
description = "Event-Driven Architecture Framework for Distributed Systems"
|
|
30
30
|
maintainers = [{name = "Vadim Kozyrevskiy", email = "vadikko2@mail.ru"}]
|
|
31
31
|
name = "python-cqrs"
|
|
32
32
|
readme = "README.md"
|
|
33
33
|
requires-python = ">=3.10"
|
|
34
|
-
version = "4.
|
|
34
|
+
version = "4.8.0"
|
|
35
35
|
|
|
36
36
|
[project.optional-dependencies]
|
|
37
37
|
aiobreaker = ["aiobreaker>=0.3.0"]
|
|
@@ -62,10 +62,10 @@ examples = [
|
|
|
62
62
|
"faststream[kafka]==0.5.28",
|
|
63
63
|
"faker>=37.12.0",
|
|
64
64
|
"uvicorn==0.32.0",
|
|
65
|
-
"aiohttp==3.13.2"
|
|
65
|
+
"aiohttp==3.13.2",
|
|
66
|
+
"protobuf>=4.25.8",
|
|
66
67
|
]
|
|
67
68
|
kafka = ["aiokafka==0.10.0"]
|
|
68
|
-
protobuf = ["protobuf==4.25.5"]
|
|
69
69
|
rabbit = ["aio-pika==9.3.0"]
|
|
70
70
|
|
|
71
71
|
[project.urls]
|
|
@@ -79,8 +79,5 @@ asyncio_mode = "auto"
|
|
|
79
79
|
junit_family = "xunit1"
|
|
80
80
|
testpaths = ["tests"]
|
|
81
81
|
|
|
82
|
-
[tool.ruff]
|
|
83
|
-
target-version = "py310"
|
|
84
|
-
|
|
85
82
|
[tool.setuptools.packages.find]
|
|
86
83
|
where = ["src"]
|
|
@@ -27,9 +27,11 @@ class EventDispatcher:
|
|
|
27
27
|
self,
|
|
28
28
|
event: IEvent,
|
|
29
29
|
handle_type: typing.Type[_EventHandler],
|
|
30
|
-
):
|
|
30
|
+
) -> None:
|
|
31
31
|
handler: _EventHandler = await self._container.resolve(handle_type)
|
|
32
32
|
await handler.handle(event)
|
|
33
|
+
for follow_up in handler.events:
|
|
34
|
+
await self.dispatch(follow_up)
|
|
33
35
|
|
|
34
36
|
async def dispatch(self, event: IEvent) -> None:
|
|
35
37
|
handler_types = self._event_map.get(type(event), [])
|
|
@@ -38,5 +40,6 @@ class EventDispatcher:
|
|
|
38
40
|
"Handlers for event %s not found",
|
|
39
41
|
type(event).__name__,
|
|
40
42
|
)
|
|
43
|
+
return
|
|
41
44
|
for h_type in handler_types:
|
|
42
45
|
await self._handle_event(event, h_type)
|
|
@@ -53,7 +53,7 @@ class SagaDispatcher:
|
|
|
53
53
|
self._compensation_retry_delay = compensation_retry_delay
|
|
54
54
|
self._compensation_retry_backoff = compensation_retry_backoff
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
def dispatch(
|
|
57
57
|
self,
|
|
58
58
|
context: SagaContext,
|
|
59
59
|
saga_id: uuid.UUID | None = None,
|
|
@@ -61,6 +61,7 @@ class SagaDispatcher:
|
|
|
61
61
|
"""
|
|
62
62
|
Dispatch a saga execution for the given context.
|
|
63
63
|
|
|
64
|
+
Called without await; returns an AsyncIterator consumed with async for.
|
|
64
65
|
Yields result after each step execution. After each yield, events are collected
|
|
65
66
|
and included in the dispatch result.
|
|
66
67
|
|
|
@@ -75,6 +76,13 @@ class SagaDispatcher:
|
|
|
75
76
|
Raises:
|
|
76
77
|
SagaDoesNotExist: If no saga is registered for the context type
|
|
77
78
|
"""
|
|
79
|
+
return self._dispatch_impl(context, saga_id=saga_id)
|
|
80
|
+
|
|
81
|
+
async def _dispatch_impl(
|
|
82
|
+
self,
|
|
83
|
+
context: SagaContext,
|
|
84
|
+
saga_id: uuid.UUID | None = None,
|
|
85
|
+
) -> typing.AsyncIterator[SagaDispatchResult]:
|
|
78
86
|
# Find saga type by context type
|
|
79
87
|
saga_type = self._saga_map.get(type(context))
|
|
80
88
|
if not saga_type:
|
|
@@ -28,16 +28,23 @@ class StreamingRequestDispatcher:
|
|
|
28
28
|
self._container = container
|
|
29
29
|
self._middleware_chain = middleware_chain or MiddlewareChain()
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
def dispatch(
|
|
32
32
|
self,
|
|
33
33
|
request: IRequest,
|
|
34
34
|
) -> typing.AsyncIterator[RequestDispatchResult]:
|
|
35
35
|
"""
|
|
36
36
|
Dispatch a request to a streaming handler and yield results.
|
|
37
37
|
|
|
38
|
+
Called without await; returns an AsyncIterator consumed with async for.
|
|
38
39
|
After each yield from the handler, events are collected and included
|
|
39
40
|
in the dispatch result. The generator continues until StopIteration.
|
|
40
41
|
"""
|
|
42
|
+
return self._dispatch_impl(request)
|
|
43
|
+
|
|
44
|
+
async def _dispatch_impl(
|
|
45
|
+
self,
|
|
46
|
+
request: IRequest,
|
|
47
|
+
) -> typing.AsyncIterator[RequestDispatchResult]:
|
|
41
48
|
handler_type = self._request_map.get(type(request), None)
|
|
42
49
|
if handler_type is None:
|
|
43
50
|
raise RequestHandlerDoesNotExist(
|
|
@@ -62,9 +69,7 @@ class StreamingRequestDispatcher:
|
|
|
62
69
|
|
|
63
70
|
if not inspect.isasyncgenfunction(handler.handle):
|
|
64
71
|
handler_name = (
|
|
65
|
-
handler_type_typed.__name__
|
|
66
|
-
if hasattr(handler_type_typed, "__name__")
|
|
67
|
-
else str(handler_type_typed)
|
|
72
|
+
handler_type_typed.__name__ if hasattr(handler_type_typed, "__name__") else str(handler_type_typed)
|
|
68
73
|
)
|
|
69
74
|
raise TypeError(
|
|
70
75
|
f"Handler {handler_name}.handle must be an async generator function",
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
"""Event types, handlers, emitter, and event map for the CQRS events layer.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
- Event types: :class:`Event`, :class:`DomainEvent`, :class:`NotificationEvent`,
|
|
5
|
+
and their interfaces/base classes.
|
|
6
|
+
- :class:`EventHandler` — handler interface; implement :meth:`EventHandler.handle`
|
|
7
|
+
and optionally :attr:`EventHandler.events` for follow-up events.
|
|
8
|
+
- :class:`EventEmitter` — sends domain events to handlers and notification events
|
|
9
|
+
to a message broker.
|
|
10
|
+
- :class:`EventMap` — registry of event type -> handler types; use :meth:`EventMap.bind`.
|
|
11
|
+
"""
|
|
12
|
+
|
|
1
13
|
from cqrs.events.event import (
|
|
2
14
|
DCEvent,
|
|
3
15
|
DCDomainEvent,
|
|
@@ -31,6 +31,31 @@ def setup_mediator(
|
|
|
31
31
|
middlewares: typing.Iterable[mediator_middlewares.Middleware],
|
|
32
32
|
events_mapper: typing.Callable[[events.EventMap], None] | None = None,
|
|
33
33
|
) -> cqrs.EventMediator:
|
|
34
|
+
"""
|
|
35
|
+
Create an event mediator with the given container and middlewares.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
container: DI container (e.g. :class:`cqrs.container.di.DIContainer`) or
|
|
39
|
+
any implementation of :class:`cqrs.container.protocol.Container`.
|
|
40
|
+
middlewares: Middleware chain for the mediator (e.g. logging).
|
|
41
|
+
events_mapper: Optional callable that receives an :class:`~cqrs.events.map.EventMap`
|
|
42
|
+
and binds event types to handler types via :meth:`~cqrs.events.map.EventMap.bind`.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Configured :class:`cqrs.EventMediator` instance.
|
|
46
|
+
|
|
47
|
+
Example::
|
|
48
|
+
|
|
49
|
+
def bind_events(event_map: events.EventMap) -> None:
|
|
50
|
+
event_map.bind(OrderCreatedEvent, OrderCreatedEventHandler)
|
|
51
|
+
|
|
52
|
+
mediator = setup_mediator(
|
|
53
|
+
container=di_container,
|
|
54
|
+
middlewares=[logging_middleware.LoggingMiddleware()],
|
|
55
|
+
events_mapper=bind_events,
|
|
56
|
+
)
|
|
57
|
+
await mediator.emit(OrderCreatedEvent(order_id="1"))
|
|
58
|
+
"""
|
|
34
59
|
_events_mapper = events.EventMap()
|
|
35
60
|
if events_mapper is not None:
|
|
36
61
|
events_mapper(_events_mapper)
|
|
@@ -71,6 +96,34 @@ def bootstrap(
|
|
|
71
96
|
events_mapper: typing.Callable[[events.EventMap], None] | None = None,
|
|
72
97
|
on_startup: typing.List[typing.Callable[[], None]] | None = None,
|
|
73
98
|
) -> cqrs.EventMediator:
|
|
99
|
+
"""
|
|
100
|
+
Bootstrap an event mediator with optional middlewares and event bindings.
|
|
101
|
+
|
|
102
|
+
If ``di_container`` is a :class:`di.Container`, it is wrapped in
|
|
103
|
+
:class:`cqrs.container.di.DIContainer`. Logging middleware is appended
|
|
104
|
+
to the middleware list. Runs all ``on_startup`` callables before setup.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
di_container: DI container from the ``di`` package or a CQRS container.
|
|
108
|
+
middlewares: Optional list of middlewares (e.g. logging, metrics).
|
|
109
|
+
events_mapper: Optional callable that receives an :class:`~cqrs.events.map.EventMap`
|
|
110
|
+
and binds event types to handler types.
|
|
111
|
+
on_startup: Optional list of callables to run before creating the mediator.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Configured :class:`cqrs.EventMediator` with logging middleware enabled.
|
|
115
|
+
|
|
116
|
+
Example::
|
|
117
|
+
|
|
118
|
+
def bind_events(event_map: events.EventMap) -> None:
|
|
119
|
+
event_map.bind(OrderCreatedEvent, OrderCreatedEventHandler)
|
|
120
|
+
|
|
121
|
+
mediator = bootstrap(
|
|
122
|
+
di_container=di.Container(),
|
|
123
|
+
events_mapper=bind_events,
|
|
124
|
+
)
|
|
125
|
+
await mediator.emit(OrderCreatedEvent(order_id="1"))
|
|
126
|
+
"""
|
|
74
127
|
if on_startup is None:
|
|
75
128
|
on_startup = []
|
|
76
129
|
|
|
@@ -90,8 +143,10 @@ def bootstrap(
|
|
|
90
143
|
middlewares_list: typing.List[mediator_middlewares.Middleware] = list(
|
|
91
144
|
middlewares or [],
|
|
92
145
|
)
|
|
146
|
+
if not any(isinstance(m, logging_middleware.LoggingMiddleware) for m in middlewares_list):
|
|
147
|
+
middlewares_list.append(logging_middleware.LoggingMiddleware())
|
|
93
148
|
return setup_mediator(
|
|
94
149
|
container,
|
|
95
150
|
events_mapper=events_mapper,
|
|
96
|
-
middlewares=middlewares_list
|
|
151
|
+
middlewares=middlewares_list,
|
|
97
152
|
)
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import abc
|
|
2
2
|
import dataclasses
|
|
3
|
-
from dataclass_wizard import fromdict, asdict
|
|
4
3
|
import datetime
|
|
5
4
|
import os
|
|
5
|
+
import sys
|
|
6
6
|
import typing
|
|
7
7
|
import uuid
|
|
8
|
-
import sys
|
|
9
8
|
|
|
10
9
|
import dotenv
|
|
11
10
|
import pydantic
|
|
11
|
+
from dataclass_wizard import asdict, fromdict
|
|
12
12
|
|
|
13
13
|
if sys.version_info >= (3, 11):
|
|
14
14
|
from typing import Self # novm
|
|
@@ -251,6 +251,9 @@ class INotificationEvent(IEvent, typing.Generic[PayloadT]):
|
|
|
251
251
|
|
|
252
252
|
def proto(self) -> typing.Any: ... # Method for protobuf representation
|
|
253
253
|
|
|
254
|
+
@classmethod
|
|
255
|
+
def from_proto(cls, proto: typing.Any) -> Self: ...
|
|
256
|
+
|
|
254
257
|
|
|
255
258
|
@dataclasses.dataclass(frozen=True)
|
|
256
259
|
class DCNotificationEvent(
|
|
@@ -300,7 +303,18 @@ class DCNotificationEvent(
|
|
|
300
303
|
NotImplementedError: This method must be implemented by subclasses
|
|
301
304
|
that need protobuf serialization.
|
|
302
305
|
"""
|
|
303
|
-
raise NotImplementedError("Method not implemented
|
|
306
|
+
raise NotImplementedError("Method not implemented")
|
|
307
|
+
|
|
308
|
+
@classmethod
|
|
309
|
+
def from_proto(cls, proto: typing.Any) -> Self:
|
|
310
|
+
"""
|
|
311
|
+
Constructs event from proto event object
|
|
312
|
+
|
|
313
|
+
Raises:
|
|
314
|
+
NotImplementedError: This method must be implemented by subclasses
|
|
315
|
+
that need protobuf deserialization.
|
|
316
|
+
"""
|
|
317
|
+
raise NotImplementedError("Method not implemented")
|
|
304
318
|
|
|
305
319
|
def __hash__(self) -> int:
|
|
306
320
|
"""
|
|
@@ -345,10 +359,34 @@ class PydanticNotificationEvent(
|
|
|
345
359
|
|
|
346
360
|
model_config = pydantic.ConfigDict(from_attributes=True)
|
|
347
361
|
|
|
348
|
-
def proto(self):
|
|
362
|
+
def proto(self) -> typing.Any:
|
|
363
|
+
"""
|
|
364
|
+
Return protobuf representation of the event.
|
|
365
|
+
|
|
366
|
+
Raises:
|
|
367
|
+
NotImplementedError: This method must be implemented by subclasses
|
|
368
|
+
that need protobuf serialization.
|
|
369
|
+
"""
|
|
349
370
|
raise NotImplementedError("Method not implemented")
|
|
350
371
|
|
|
351
|
-
|
|
372
|
+
@classmethod
|
|
373
|
+
def from_proto(cls, proto: typing.Any) -> Self:
|
|
374
|
+
"""
|
|
375
|
+
Constructs event from proto event object
|
|
376
|
+
|
|
377
|
+
Raises:
|
|
378
|
+
NotImplementedError: This method must be implemented by subclasses
|
|
379
|
+
that need protobuf deserialization.
|
|
380
|
+
"""
|
|
381
|
+
raise NotImplementedError("Method not implemented")
|
|
382
|
+
|
|
383
|
+
def __hash__(self) -> int:
|
|
384
|
+
"""
|
|
385
|
+
Return the hash of the event based on its event_id.
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
Hash value of the event_id.
|
|
389
|
+
"""
|
|
352
390
|
return hash(self.event_id)
|
|
353
391
|
|
|
354
392
|
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import logging
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from cqrs import container as di_container, message_brokers
|
|
6
|
+
from cqrs.events.event import IDomainEvent, IEvent, INotificationEvent
|
|
7
|
+
from cqrs.events import event_handler, map
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("cqrs")
|
|
10
|
+
|
|
11
|
+
_H: typing.TypeAlias = event_handler.EventHandler
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EventEmitter:
|
|
15
|
+
"""
|
|
16
|
+
Sends events to registered handlers or to a message broker.
|
|
17
|
+
|
|
18
|
+
For :class:`~cqrs.events.event.IDomainEvent`: resolves handlers from the
|
|
19
|
+
container, runs :meth:`~cqrs.events.event_handler.EventHandler.handle`, and
|
|
20
|
+
returns follow-up events from :attr:`~cqrs.events.event_handler.EventHandler.events`.
|
|
21
|
+
For :class:`~cqrs.events.event.INotificationEvent`: sends the event to the
|
|
22
|
+
message broker (if configured) and returns an empty sequence.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
event_map: map.EventMap,
|
|
28
|
+
container: di_container.Container,
|
|
29
|
+
message_broker: message_brokers.MessageBroker | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Initialize the event emitter.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
event_map: Map of event types to handler types (used for domain events).
|
|
36
|
+
container: DI container to resolve handler instances.
|
|
37
|
+
message_broker: Optional broker for notification events; required
|
|
38
|
+
when emitting :class:`~cqrs.events.event.INotificationEvent`.
|
|
39
|
+
|
|
40
|
+
Example::
|
|
41
|
+
|
|
42
|
+
event_map = EventMap()
|
|
43
|
+
event_map.bind(OrderCreatedEvent, OrderCreatedEventHandler)
|
|
44
|
+
emitter = EventEmitter(
|
|
45
|
+
event_map=event_map,
|
|
46
|
+
container=di_container,
|
|
47
|
+
message_broker=kafka_broker,
|
|
48
|
+
)
|
|
49
|
+
follow_ups = await emitter.emit(OrderCreatedEvent(order_id="1"))
|
|
50
|
+
"""
|
|
51
|
+
self._event_map = event_map
|
|
52
|
+
self._container = container
|
|
53
|
+
self._message_broker = message_broker
|
|
54
|
+
|
|
55
|
+
@functools.singledispatchmethod
|
|
56
|
+
async def emit(self, event: IEvent) -> typing.Sequence[IEvent]:
|
|
57
|
+
"""
|
|
58
|
+
Emit an event and return follow-up events from handlers.
|
|
59
|
+
|
|
60
|
+
For unknown event types returns an empty sequence. For domain events
|
|
61
|
+
invokes all registered handlers and collects events from
|
|
62
|
+
:attr:`~cqrs.events.event_handler.EventHandler.events`. For notification
|
|
63
|
+
events sends to the message broker.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
event: The event to emit (domain or notification).
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Follow-up events returned by domain event handlers; empty for
|
|
70
|
+
notification events or when no handlers are registered.
|
|
71
|
+
|
|
72
|
+
Example::
|
|
73
|
+
|
|
74
|
+
follow_ups = await emitter.emit(OrderCreatedEvent(order_id="1"))
|
|
75
|
+
for e in follow_ups:
|
|
76
|
+
await emitter.emit(e) # or process via EventProcessor
|
|
77
|
+
"""
|
|
78
|
+
return ()
|
|
79
|
+
|
|
80
|
+
async def _send_to_broker(
|
|
81
|
+
self,
|
|
82
|
+
event: INotificationEvent,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Send a notification event to the message broker.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
event: Notification event to send.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
RuntimeError: If no message broker was configured.
|
|
92
|
+
"""
|
|
93
|
+
if not self._message_broker:
|
|
94
|
+
raise RuntimeError(
|
|
95
|
+
f"To send event {event}, message broker must be specified.",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
message = message_brokers.Message(
|
|
99
|
+
message_name=type(event).__name__,
|
|
100
|
+
message_id=event.event_id,
|
|
101
|
+
topic=event.topic,
|
|
102
|
+
payload=event.to_dict(),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
logger.debug(
|
|
106
|
+
"Sending Event(%s) to message broker %s",
|
|
107
|
+
event.event_id,
|
|
108
|
+
type(self._message_broker).__name__,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
await self._message_broker.send_message(message)
|
|
112
|
+
|
|
113
|
+
@emit.register(IDomainEvent)
|
|
114
|
+
async def _(self, event: IDomainEvent) -> typing.Sequence[IEvent]:
|
|
115
|
+
"""Emit domain event: run all registered handlers and return their follow-up events."""
|
|
116
|
+
handlers_types = self._event_map.get(type(event), [])
|
|
117
|
+
if not handlers_types:
|
|
118
|
+
logger.warning(
|
|
119
|
+
"Handlers for domain event %s not found",
|
|
120
|
+
type(event).__name__,
|
|
121
|
+
)
|
|
122
|
+
return ()
|
|
123
|
+
follow_ups: list[IEvent] = []
|
|
124
|
+
for handler_type in handlers_types:
|
|
125
|
+
handler: _H = await self._container.resolve(
|
|
126
|
+
handler_type,
|
|
127
|
+
)
|
|
128
|
+
logger.debug(
|
|
129
|
+
"Handling Event(%s) via event handler(%s)",
|
|
130
|
+
type(event).__name__,
|
|
131
|
+
handler_type.__name__,
|
|
132
|
+
)
|
|
133
|
+
await handler.handle(event)
|
|
134
|
+
# Snapshot follow-ups so shared handlers don't expose stale state
|
|
135
|
+
follow_ups.extend(list(handler.events))
|
|
136
|
+
return follow_ups
|
|
137
|
+
|
|
138
|
+
@emit.register(INotificationEvent)
|
|
139
|
+
async def _(self, event: INotificationEvent) -> typing.Sequence[IEvent]:
|
|
140
|
+
"""Emit notification event: send to message broker; no follow-ups."""
|
|
141
|
+
await self._send_to_broker(event)
|
|
142
|
+
return ()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from cqrs.events.event import IEvent
|
|
6
|
+
|
|
7
|
+
E = typing.TypeVar("E", bound=IEvent, contravariant=True)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EventHandler(abc.ABC, typing.Generic[E]):
|
|
11
|
+
"""
|
|
12
|
+
The event handler interface.
|
|
13
|
+
|
|
14
|
+
Subclasses must implement :meth:`handle`. Optionally override :attr:`events`
|
|
15
|
+
to return follow-up events emitted after handling (e.g. for multi-level
|
|
16
|
+
event chains).
|
|
17
|
+
|
|
18
|
+
Example::
|
|
19
|
+
|
|
20
|
+
class UserJoinedEvent(DomainEvent):
|
|
21
|
+
meeting_id: str
|
|
22
|
+
user_id: str
|
|
23
|
+
|
|
24
|
+
class UserJoinedEventHandler(EventHandler[UserJoinedEvent]):
|
|
25
|
+
def __init__(self, meetings_api: MeetingAPIProtocol) -> None:
|
|
26
|
+
self._meetings_api = meetings_api
|
|
27
|
+
|
|
28
|
+
async def handle(self, event: UserJoinedEvent) -> None:
|
|
29
|
+
await self._meetings_api.notify_room(
|
|
30
|
+
event.meeting_id, "New user joined!"
|
|
31
|
+
)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def events(self) -> Sequence[IEvent]:
|
|
36
|
+
"""
|
|
37
|
+
Events produced by this handler after :meth:`handle` was called.
|
|
38
|
+
|
|
39
|
+
Override in subclasses to return follow-up events that should be
|
|
40
|
+
processed by the same pipeline (e.g. domain events to emit). By default
|
|
41
|
+
returns an empty sequence.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Sequence of follow-up events (e.g. new domain events) to process.
|
|
45
|
+
"""
|
|
46
|
+
return ()
|
|
47
|
+
|
|
48
|
+
@abc.abstractmethod
|
|
49
|
+
async def handle(self, event: E) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Handle the given event.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
event: The event instance to handle.
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
NotImplementedError: Must be implemented by subclasses.
|
|
58
|
+
"""
|
|
59
|
+
raise NotImplementedError
|