python-cqrs 4.9.0__tar.gz → 4.10.1__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.9.0 → python_cqrs-4.10.1}/PKG-INFO +390 -220
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/README.md +386 -218
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/pyproject.toml +3 -2
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/__init__.py +6 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/adapters/circuit_breaker.py +32 -44
- python_cqrs-4.10.1/src/cqrs/circuit_breaker.py +77 -0
- python_cqrs-4.10.1/src/cqrs/dispatcher/event.py +96 -0
- python_cqrs-4.10.1/src/cqrs/dispatcher/request.py +139 -0
- python_cqrs-4.10.1/src/cqrs/dispatcher/streaming.py +146 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/events/__init__.py +3 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/events/event_emitter.py +57 -5
- python_cqrs-4.10.1/src/cqrs/events/fallback.py +92 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/events/map.py +10 -11
- python_cqrs-4.10.1/src/cqrs/generic_utils.py +43 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/middlewares/base.py +1 -1
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/outbox/sqlalchemy.py +64 -21
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/requests/cor_request_handler.py +1 -1
- python_cqrs-4.10.1/src/cqrs/requests/fallback.py +98 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/requests/map.py +6 -1
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/requests/request.py +10 -1
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/requests/request_handler.py +1 -1
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/saga/execution.py +3 -3
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/saga/fallback.py +3 -3
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/saga/storage/sqlalchemy.py +13 -6
- python_cqrs-4.10.1/src/cqrs/types.py +11 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/python_cqrs.egg-info/PKG-INFO +390 -220
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/python_cqrs.egg-info/SOURCES.txt +4 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/python_cqrs.egg-info/requires.txt +4 -1
- python_cqrs-4.9.0/src/cqrs/dispatcher/event.py +0 -45
- python_cqrs-4.9.0/src/cqrs/dispatcher/request.py +0 -78
- python_cqrs-4.9.0/src/cqrs/dispatcher/streaming.py +0 -85
- python_cqrs-4.9.0/src/cqrs/types.py +0 -18
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/LICENSE +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/setup.cfg +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/adapters/__init__.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/adapters/amqp.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/adapters/kafka.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/adapters/protocol.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/compressors/__init__.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/compressors/protocol.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/compressors/zlib.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/container/__init__.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/container/dependency_injector.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/container/di.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/container/protocol.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/deserializers/__init__.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/deserializers/exceptions.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/deserializers/json.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/dispatcher/__init__.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/dispatcher/exceptions.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/dispatcher/models.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/dispatcher/saga.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/events/bootstrap.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/events/event.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/events/event_handler.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/events/event_processor.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/mediator.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/message_brokers/__init__.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/message_brokers/amqp.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/message_brokers/devnull.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/message_brokers/kafka.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/message_brokers/protocol.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/middlewares/__init__.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/middlewares/logging.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/outbox/__init__.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/outbox/map.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/outbox/mock.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/outbox/repository.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/producer.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/requests/__init__.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/requests/bootstrap.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/requests/mermaid.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/response.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/saga/__init__.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/saga/bootstrap.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/saga/circuit_breaker.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/saga/compensation.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/saga/mermaid.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/saga/models.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/saga/recovery.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/saga/saga.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/saga/step.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/saga/storage/__init__.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/saga/storage/enums.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/saga/storage/memory.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/saga/storage/models.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/saga/storage/protocol.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/saga/validation.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/serializers/__init__.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/cqrs/serializers/default.py +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/python_cqrs.egg-info/dependency_links.txt +0 -0
- {python_cqrs-4.9.0 → python_cqrs-4.10.1}/src/python_cqrs.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-cqrs
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.10.1
|
|
4
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>
|
|
@@ -21,9 +21,9 @@ Requires-Dist: di[anyio]==0.*
|
|
|
21
21
|
Requires-Dist: dependency-injector>=4.0
|
|
22
22
|
Requires-Dist: orjson==3.*
|
|
23
23
|
Requires-Dist: pydantic==2.*
|
|
24
|
+
Requires-Dist: sqlalchemy[asyncio]==2.0.*
|
|
24
25
|
Requires-Dist: python-dotenv==1.*
|
|
25
26
|
Requires-Dist: retry-async==0.1.*
|
|
26
|
-
Requires-Dist: sqlalchemy[asyncio]==2.0.*
|
|
27
27
|
Requires-Dist: typing-extensions>=4.0
|
|
28
28
|
Provides-Extra: aiobreaker
|
|
29
29
|
Requires-Dist: aiobreaker>=0.3.0; extra == "aiobreaker"
|
|
@@ -57,18 +57,21 @@ Provides-Extra: kafka
|
|
|
57
57
|
Requires-Dist: aiokafka==0.10.0; extra == "kafka"
|
|
58
58
|
Provides-Extra: rabbit
|
|
59
59
|
Requires-Dist: aio-pika==9.3.0; extra == "rabbit"
|
|
60
|
+
Provides-Extra: sqlalchemy
|
|
61
|
+
Requires-Dist: sqlalchemy[asyncio]==2.0.*; extra == "sqlalchemy"
|
|
60
62
|
Dynamic: license-file
|
|
61
63
|
|
|
62
|
-
<div align="center">
|
|
63
64
|
<div align="center">
|
|
64
65
|
<img
|
|
65
66
|
src="https://raw.githubusercontent.com/vadikko2/python-cqrs-mkdocs/master/docs/img.png"
|
|
66
67
|
alt="Python CQRS"
|
|
67
|
-
style="max-width: 80%;
|
|
68
|
+
style="max-width: 80%;"
|
|
68
69
|
>
|
|
69
|
-
</div>
|
|
70
70
|
<h1>Python CQRS</h1>
|
|
71
|
-
<
|
|
71
|
+
<p><strong>Event-Driven Architecture Framework for Distributed Systems</strong></p>
|
|
72
|
+
<p>
|
|
73
|
+
<strong>Python 3.10+</strong> · Full documentation: <a href="https://mkdocs.python-cqrs.dev/">mkdocs.python-cqrs.dev</a>
|
|
74
|
+
</p>
|
|
72
75
|
<p>
|
|
73
76
|
<a href="https://pypi.org/project/python-cqrs/">
|
|
74
77
|
<img src="https://img.shields.io/pypi/pyversions/python-cqrs?logo=python&logoColor=white" alt="Python Versions">
|
|
@@ -102,6 +105,29 @@ Dynamic: license-file
|
|
|
102
105
|
>
|
|
103
106
|
> Starting with version 5.0.0, Pydantic support will become optional. The default implementations of `Request`, `Response`, `DomainEvent`, and `NotificationEvent` will be migrated to dataclasses-based implementations.
|
|
104
107
|
|
|
108
|
+
## Table of Contents
|
|
109
|
+
|
|
110
|
+
- [Overview](#overview)
|
|
111
|
+
- [Installation](#installation)
|
|
112
|
+
- [Quick Start](#quick-start)
|
|
113
|
+
- [Request and Response Types](#request-and-response-types)
|
|
114
|
+
- [Request Handlers](#request-handlers)
|
|
115
|
+
- [Mapping](#mapping)
|
|
116
|
+
- [DI container](#di-container)
|
|
117
|
+
- [Bootstrap](#bootstrap)
|
|
118
|
+
- [Saga Pattern](#saga-pattern)
|
|
119
|
+
- [Producing Notification Events](#producing-notification-events)
|
|
120
|
+
- [Kafka broker](#kafka-broker)
|
|
121
|
+
- [Transactional Outbox](#transactional-outbox)
|
|
122
|
+
- [Producing Events from Outbox to Kafka](#producing-events-from-outbox-to-kafka)
|
|
123
|
+
- [Transaction log tailing](#transaction-log-tailing)
|
|
124
|
+
- [Event Handlers](#event-handlers)
|
|
125
|
+
- [Integration with presentation layers](#integration-with-presentation-layers)
|
|
126
|
+
- [Protobuf messaging](#protobuf-messaging)
|
|
127
|
+
- [Contributing](#contributing)
|
|
128
|
+
- [Changelog](#changelog)
|
|
129
|
+
- [License](#license)
|
|
130
|
+
|
|
105
131
|
## Overview
|
|
106
132
|
|
|
107
133
|
An event-driven framework for building distributed systems in Python. It centers on CQRS (Command Query Responsibility Segregation) and extends into messaging, sagas, and reliable event delivery — so you can separate read and write flows, react to events from the bus, run distributed transactions with compensation, and publish events via Transaction Outbox. The result is clearer structure, better scalability, and easier evolution of the application.
|
|
@@ -128,6 +154,104 @@ project ([documentation](https://akhundmurad.github.io/diator/)) with several en
|
|
|
128
154
|
- **Documentation:** Built-in Mermaid diagram generation (Sequence and Class diagrams).
|
|
129
155
|
- **Protobuf:** Interface-level support for converting Notification events to Protobuf and back.
|
|
130
156
|
|
|
157
|
+
## Installation
|
|
158
|
+
|
|
159
|
+
**Python 3.10+** is required.
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
pip install python-cqrs
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Optional dependencies (see [pyproject.toml](https://github.com/vadikko2/python-cqrs/blob/master/pyproject.toml) for full list):
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
pip install python-cqrs[kafka] # Kafka broker (aiokafka)
|
|
169
|
+
pip install python-cqrs[examples] # FastAPI, FastStream, uvicorn, etc.
|
|
170
|
+
pip install python-cqrs[aiobreaker] # Circuit breaker for saga fallbacks
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Quick Start
|
|
174
|
+
|
|
175
|
+
Define a command, a handler, bind them, and run via the mediator:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
import di
|
|
179
|
+
import cqrs
|
|
180
|
+
from cqrs.requests import bootstrap
|
|
181
|
+
|
|
182
|
+
class CreateOrderCommand(cqrs.Request):
|
|
183
|
+
order_id: str
|
|
184
|
+
amount: float
|
|
185
|
+
|
|
186
|
+
class CreateOrderHandler(cqrs.RequestHandler[CreateOrderCommand, None]):
|
|
187
|
+
async def handle(self, request: CreateOrderCommand) -> None:
|
|
188
|
+
print(f"Order {request.order_id}, amount {request.amount}")
|
|
189
|
+
|
|
190
|
+
def commands_mapper(mapper: cqrs.RequestMap) -> None:
|
|
191
|
+
mapper.bind(CreateOrderCommand, CreateOrderHandler)
|
|
192
|
+
|
|
193
|
+
container = di.Container()
|
|
194
|
+
mediator = bootstrap.bootstrap(di_container=container, commands_mapper=commands_mapper)
|
|
195
|
+
await mediator.send(CreateOrderCommand(order_id="ord-1", amount=99.99))
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
For full setup with DI, events, and outbox, see the [documentation](https://mkdocs.python-cqrs.dev/) and the [examples](https://github.com/vadikko2/python-cqrs/tree/master/examples) directory.
|
|
199
|
+
|
|
200
|
+
## Request and Response Types
|
|
201
|
+
|
|
202
|
+
The library supports both Pydantic-based (`PydanticRequest`/`PydanticResponse`, aliased as `Request`/`Response`) and Dataclass-based (`DCRequest`/`DCResponse`) implementations. You can also implement custom classes by implementing the `IRequest`/`IResponse` interfaces directly.
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
import dataclasses
|
|
206
|
+
|
|
207
|
+
# Pydantic-based (default)
|
|
208
|
+
class CreateUserCommand(cqrs.Request):
|
|
209
|
+
username: str
|
|
210
|
+
email: str
|
|
211
|
+
|
|
212
|
+
class UserResponse(cqrs.Response):
|
|
213
|
+
user_id: str
|
|
214
|
+
username: str
|
|
215
|
+
|
|
216
|
+
# Dataclass-based
|
|
217
|
+
@dataclasses.dataclass
|
|
218
|
+
class CreateProductCommand(cqrs.DCRequest):
|
|
219
|
+
name: str
|
|
220
|
+
price: float
|
|
221
|
+
|
|
222
|
+
@dataclasses.dataclass
|
|
223
|
+
class ProductResponse(cqrs.DCResponse):
|
|
224
|
+
product_id: str
|
|
225
|
+
name: str
|
|
226
|
+
|
|
227
|
+
# Custom implementation
|
|
228
|
+
class CustomRequest(cqrs.IRequest):
|
|
229
|
+
def __init__(self, user_id: str, action: str):
|
|
230
|
+
self.user_id = user_id
|
|
231
|
+
self.action = action
|
|
232
|
+
|
|
233
|
+
def to_dict(self) -> dict:
|
|
234
|
+
return {"user_id": self.user_id, "action": self.action}
|
|
235
|
+
|
|
236
|
+
@classmethod
|
|
237
|
+
def from_dict(cls, **kwargs) -> "CustomRequest":
|
|
238
|
+
return cls(user_id=kwargs["user_id"], action=kwargs["action"])
|
|
239
|
+
|
|
240
|
+
class CustomResponse(cqrs.IResponse):
|
|
241
|
+
def __init__(self, result: str, status: int):
|
|
242
|
+
self.result = result
|
|
243
|
+
self.status = status
|
|
244
|
+
|
|
245
|
+
def to_dict(self) -> dict:
|
|
246
|
+
return {"result": self.result, "status": self.status}
|
|
247
|
+
|
|
248
|
+
@classmethod
|
|
249
|
+
def from_dict(cls, **kwargs) -> "CustomResponse":
|
|
250
|
+
return cls(result=kwargs["result"], status=kwargs["status"])
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
A complete example can be found in [request_response_types.py](https://github.com/vadikko2/python-cqrs/blob/master/examples/request_response_types.py)
|
|
254
|
+
|
|
131
255
|
## Request Handlers
|
|
132
256
|
|
|
133
257
|
Request handlers can be divided into two main types:
|
|
@@ -159,7 +283,7 @@ class JoinMeetingCommandHandler(RequestHandler[JoinMeetingCommand, None]):
|
|
|
159
283
|
```
|
|
160
284
|
|
|
161
285
|
A complete example can be found in
|
|
162
|
-
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/request_handler.py)
|
|
286
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/request_handler.py)
|
|
163
287
|
|
|
164
288
|
### Query handler
|
|
165
289
|
|
|
@@ -189,7 +313,7 @@ class ReadMeetingQueryHandler(RequestHandler[ReadMeetingQuery, ReadMeetingQueryR
|
|
|
189
313
|
```
|
|
190
314
|
|
|
191
315
|
A complete example can be found in
|
|
192
|
-
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/request_handler.py)
|
|
316
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/request_handler.py)
|
|
193
317
|
|
|
194
318
|
### Streaming Request Handler
|
|
195
319
|
|
|
@@ -225,7 +349,7 @@ class ProcessFilesCommandHandler(StreamingRequestHandler[ProcessFilesCommand, Fi
|
|
|
225
349
|
```
|
|
226
350
|
|
|
227
351
|
A complete example can be found in
|
|
228
|
-
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/streaming_handler_parallel_events.py)
|
|
352
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/streaming_handler_parallel_events.py)
|
|
229
353
|
|
|
230
354
|
### Chain of Responsibility Request Handler
|
|
231
355
|
|
|
@@ -288,7 +412,7 @@ def payment_mapper(mapper: cqrs.RequestMap) -> None:
|
|
|
288
412
|
```
|
|
289
413
|
|
|
290
414
|
A complete example can be found in
|
|
291
|
-
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/cor_request_handler.py)
|
|
415
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/cor_request_handler.py)
|
|
292
416
|
|
|
293
417
|
#### Mermaid Diagram Generation
|
|
294
418
|
|
|
@@ -308,86 +432,126 @@ sequence_diagram = generator.sequence()
|
|
|
308
432
|
class_diagram = generator.class_diagram()
|
|
309
433
|
```
|
|
310
434
|
|
|
311
|
-
Complete example: [CoR Mermaid Diagrams](https://github.com/vadikko2/cqrs/blob/master/examples/cor_mermaid.py)
|
|
435
|
+
Complete example: [CoR Mermaid Diagrams](https://github.com/vadikko2/python-cqrs/blob/master/examples/cor_mermaid.py)
|
|
312
436
|
|
|
313
|
-
##
|
|
437
|
+
## Mapping
|
|
314
438
|
|
|
315
|
-
|
|
439
|
+
To bind commands, queries and events with specific handlers, you can use the registries `EventMap`, `RequestMap`, and `SagaMap`.
|
|
440
|
+
|
|
441
|
+
**Commands, queries and events:**
|
|
316
442
|
|
|
317
443
|
```python
|
|
318
|
-
import
|
|
444
|
+
from cqrs import requests, events
|
|
319
445
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
email: str
|
|
446
|
+
from app import commands, command_handlers
|
|
447
|
+
from app import queries, query_handlers
|
|
448
|
+
from app import events as event_models, event_handlers
|
|
324
449
|
|
|
325
|
-
class UserResponse(cqrs.Response):
|
|
326
|
-
user_id: str
|
|
327
|
-
username: str
|
|
328
450
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
class CreateProductCommand(cqrs.DCRequest):
|
|
332
|
-
name: str
|
|
333
|
-
price: float
|
|
451
|
+
def init_commands(mapper: requests.RequestMap) -> None:
|
|
452
|
+
mapper.bind(commands.JoinMeetingCommand, command_handlers.JoinMeetingCommandHandler)
|
|
334
453
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
product_id: str
|
|
338
|
-
name: str
|
|
454
|
+
def init_queries(mapper: requests.RequestMap) -> None:
|
|
455
|
+
mapper.bind(queries.ReadMeetingQuery, query_handlers.ReadMeetingQueryHandler)
|
|
339
456
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
self.action = action
|
|
457
|
+
def init_events(mapper: events.EventMap) -> None:
|
|
458
|
+
mapper.bind(events.NotificationEvent[event_models.NotificationMeetingRoomClosed], event_handlers.MeetingRoomClosedNotificationHandler)
|
|
459
|
+
mapper.bind(events.NotificationEvent[event_models.ECSTMeetingRoomClosed], event_handlers.UpdateMeetingRoomReadModelHandler)
|
|
460
|
+
```
|
|
345
461
|
|
|
346
|
-
|
|
347
|
-
return {"user_id": self.user_id, "action": self.action}
|
|
462
|
+
**Chain of Responsibility** — bind a list of handlers (the first one that can handle the request processes it, otherwise the request is passed to the next):
|
|
348
463
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
464
|
+
```python
|
|
465
|
+
def payment_mapper(mapper: cqrs.RequestMap) -> None:
|
|
466
|
+
mapper.bind(
|
|
467
|
+
ProcessPaymentCommand,
|
|
468
|
+
[
|
|
469
|
+
CreditCardPaymentHandler,
|
|
470
|
+
PayPalPaymentHandler,
|
|
471
|
+
DefaultPaymentHandler, # Fallback
|
|
472
|
+
],
|
|
473
|
+
)
|
|
474
|
+
```
|
|
352
475
|
|
|
353
|
-
|
|
354
|
-
def __init__(self, result: str, status: int):
|
|
355
|
-
self.result = result
|
|
356
|
-
self.status = status
|
|
476
|
+
**Streaming handler** — bind a command to a `StreamingRequestHandler` (results are yielded as they become available):
|
|
357
477
|
|
|
358
|
-
|
|
359
|
-
|
|
478
|
+
```python
|
|
479
|
+
def commands_mapper(mapper: cqrs.RequestMap) -> None:
|
|
480
|
+
mapper.bind(ProcessOrdersCommand, ProcessOrdersCommandHandler) # StreamingRequestHandler
|
|
481
|
+
```
|
|
360
482
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
483
|
+
**Saga (including with fallback)** — bind the saga context type to the saga class in `SagaMap`:
|
|
484
|
+
|
|
485
|
+
```python
|
|
486
|
+
def saga_mapper(mapper: cqrs.SagaMap) -> None:
|
|
487
|
+
mapper.bind(OrderContext, OrderSaga)
|
|
488
|
+
mapper.bind(OrderContext, OrderSagaWithFallback)
|
|
364
489
|
```
|
|
365
490
|
|
|
366
|
-
|
|
491
|
+
## DI container
|
|
367
492
|
|
|
368
|
-
|
|
493
|
+
Use the following example to set up dependency injection in your command, query and event handlers. This will make
|
|
494
|
+
dependency management simpler.
|
|
369
495
|
|
|
370
|
-
|
|
496
|
+
The package supports two DI container libraries:
|
|
497
|
+
|
|
498
|
+
### di library
|
|
371
499
|
|
|
372
500
|
```python
|
|
373
|
-
|
|
501
|
+
import di
|
|
502
|
+
...
|
|
374
503
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
504
|
+
def setup_di() -> di.Container:
|
|
505
|
+
"""
|
|
506
|
+
Binds implementations to dependencies
|
|
507
|
+
"""
|
|
508
|
+
container = di.Container()
|
|
509
|
+
container.bind(
|
|
510
|
+
di.bind_by_type(
|
|
511
|
+
dependent.Dependent(cqrs.SqlAlchemyOutboxedEventRepository, scope="request"),
|
|
512
|
+
cqrs.OutboxedEventRepository
|
|
513
|
+
)
|
|
514
|
+
)
|
|
515
|
+
container.bind(
|
|
516
|
+
di.bind_by_type(
|
|
517
|
+
dependent.Dependent(MeetingAPIImplementaion, scope="request"),
|
|
518
|
+
MeetingAPIProtocol
|
|
519
|
+
)
|
|
520
|
+
)
|
|
521
|
+
return container
|
|
522
|
+
```
|
|
378
523
|
|
|
524
|
+
A complete example can be found in
|
|
525
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/dependency_injection.py)
|
|
379
526
|
|
|
380
|
-
|
|
381
|
-
mapper.bind(commands.JoinMeetingCommand, command_handlers.JoinMeetingCommandHandler)
|
|
527
|
+
### dependency-injector library
|
|
382
528
|
|
|
383
|
-
|
|
384
|
-
|
|
529
|
+
The package also supports [dependency-injector](https://github.com/ets-labs/python-dependency-injector) library.
|
|
530
|
+
You can use `DependencyInjectorCQRSContainer` adapter to integrate dependency-injector containers with python-cqrs.
|
|
385
531
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
532
|
+
```python
|
|
533
|
+
from dependency_injector import containers, providers
|
|
534
|
+
from cqrs.container.dependency_injector import DependencyInjectorCQRSContainer
|
|
535
|
+
|
|
536
|
+
class ApplicationContainer(containers.DeclarativeContainer):
|
|
537
|
+
# Define your providers
|
|
538
|
+
service = providers.Factory(ServiceImplementation)
|
|
539
|
+
|
|
540
|
+
# Create CQRS container adapter
|
|
541
|
+
cqrs_container = DependencyInjectorCQRSContainer(ApplicationContainer())
|
|
542
|
+
|
|
543
|
+
# Use with bootstrap
|
|
544
|
+
mediator = bootstrap.bootstrap(
|
|
545
|
+
di_container=cqrs_container,
|
|
546
|
+
commands_mapper=commands_mapper,
|
|
547
|
+
...
|
|
548
|
+
)
|
|
389
549
|
```
|
|
390
550
|
|
|
551
|
+
Complete examples can be found in:
|
|
552
|
+
- [Simple example](https://github.com/vadikko2/python-cqrs/blob/master/examples/dependency_injector_integration_simple_example.py)
|
|
553
|
+
- [Practical example with FastAPI](https://github.com/vadikko2/python-cqrs/blob/master/examples/dependency_injector_integration_practical_example.py)
|
|
554
|
+
|
|
391
555
|
## Bootstrap
|
|
392
556
|
|
|
393
557
|
The `python-cqrs` package implements a set of bootstrap utilities designed to simplify the initial configuration of an
|
|
@@ -420,6 +584,16 @@ def event_mediator_factory():
|
|
|
420
584
|
events_mapper=mapping.init_events,
|
|
421
585
|
on_startup=[orm.init_store_event_mapper],
|
|
422
586
|
)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
@functools.lru_cache
|
|
590
|
+
def saga_mediator_factory():
|
|
591
|
+
return saga_bootstrap.bootstrap(
|
|
592
|
+
di_container=dependencies.setup_di(),
|
|
593
|
+
sagas_mapper=mapping.init_sagas,
|
|
594
|
+
domain_events_mapper=mapping.init_events,
|
|
595
|
+
saga_storage=MemorySagaStorage(),
|
|
596
|
+
)
|
|
423
597
|
```
|
|
424
598
|
|
|
425
599
|
## Saga Pattern
|
|
@@ -573,68 +747,7 @@ sequence_diagram = generator.sequence()
|
|
|
573
747
|
class_diagram = generator.class_diagram()
|
|
574
748
|
```
|
|
575
749
|
|
|
576
|
-
Complete example: [Saga Mermaid Diagrams](https://github.com/vadikko2/cqrs/blob/master/examples/saga_mermaid.py)
|
|
577
|
-
|
|
578
|
-
## Event Handlers
|
|
579
|
-
|
|
580
|
-
Event handlers are designed to process `Notification` and `ECST` events that are consumed from the broker.
|
|
581
|
-
To configure event handling, you need to implement a broker consumer on the side of your application.
|
|
582
|
-
Below is an example of `Kafka event consuming` that can be used in the Presentation Layer.
|
|
583
|
-
|
|
584
|
-
```python
|
|
585
|
-
class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
|
|
586
|
-
def __init__(self):
|
|
587
|
-
self._events = []
|
|
588
|
-
|
|
589
|
-
@property
|
|
590
|
-
def events(self):
|
|
591
|
-
return self._events
|
|
592
|
-
|
|
593
|
-
async def handle(self, request: JoinMeetingCommand) -> None:
|
|
594
|
-
STORAGE[request.meeting_id].append(request.user_id)
|
|
595
|
-
self._events.append(
|
|
596
|
-
UserJoined(user_id=request.user_id, meeting_id=request.meeting_id),
|
|
597
|
-
)
|
|
598
|
-
print(f"User {request.user_id} joined meeting {request.meeting_id}")
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
class UserJoinedEventHandler(cqrs.EventHandler[UserJoined]):
|
|
602
|
-
async def handle(self, event: UserJoined) -> None:
|
|
603
|
-
print(f"Handle user {event.user_id} joined meeting {event.meeting_id} event")
|
|
604
|
-
```
|
|
605
|
-
|
|
606
|
-
A complete example can be found in
|
|
607
|
-
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/domain_event_handler.py)
|
|
608
|
-
|
|
609
|
-
### Parallel Event Processing
|
|
610
|
-
|
|
611
|
-
Both `RequestMediator` and `StreamingRequestMediator` support parallel processing of domain events. You can control
|
|
612
|
-
the number of event handlers that run simultaneously using the `max_concurrent_event_handlers` parameter.
|
|
613
|
-
|
|
614
|
-
This feature is especially useful when:
|
|
615
|
-
- Multiple event handlers need to process events independently
|
|
616
|
-
- You want to improve performance by processing events concurrently
|
|
617
|
-
- You need to limit resource consumption by controlling concurrency
|
|
618
|
-
|
|
619
|
-
**Configuration:**
|
|
620
|
-
|
|
621
|
-
```python
|
|
622
|
-
from cqrs.requests import bootstrap
|
|
623
|
-
|
|
624
|
-
mediator = bootstrap.bootstrap_streaming(
|
|
625
|
-
di_container=container,
|
|
626
|
-
commands_mapper=commands_mapper,
|
|
627
|
-
domain_events_mapper=domain_events_mapper,
|
|
628
|
-
message_broker=broker,
|
|
629
|
-
max_concurrent_event_handlers=3, # Process up to 3 events in parallel
|
|
630
|
-
concurrent_event_handle_enable=True, # Enable parallel processing
|
|
631
|
-
)
|
|
632
|
-
```
|
|
633
|
-
|
|
634
|
-
> [!TIP]
|
|
635
|
-
> - Set `max_concurrent_event_handlers` to limit the number of simultaneously running event handlers
|
|
636
|
-
> - Set `concurrent_event_handle_enable=False` to disable parallel processing and process events sequentially
|
|
637
|
-
> - The default value for `max_concurrent_event_handlers` is `10` for `StreamingRequestMediator` and `1` for `RequestMediator`
|
|
750
|
+
Complete example: [Saga Mermaid Diagrams](https://github.com/vadikko2/python-cqrs/blob/master/examples/saga_mermaid.py)
|
|
638
751
|
|
|
639
752
|
## Producing Notification Events
|
|
640
753
|
|
|
@@ -674,7 +787,7 @@ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
|
|
|
674
787
|
```
|
|
675
788
|
|
|
676
789
|
A complete example can be found in
|
|
677
|
-
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/event_producing.py)
|
|
790
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/event_producing.py)
|
|
678
791
|
|
|
679
792
|
After processing the command/request, if there are any Notification/ECST events,
|
|
680
793
|
the EventEmitter is invoked to produce the events via the message broker.
|
|
@@ -709,13 +822,6 @@ The package implements the [Transactional Outbox](https://microservices.io/patte
|
|
|
709
822
|
pattern, which ensures that messages are produced to the broker according to the at-least-once semantics.
|
|
710
823
|
|
|
711
824
|
```python
|
|
712
|
-
def do_some_logic(meeting_room_id: int, session: sql_session.AsyncSession):
|
|
713
|
-
"""
|
|
714
|
-
Make changes to the database
|
|
715
|
-
"""
|
|
716
|
-
session.add(...)
|
|
717
|
-
|
|
718
|
-
|
|
719
825
|
class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
|
|
720
826
|
def __init__(self, outbox: cqrs.OutboxedEventRepository):
|
|
721
827
|
self.outbox = outbox
|
|
@@ -726,35 +832,33 @@ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
|
|
|
726
832
|
|
|
727
833
|
async def handle(self, request: JoinMeetingCommand) -> None:
|
|
728
834
|
print(f"User {request.user_id} joined meeting {request.meeting_id}")
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
meeting_id=request.meeting_id,
|
|
739
|
-
),
|
|
835
|
+
# Outbox repository is bound to a session (e.g. via DI request scope).
|
|
836
|
+
# add() takes only the event; commit() persists the outbox and your changes.
|
|
837
|
+
self.outbox.add(
|
|
838
|
+
cqrs.NotificationEvent[UserJoinedNotificationPayload](
|
|
839
|
+
event_name="UserJoined",
|
|
840
|
+
topic="user_notification_events",
|
|
841
|
+
payload=UserJoinedNotificationPayload(
|
|
842
|
+
user_id=request.user_id,
|
|
843
|
+
meeting_id=request.meeting_id,
|
|
740
844
|
),
|
|
741
|
-
)
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
),
|
|
845
|
+
),
|
|
846
|
+
)
|
|
847
|
+
self.outbox.add(
|
|
848
|
+
cqrs.NotificationEvent[UserJoinedECSTPayload](
|
|
849
|
+
event_name="UserJoined",
|
|
850
|
+
topic="user_ecst_events",
|
|
851
|
+
payload=UserJoinedECSTPayload(
|
|
852
|
+
user_id=request.user_id,
|
|
853
|
+
meeting_id=request.meeting_id,
|
|
751
854
|
),
|
|
752
|
-
)
|
|
753
|
-
|
|
855
|
+
),
|
|
856
|
+
)
|
|
857
|
+
await self.outbox.commit()
|
|
754
858
|
```
|
|
755
859
|
|
|
756
860
|
A complete example can be found in
|
|
757
|
-
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/save_events_into_outbox.py)
|
|
861
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/save_events_into_outbox.py)
|
|
758
862
|
|
|
759
863
|
> [!TIP]
|
|
760
864
|
> You can specify the name of the Outbox table using the environment variable `OUTBOX_SQLA_TABLE`.
|
|
@@ -762,8 +866,8 @@ the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/save_e
|
|
|
762
866
|
|
|
763
867
|
> [!TIP]
|
|
764
868
|
> If you use the protobuf events you should specify `OutboxedEventRepository`
|
|
765
|
-
> by [protobuf serialize](https://github.com/vadikko2/cqrs/blob/master/src/cqrs/serializers/protobuf.py). A complete example can be found in
|
|
766
|
-
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/save_proto_events_into_outbox.py)
|
|
869
|
+
> by [protobuf serialize](https://github.com/vadikko2/python-cqrs/blob/master/src/cqrs/serializers/protobuf.py). A complete example can be found in
|
|
870
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/save_proto_events_into_outbox.py)
|
|
767
871
|
|
|
768
872
|
## Producing Events from Outbox to Kafka
|
|
769
873
|
|
|
@@ -789,89 +893,93 @@ broker = kafka.KafkaMessageBroker(
|
|
|
789
893
|
producer=kafka_adapters.kafka_producer_factory(dsn="localhost:9092"),
|
|
790
894
|
)
|
|
791
895
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
async for messages in producer.event_batch_generator():
|
|
797
|
-
for message in messages:
|
|
798
|
-
await producer.send_message(message)
|
|
799
|
-
await producer.repository.commit()
|
|
800
|
-
await asyncio.sleep(10)
|
|
896
|
+
# SqlAlchemyOutboxedEventRepository expects (session, compressor), not session_factory.
|
|
897
|
+
async with session_factory() as session:
|
|
898
|
+
repository = cqrs.SqlAlchemyOutboxedEventRepository(session, zlib.ZlibCompressor())
|
|
899
|
+
producer = cqrs.EventProducer(broker, repository)
|
|
801
900
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
901
|
+
async for messages in producer.event_batch_generator():
|
|
902
|
+
for message in messages:
|
|
903
|
+
await producer.send_message(message)
|
|
904
|
+
await producer.repository.commit()
|
|
905
|
+
await asyncio.sleep(10)
|
|
805
906
|
```
|
|
806
907
|
|
|
807
908
|
A complete example can be found in
|
|
808
|
-
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/kafka_outboxed_event_producing.py)
|
|
909
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/kafka_outboxed_event_producing.py)
|
|
809
910
|
|
|
810
|
-
|
|
911
|
+
## Transaction log tailing
|
|
811
912
|
|
|
812
|
-
|
|
913
|
+
If the Outbox polling strategy does not suit your needs, I recommend exploring
|
|
914
|
+
the [Transaction Log Tailing](https://microservices.io/patterns/data/transaction-log-tailing.html) pattern.
|
|
915
|
+
The current version of the python-cqrs package does not support the implementation of this pattern.
|
|
813
916
|
|
|
814
|
-
|
|
815
|
-
|
|
917
|
+
> [!TIP]
|
|
918
|
+
> However, it can be implemented
|
|
919
|
+
> using [Debezium + Kafka Connect](https://debezium.io/documentation/reference/stable/architecture.html),
|
|
920
|
+
> which allows you to produce all newly created events within the Outbox storage directly to the corresponding topic in
|
|
921
|
+
> Kafka (or any other broker).
|
|
816
922
|
|
|
817
|
-
|
|
923
|
+
## Event Handlers
|
|
818
924
|
|
|
819
|
-
|
|
925
|
+
Event handlers are designed to process `Notification` and `ECST` events that are consumed from the broker.
|
|
926
|
+
To configure event handling, you need to implement a broker consumer on the side of your application.
|
|
927
|
+
Below is an example of `Kafka event consuming` that can be used in the Presentation Layer.
|
|
820
928
|
|
|
821
929
|
```python
|
|
822
|
-
|
|
823
|
-
|
|
930
|
+
class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
|
|
931
|
+
def __init__(self):
|
|
932
|
+
self._events = []
|
|
824
933
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
cqrs.OutboxedEventRepository
|
|
834
|
-
)
|
|
835
|
-
)
|
|
836
|
-
container.bind(
|
|
837
|
-
di.bind_by_type(
|
|
838
|
-
dependent.Dependent(MeetingAPIImplementaion, scope="request"),
|
|
839
|
-
MeetingAPIProtocol
|
|
934
|
+
@property
|
|
935
|
+
def events(self):
|
|
936
|
+
return self._events
|
|
937
|
+
|
|
938
|
+
async def handle(self, request: JoinMeetingCommand) -> None:
|
|
939
|
+
STORAGE[request.meeting_id].append(request.user_id)
|
|
940
|
+
self._events.append(
|
|
941
|
+
UserJoined(user_id=request.user_id, meeting_id=request.meeting_id),
|
|
840
942
|
)
|
|
841
|
-
|
|
842
|
-
|
|
943
|
+
print(f"User {request.user_id} joined meeting {request.meeting_id}")
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
class UserJoinedEventHandler(cqrs.EventHandler[UserJoined]):
|
|
947
|
+
async def handle(self, event: UserJoined) -> None:
|
|
948
|
+
print(f"Handle user {event.user_id} joined meeting {event.meeting_id} event")
|
|
843
949
|
```
|
|
844
950
|
|
|
845
951
|
A complete example can be found in
|
|
846
|
-
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/
|
|
952
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/domain_event_handler.py)
|
|
847
953
|
|
|
848
|
-
###
|
|
954
|
+
### Parallel Event Processing
|
|
849
955
|
|
|
850
|
-
|
|
851
|
-
|
|
956
|
+
Both `RequestMediator` and `StreamingRequestMediator` support parallel processing of domain events. You can control
|
|
957
|
+
the number of event handlers that run simultaneously using the `max_concurrent_event_handlers` parameter.
|
|
852
958
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
959
|
+
This feature is especially useful when:
|
|
960
|
+
- Multiple event handlers need to process events independently
|
|
961
|
+
- You want to improve performance by processing events concurrently
|
|
962
|
+
- You need to limit resource consumption by controlling concurrency
|
|
856
963
|
|
|
857
|
-
|
|
858
|
-
# Define your providers
|
|
859
|
-
service = providers.Factory(ServiceImplementation)
|
|
964
|
+
**Configuration:**
|
|
860
965
|
|
|
861
|
-
|
|
862
|
-
|
|
966
|
+
```python
|
|
967
|
+
from cqrs.requests import bootstrap
|
|
863
968
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
di_container=cqrs_container,
|
|
969
|
+
mediator = bootstrap.bootstrap_streaming(
|
|
970
|
+
di_container=container,
|
|
867
971
|
commands_mapper=commands_mapper,
|
|
868
|
-
|
|
972
|
+
domain_events_mapper=domain_events_mapper,
|
|
973
|
+
message_broker=broker,
|
|
974
|
+
max_concurrent_event_handlers=3, # Process up to 3 events in parallel
|
|
975
|
+
concurrent_event_handle_enable=True, # Enable parallel processing
|
|
869
976
|
)
|
|
870
977
|
```
|
|
871
978
|
|
|
872
|
-
|
|
873
|
-
-
|
|
874
|
-
-
|
|
979
|
+
> [!TIP]
|
|
980
|
+
> - Set `max_concurrent_event_handlers` to limit the number of simultaneously running event handlers
|
|
981
|
+
> - Set `concurrent_event_handle_enable=False` to disable parallel processing and process events sequentially
|
|
982
|
+
> - The default value for `max_concurrent_event_handlers` is `10` for `StreamingRequestMediator` and `1` for `RequestMediator`
|
|
875
983
|
|
|
876
984
|
## Integration with presentation layers
|
|
877
985
|
|
|
@@ -893,7 +1001,7 @@ In this case you can use python-cqrs to route requests to the appropriate handle
|
|
|
893
1001
|
import fastapi
|
|
894
1002
|
import pydantic
|
|
895
1003
|
|
|
896
|
-
from app import
|
|
1004
|
+
from app import dependencies, commands
|
|
897
1005
|
|
|
898
1006
|
router = fastapi.APIRouter(prefix="/meetings")
|
|
899
1007
|
|
|
@@ -909,7 +1017,7 @@ async def join_metting(
|
|
|
909
1017
|
```
|
|
910
1018
|
|
|
911
1019
|
A complete example can be found in
|
|
912
|
-
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/fastapi_integration.py)
|
|
1020
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/fastapi_integration.py)
|
|
913
1021
|
|
|
914
1022
|
### Kafka events consuming
|
|
915
1023
|
|
|
@@ -1005,9 +1113,71 @@ async def process_files_stream(
|
|
|
1005
1113
|
```
|
|
1006
1114
|
|
|
1007
1115
|
A complete example can be found in
|
|
1008
|
-
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/fastapi_sse_streaming.py)
|
|
1116
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/fastapi_sse_streaming.py)
|
|
1009
1117
|
|
|
1010
1118
|
## Protobuf messaging
|
|
1011
1119
|
|
|
1012
1120
|
The `python-cqrs` package supports integration with [protobuf](https://developers.google.com/protocol-buffers/).
|
|
1013
|
-
|
|
1121
|
+
Notification events can be serialized to Protobuf and back: implement the `proto()` method (returns a protobuf message) and the class method `from_proto()` (creates an event instance from proto) on your event class.
|
|
1122
|
+
|
|
1123
|
+
Example (assuming generated `user_joined_pb2` from your `.proto` with fields `event_id`, `event_timestamp`, `event_name`, `payload`):
|
|
1124
|
+
|
|
1125
|
+
```python
|
|
1126
|
+
import uuid
|
|
1127
|
+
from datetime import datetime
|
|
1128
|
+
|
|
1129
|
+
import cqrs
|
|
1130
|
+
from app.generated import user_joined_pb2 # generated from .proto
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
class UserJoinedPayload(cqrs.Response):
|
|
1134
|
+
user_id: str
|
|
1135
|
+
meeting_id: str
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
class UserJoinedNotificationEvent(cqrs.NotificationEvent[UserJoinedPayload]):
|
|
1139
|
+
"""Event with Protobuf serialization support."""
|
|
1140
|
+
|
|
1141
|
+
event_name: str = "UserJoined"
|
|
1142
|
+
|
|
1143
|
+
def proto(self):
|
|
1144
|
+
msg = user_joined_pb2.UserJoinedNotification()
|
|
1145
|
+
msg.event_id = str(self.event_id)
|
|
1146
|
+
msg.event_timestamp = self.event_timestamp.isoformat()
|
|
1147
|
+
msg.event_name = self.event_name
|
|
1148
|
+
msg.payload.user_id = self.payload.user_id
|
|
1149
|
+
msg.payload.meeting_id = self.payload.meeting_id
|
|
1150
|
+
return msg
|
|
1151
|
+
|
|
1152
|
+
@classmethod
|
|
1153
|
+
def from_proto(cls, proto_msg):
|
|
1154
|
+
return cls(
|
|
1155
|
+
event_id=uuid.UUID(proto_msg.event_id),
|
|
1156
|
+
event_timestamp=datetime.fromisoformat(proto_msg.event_timestamp),
|
|
1157
|
+
event_name=proto_msg.event_name,
|
|
1158
|
+
topic="user_notification_events",
|
|
1159
|
+
payload=UserJoinedPayload(
|
|
1160
|
+
user_id=proto_msg.payload.user_id,
|
|
1161
|
+
meeting_id=proto_msg.payload.meeting_id,
|
|
1162
|
+
),
|
|
1163
|
+
)
|
|
1164
|
+
```
|
|
1165
|
+
|
|
1166
|
+
## Contributing
|
|
1167
|
+
|
|
1168
|
+
Contributions are welcome. To develop locally:
|
|
1169
|
+
|
|
1170
|
+
1. Clone the repository and create a virtual environment.
|
|
1171
|
+
2. Install dev dependencies: `pip install -e ".[dev]"`.
|
|
1172
|
+
3. Run tests: `pytest`.
|
|
1173
|
+
4. Install pre-commit and run hooks: `pre-commit install && pre-commit run --all-files`.
|
|
1174
|
+
|
|
1175
|
+
The project uses [ruff](https://docs.astral.sh/ruff/) for linting and [pyright](https://microsoft.github.io/pyright/) for type checking.
|
|
1176
|
+
|
|
1177
|
+
## Changelog
|
|
1178
|
+
|
|
1179
|
+
Release notes and migration guides are published on [GitHub Releases](https://github.com/vadikko2/python-cqrs/releases).
|
|
1180
|
+
|
|
1181
|
+
## License
|
|
1182
|
+
|
|
1183
|
+
This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details.
|