python-cqrs 4.7.3__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.3/src/python_cqrs.egg-info → python_cqrs-4.8.0}/PKG-INFO +8 -9
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/pyproject.toml +9 -9
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/dispatcher/event.py +4 -1
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/events/__init__.py +12 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/events/bootstrap.py +56 -1
- {python_cqrs-4.7.3 → 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.3 → python_cqrs-4.8.0}/src/cqrs/requests/bootstrap.py +6 -2
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/requests/cor_request_handler.py +7 -2
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/requests/request_handler.py +14 -4
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/saga/bootstrap.py +3 -1
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/saga/step.py +4 -3
- {python_cqrs-4.7.3 → python_cqrs-4.8.0/src/python_cqrs.egg-info}/PKG-INFO +8 -9
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/python_cqrs.egg-info/requires.txt +6 -8
- python_cqrs-4.7.3/src/cqrs/events/event_emitter.py +0 -82
- python_cqrs-4.7.3/src/cqrs/events/event_handler.py +0 -26
- python_cqrs-4.7.3/src/cqrs/events/event_processor.py +0 -66
- python_cqrs-4.7.3/src/cqrs/events/map.py +0 -29
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/LICENSE +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/README.md +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/setup.cfg +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/__init__.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/adapters/__init__.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/adapters/amqp.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/adapters/circuit_breaker.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/adapters/kafka.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/adapters/protocol.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/compressors/__init__.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/compressors/protocol.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/compressors/zlib.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/container/__init__.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/container/dependency_injector.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/container/di.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/container/protocol.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/deserializers/__init__.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/deserializers/exceptions.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/deserializers/json.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/dispatcher/__init__.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/dispatcher/exceptions.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/dispatcher/models.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/dispatcher/request.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/dispatcher/saga.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/dispatcher/streaming.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/mediator.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/message_brokers/__init__.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/message_brokers/amqp.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/message_brokers/devnull.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/message_brokers/kafka.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/message_brokers/protocol.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/middlewares/__init__.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/middlewares/base.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/middlewares/logging.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/outbox/__init__.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/outbox/map.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/outbox/mock.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/outbox/repository.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/outbox/sqlalchemy.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/producer.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/requests/__init__.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/requests/map.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/requests/mermaid.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/requests/request.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/response.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/saga/__init__.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/saga/circuit_breaker.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/saga/compensation.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/saga/execution.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/saga/fallback.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/saga/mermaid.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/saga/models.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/saga/recovery.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/saga/saga.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/saga/storage/__init__.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/saga/storage/enums.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/saga/storage/memory.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/saga/storage/models.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/saga/storage/protocol.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/saga/storage/sqlalchemy.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/saga/validation.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/serializers/__init__.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/serializers/default.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/cqrs/types.py +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/python_cqrs.egg-info/SOURCES.txt +0 -0
- {python_cqrs-4.7.3 → python_cqrs-4.8.0}/src/python_cqrs.egg-info/dependency_links.txt +0 -0
- {python_cqrs-4.7.3 → 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]
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import typing
|
|
3
|
+
from collections import deque
|
|
4
|
+
|
|
5
|
+
from cqrs.events.event import IEvent
|
|
6
|
+
from cqrs.events.event_emitter import EventEmitter
|
|
7
|
+
from cqrs.events.map import EventMap
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EventProcessor:
|
|
11
|
+
"""
|
|
12
|
+
Processes events in parallel or sequentially via an event emitter.
|
|
13
|
+
|
|
14
|
+
Emits events through the configured :class:`~cqrs.events.event_emitter.EventEmitter`.
|
|
15
|
+
Follow-up events returned by handlers (via :attr:`~cqrs.events.event_handler.EventHandler.events`)
|
|
16
|
+
are processed in the same pipeline: BFS in sequential mode, under the same
|
|
17
|
+
semaphore in parallel mode. Can be reused across different mediators.
|
|
18
|
+
|
|
19
|
+
Example::
|
|
20
|
+
|
|
21
|
+
event_map = EventMap()
|
|
22
|
+
event_map.bind(OrderCreatedEvent, OrderCreatedEventHandler)
|
|
23
|
+
emitter = EventEmitter(event_map=event_map, container=container)
|
|
24
|
+
processor = EventProcessor(
|
|
25
|
+
event_map=event_map,
|
|
26
|
+
event_emitter=emitter,
|
|
27
|
+
max_concurrent_event_handlers=4,
|
|
28
|
+
concurrent_event_handle_enable=True,
|
|
29
|
+
)
|
|
30
|
+
await processor.emit_events([OrderCreatedEvent(order_id="1")])
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
event_map: EventMap,
|
|
36
|
+
event_emitter: EventEmitter | None = None,
|
|
37
|
+
max_concurrent_event_handlers: int = 1,
|
|
38
|
+
concurrent_event_handle_enable: bool = True,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Initialize the event processor.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
event_map: Map of event types to handler types.
|
|
45
|
+
event_emitter: Emitter used to publish events; if None, :meth:`emit_events`
|
|
46
|
+
is a no-op.
|
|
47
|
+
max_concurrent_event_handlers: Semaphore limit for parallel mode.
|
|
48
|
+
concurrent_event_handle_enable: If True, process events in parallel
|
|
49
|
+
(with semaphore); if False, process sequentially (BFS over events
|
|
50
|
+
and follow-ups).
|
|
51
|
+
"""
|
|
52
|
+
self._event_emitter = event_emitter
|
|
53
|
+
self._event_map = event_map
|
|
54
|
+
self._max_concurrent_event_handlers = max_concurrent_event_handlers
|
|
55
|
+
self._concurrent_event_handle_enable = concurrent_event_handle_enable
|
|
56
|
+
self._event_semaphore = asyncio.Semaphore(max_concurrent_event_handlers)
|
|
57
|
+
|
|
58
|
+
async def emit_events(self, events: typing.Sequence[IEvent]) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Emit all events and process follow-ups in the same pipeline.
|
|
61
|
+
|
|
62
|
+
In sequential mode, events and follow-ups are processed in BFS order.
|
|
63
|
+
In parallel mode, events are processed under the same semaphore limit;
|
|
64
|
+
as soon as any event completes, its follow-ups are queued and started
|
|
65
|
+
(FIRST_COMPLETED), without waiting for siblings. Returns when all work
|
|
66
|
+
is finished.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
events: Events to emit (e.g. domain events). Handlers may return
|
|
70
|
+
follow-up events via :attr:`~cqrs.events.event_handler.EventHandler.events`.
|
|
71
|
+
|
|
72
|
+
Example::
|
|
73
|
+
|
|
74
|
+
await processor.emit_events([
|
|
75
|
+
OrderCreatedEvent(order_id="1"),
|
|
76
|
+
OrderCreatedEvent(order_id="2"),
|
|
77
|
+
])
|
|
78
|
+
"""
|
|
79
|
+
if not events:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
if not self._event_emitter:
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
if not self._concurrent_event_handle_enable:
|
|
86
|
+
# Process events sequentially (BFS: follow-ups re-queued, O(1) popleft)
|
|
87
|
+
to_process: deque[IEvent] = deque(events)
|
|
88
|
+
while to_process:
|
|
89
|
+
event = to_process.popleft()
|
|
90
|
+
follow_ups = await self._event_emitter.emit(event)
|
|
91
|
+
to_process.extend(follow_ups)
|
|
92
|
+
else:
|
|
93
|
+
# Process events in parallel: start follow-ups as soon as any task completes
|
|
94
|
+
# (FIRST_COMPLETED), all under the same semaphore
|
|
95
|
+
await self._emit_events_parallel_first_completed(deque(events))
|
|
96
|
+
|
|
97
|
+
async def _emit_one_event(self, event: IEvent) -> typing.Sequence[IEvent]:
|
|
98
|
+
"""
|
|
99
|
+
Emit one event under the semaphore. Returns follow-up events from the handler.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
event: The event to emit.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Follow-up events to process next, or empty sequence.
|
|
106
|
+
"""
|
|
107
|
+
if not self._event_emitter:
|
|
108
|
+
return ()
|
|
109
|
+
async with self._event_semaphore:
|
|
110
|
+
follow_ups = await self._event_emitter.emit(event)
|
|
111
|
+
if follow_ups is None:
|
|
112
|
+
return ()
|
|
113
|
+
return follow_ups
|
|
114
|
+
|
|
115
|
+
async def _emit_events_parallel_first_completed(
|
|
116
|
+
self,
|
|
117
|
+
initial_events: deque[IEvent],
|
|
118
|
+
) -> None:
|
|
119
|
+
"""
|
|
120
|
+
Process events in parallel under the semaphore; as soon as any task completes,
|
|
121
|
+
its follow-up events are queued and started, without waiting for siblings.
|
|
122
|
+
Uses deque for O(1) popleft when taking the next event.
|
|
123
|
+
"""
|
|
124
|
+
pending_events: deque[IEvent] = initial_events
|
|
125
|
+
running_tasks: set[asyncio.Task[typing.Sequence[IEvent]]] = set()
|
|
126
|
+
|
|
127
|
+
while pending_events or running_tasks:
|
|
128
|
+
# Start a task for each pending event (semaphore limits concurrency)
|
|
129
|
+
while pending_events:
|
|
130
|
+
event = pending_events.popleft()
|
|
131
|
+
task = asyncio.create_task(self._emit_one_event(event))
|
|
132
|
+
running_tasks.add(task)
|
|
133
|
+
|
|
134
|
+
if not running_tasks:
|
|
135
|
+
break
|
|
136
|
+
|
|
137
|
+
done, running_tasks = await asyncio.wait(
|
|
138
|
+
running_tasks,
|
|
139
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
140
|
+
)
|
|
141
|
+
for task in done:
|
|
142
|
+
follow_ups = task.result()
|
|
143
|
+
pending_events.extend(follow_ups)
|