python-cqrs 4.8.1__tar.gz → 4.10.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.8.1/src/python_cqrs.egg-info → python_cqrs-4.10.0}/PKG-INFO +449 -291
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/README.md +447 -290
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/pyproject.toml +2 -1
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/__init__.py +6 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/adapters/circuit_breaker.py +32 -44
- python_cqrs-4.10.0/src/cqrs/circuit_breaker.py +77 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/deserializers/json.py +1 -2
- python_cqrs-4.10.0/src/cqrs/dispatcher/event.py +96 -0
- python_cqrs-4.10.0/src/cqrs/dispatcher/request.py +139 -0
- python_cqrs-4.10.0/src/cqrs/dispatcher/streaming.py +146 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/events/__init__.py +3 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/events/event_emitter.py +57 -5
- python_cqrs-4.10.0/src/cqrs/events/fallback.py +92 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/events/map.py +10 -11
- python_cqrs-4.10.0/src/cqrs/generic_utils.py +43 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/middlewares/base.py +1 -1
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/outbox/mock.py +1 -3
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/outbox/sqlalchemy.py +3 -9
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/requests/cor_request_handler.py +1 -1
- python_cqrs-4.10.0/src/cqrs/requests/fallback.py +98 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/requests/map.py +2 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/requests/mermaid.py +5 -15
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/requests/request.py +10 -1
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/requests/request_handler.py +1 -1
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/compensation.py +38 -22
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/execution.py +23 -27
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/fallback.py +3 -3
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/mermaid.py +7 -25
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/recovery.py +1 -2
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/saga.py +149 -74
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/storage/__init__.py +2 -1
- python_cqrs-4.10.0/src/cqrs/saga/storage/memory.py +357 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/storage/protocol.py +141 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/storage/sqlalchemy.py +286 -15
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/validation.py +4 -12
- python_cqrs-4.10.0/src/cqrs/types.py +11 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0/src/python_cqrs.egg-info}/PKG-INFO +449 -291
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/python_cqrs.egg-info/SOURCES.txt +4 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/python_cqrs.egg-info/requires.txt +1 -0
- python_cqrs-4.8.1/src/cqrs/dispatcher/event.py +0 -45
- python_cqrs-4.8.1/src/cqrs/dispatcher/request.py +0 -78
- python_cqrs-4.8.1/src/cqrs/dispatcher/streaming.py +0 -85
- python_cqrs-4.8.1/src/cqrs/saga/storage/memory.py +0 -169
- python_cqrs-4.8.1/src/cqrs/types.py +0 -18
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/LICENSE +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/setup.cfg +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/adapters/__init__.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/adapters/amqp.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/adapters/kafka.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/adapters/protocol.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/compressors/__init__.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/compressors/protocol.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/compressors/zlib.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/container/__init__.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/container/dependency_injector.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/container/di.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/container/protocol.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/deserializers/__init__.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/deserializers/exceptions.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/dispatcher/__init__.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/dispatcher/exceptions.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/dispatcher/models.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/dispatcher/saga.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/events/bootstrap.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/events/event.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/events/event_handler.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/events/event_processor.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/mediator.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/message_brokers/__init__.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/message_brokers/amqp.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/message_brokers/devnull.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/message_brokers/kafka.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/message_brokers/protocol.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/middlewares/__init__.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/middlewares/logging.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/outbox/__init__.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/outbox/map.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/outbox/repository.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/producer.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/requests/__init__.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/requests/bootstrap.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/response.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/__init__.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/bootstrap.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/circuit_breaker.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/models.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/step.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/storage/enums.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/storage/models.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/serializers/__init__.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/serializers/default.py +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/python_cqrs.egg-info/dependency_links.txt +0 -0
- {python_cqrs-4.8.1 → python_cqrs-4.10.0}/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.0
|
|
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>
|
|
@@ -43,6 +43,7 @@ Requires-Dist: pytest-asyncio~=0.21.1; extra == "dev"
|
|
|
43
43
|
Requires-Dist: pytest-env==0.6.2; extra == "dev"
|
|
44
44
|
Requires-Dist: cryptography==42.0.2; extra == "dev"
|
|
45
45
|
Requires-Dist: asyncmy==0.2.9; extra == "dev"
|
|
46
|
+
Requires-Dist: asyncpg>=0.29.0; extra == "dev"
|
|
46
47
|
Requires-Dist: redis>=5.0.0; extra == "dev"
|
|
47
48
|
Requires-Dist: aiobreaker>=0.3.0; extra == "dev"
|
|
48
49
|
Provides-Extra: examples
|
|
@@ -58,16 +59,17 @@ Provides-Extra: rabbit
|
|
|
58
59
|
Requires-Dist: aio-pika==9.3.0; extra == "rabbit"
|
|
59
60
|
Dynamic: license-file
|
|
60
61
|
|
|
61
|
-
<div align="center">
|
|
62
62
|
<div align="center">
|
|
63
63
|
<img
|
|
64
64
|
src="https://raw.githubusercontent.com/vadikko2/python-cqrs-mkdocs/master/docs/img.png"
|
|
65
65
|
alt="Python CQRS"
|
|
66
|
-
style="max-width: 80%;
|
|
66
|
+
style="max-width: 80%;"
|
|
67
67
|
>
|
|
68
|
-
</div>
|
|
69
68
|
<h1>Python CQRS</h1>
|
|
70
|
-
<
|
|
69
|
+
<p><strong>Event-Driven Architecture Framework for Distributed Systems</strong></p>
|
|
70
|
+
<p>
|
|
71
|
+
<strong>Python 3.10+</strong> · Full documentation: <a href="https://mkdocs.python-cqrs.dev/">mkdocs.python-cqrs.dev</a>
|
|
72
|
+
</p>
|
|
71
73
|
<p>
|
|
72
74
|
<a href="https://pypi.org/project/python-cqrs/">
|
|
73
75
|
<img src="https://img.shields.io/pypi/pyversions/python-cqrs?logo=python&logoColor=white" alt="Python Versions">
|
|
@@ -101,31 +103,152 @@ Dynamic: license-file
|
|
|
101
103
|
>
|
|
102
104
|
> 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.
|
|
103
105
|
|
|
106
|
+
## Table of Contents
|
|
107
|
+
|
|
108
|
+
- [Overview](#overview)
|
|
109
|
+
- [Installation](#installation)
|
|
110
|
+
- [Quick Start](#quick-start)
|
|
111
|
+
- [Request and Response Types](#request-and-response-types)
|
|
112
|
+
- [Request Handlers](#request-handlers)
|
|
113
|
+
- [Mapping](#mapping)
|
|
114
|
+
- [DI container](#di-container)
|
|
115
|
+
- [Bootstrap](#bootstrap)
|
|
116
|
+
- [Saga Pattern](#saga-pattern)
|
|
117
|
+
- [Producing Notification Events](#producing-notification-events)
|
|
118
|
+
- [Kafka broker](#kafka-broker)
|
|
119
|
+
- [Transactional Outbox](#transactional-outbox)
|
|
120
|
+
- [Producing Events from Outbox to Kafka](#producing-events-from-outbox-to-kafka)
|
|
121
|
+
- [Transaction log tailing](#transaction-log-tailing)
|
|
122
|
+
- [Event Handlers](#event-handlers)
|
|
123
|
+
- [Integration with presentation layers](#integration-with-presentation-layers)
|
|
124
|
+
- [Protobuf messaging](#protobuf-messaging)
|
|
125
|
+
- [Contributing](#contributing)
|
|
126
|
+
- [Changelog](#changelog)
|
|
127
|
+
- [License](#license)
|
|
128
|
+
|
|
104
129
|
## Overview
|
|
105
130
|
|
|
106
|
-
|
|
107
|
-
It provides a set of abstractions and utilities to help separate read and write use cases, ensuring better scalability,
|
|
108
|
-
performance, and maintainability of the application.
|
|
131
|
+
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.
|
|
109
132
|
|
|
110
133
|
This package is a fork of the [diator](https://github.com/akhundMurad/diator)
|
|
111
|
-
project ([documentation](https://akhundmurad.github.io/diator/)) with several enhancements:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
134
|
+
project ([documentation](https://akhundmurad.github.io/diator/)) with several enhancements, ordered by importance:
|
|
135
|
+
|
|
136
|
+
**Core framework**
|
|
137
|
+
|
|
138
|
+
1. Redesigned the event and request mapping mechanism to handlers;
|
|
139
|
+
2. `EventMediator` for handling `Notification` and `ECST` events coming from the bus;
|
|
140
|
+
3. `bootstrap` for easy setup;
|
|
141
|
+
4. **Transaction Outbox**, ensuring that `Notification` and `ECST` events are sent to the broker;
|
|
142
|
+
5. **Orchestrated Saga** pattern for distributed transactions with automatic compensation and recovery;
|
|
143
|
+
6. `StreamingRequestMediator` and `StreamingRequestHandler` for streaming requests with real-time progress updates;
|
|
144
|
+
7. **Chain of Responsibility** with `CORRequestHandler` for processing requests through multiple handlers in sequence;
|
|
145
|
+
8. **Parallel event processing** with configurable concurrency limits.
|
|
146
|
+
|
|
147
|
+
**Also**
|
|
148
|
+
|
|
149
|
+
- **Typing:** Pydantic [v2.*](https://docs.pydantic.dev/2.8/) and `IRequest`/`IResponse` interfaces — use Pydantic-based, dataclass-based, or custom Request/Response implementations.
|
|
150
|
+
- **Broker:** Kafka via [aiokafka](https://github.com/aio-libs/aiokafka).
|
|
151
|
+
- **Integration:** Ready for integration with FastAPI and FastStream.
|
|
152
|
+
- **Documentation:** Built-in Mermaid diagram generation (Sequence and Class diagrams).
|
|
153
|
+
- **Protobuf:** Interface-level support for converting Notification events to Protobuf and back.
|
|
154
|
+
|
|
155
|
+
## Installation
|
|
156
|
+
|
|
157
|
+
**Python 3.10+** is required.
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
pip install python-cqrs
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Optional dependencies (see [pyproject.toml](https://github.com/vadikko2/python-cqrs/blob/master/pyproject.toml) for full list):
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
pip install python-cqrs[kafka] # Kafka broker (aiokafka)
|
|
167
|
+
pip install python-cqrs[examples] # FastAPI, FastStream, uvicorn, etc.
|
|
168
|
+
pip install python-cqrs[aiobreaker] # Circuit breaker for saga fallbacks
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Quick Start
|
|
172
|
+
|
|
173
|
+
Define a command, a handler, bind them, and run via the mediator:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
import di
|
|
177
|
+
import cqrs
|
|
178
|
+
from cqrs.requests import bootstrap
|
|
179
|
+
|
|
180
|
+
class CreateOrderCommand(cqrs.Request):
|
|
181
|
+
order_id: str
|
|
182
|
+
amount: float
|
|
183
|
+
|
|
184
|
+
class CreateOrderHandler(cqrs.RequestHandler[CreateOrderCommand, None]):
|
|
185
|
+
async def handle(self, request: CreateOrderCommand) -> None:
|
|
186
|
+
print(f"Order {request.order_id}, amount {request.amount}")
|
|
187
|
+
|
|
188
|
+
def commands_mapper(mapper: cqrs.RequestMap) -> None:
|
|
189
|
+
mapper.bind(CreateOrderCommand, CreateOrderHandler)
|
|
190
|
+
|
|
191
|
+
container = di.Container()
|
|
192
|
+
mediator = bootstrap.bootstrap(di_container=container, commands_mapper=commands_mapper)
|
|
193
|
+
await mediator.send(CreateOrderCommand(order_id="ord-1", amount=99.99))
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
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.
|
|
197
|
+
|
|
198
|
+
## Request and Response Types
|
|
199
|
+
|
|
200
|
+
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.
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
import dataclasses
|
|
204
|
+
|
|
205
|
+
# Pydantic-based (default)
|
|
206
|
+
class CreateUserCommand(cqrs.Request):
|
|
207
|
+
username: str
|
|
208
|
+
email: str
|
|
209
|
+
|
|
210
|
+
class UserResponse(cqrs.Response):
|
|
211
|
+
user_id: str
|
|
212
|
+
username: str
|
|
213
|
+
|
|
214
|
+
# Dataclass-based
|
|
215
|
+
@dataclasses.dataclass
|
|
216
|
+
class CreateProductCommand(cqrs.DCRequest):
|
|
217
|
+
name: str
|
|
218
|
+
price: float
|
|
219
|
+
|
|
220
|
+
@dataclasses.dataclass
|
|
221
|
+
class ProductResponse(cqrs.DCResponse):
|
|
222
|
+
product_id: str
|
|
223
|
+
name: str
|
|
224
|
+
|
|
225
|
+
# Custom implementation
|
|
226
|
+
class CustomRequest(cqrs.IRequest):
|
|
227
|
+
def __init__(self, user_id: str, action: str):
|
|
228
|
+
self.user_id = user_id
|
|
229
|
+
self.action = action
|
|
230
|
+
|
|
231
|
+
def to_dict(self) -> dict:
|
|
232
|
+
return {"user_id": self.user_id, "action": self.action}
|
|
233
|
+
|
|
234
|
+
@classmethod
|
|
235
|
+
def from_dict(cls, **kwargs) -> "CustomRequest":
|
|
236
|
+
return cls(user_id=kwargs["user_id"], action=kwargs["action"])
|
|
237
|
+
|
|
238
|
+
class CustomResponse(cqrs.IResponse):
|
|
239
|
+
def __init__(self, result: str, status: int):
|
|
240
|
+
self.result = result
|
|
241
|
+
self.status = status
|
|
242
|
+
|
|
243
|
+
def to_dict(self) -> dict:
|
|
244
|
+
return {"result": self.result, "status": self.status}
|
|
245
|
+
|
|
246
|
+
@classmethod
|
|
247
|
+
def from_dict(cls, **kwargs) -> "CustomResponse":
|
|
248
|
+
return cls(result=kwargs["result"], status=kwargs["status"])
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
A complete example can be found in [request_response_types.py](https://github.com/vadikko2/python-cqrs/blob/master/examples/request_response_types.py)
|
|
129
252
|
|
|
130
253
|
## Request Handlers
|
|
131
254
|
|
|
@@ -147,7 +270,7 @@ class JoinMeetingCommandHandler(RequestHandler[JoinMeetingCommand, None]):
|
|
|
147
270
|
|
|
148
271
|
def __init__(self, meetings_api: MeetingAPIProtocol) -> None:
|
|
149
272
|
self._meetings_api = meetings_api
|
|
150
|
-
self.
|
|
273
|
+
self._events: list[Event] = []
|
|
151
274
|
|
|
152
275
|
@property
|
|
153
276
|
def events(self) -> typing.List[events.Event]:
|
|
@@ -158,7 +281,7 @@ class JoinMeetingCommandHandler(RequestHandler[JoinMeetingCommand, None]):
|
|
|
158
281
|
```
|
|
159
282
|
|
|
160
283
|
A complete example can be found in
|
|
161
|
-
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/request_handler.py)
|
|
284
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/request_handler.py)
|
|
162
285
|
|
|
163
286
|
### Query handler
|
|
164
287
|
|
|
@@ -175,7 +298,7 @@ class ReadMeetingQueryHandler(RequestHandler[ReadMeetingQuery, ReadMeetingQueryR
|
|
|
175
298
|
|
|
176
299
|
def __init__(self, meetings_api: MeetingAPIProtocol) -> None:
|
|
177
300
|
self._meetings_api = meetings_api
|
|
178
|
-
self.
|
|
301
|
+
self._events: list[Event] = []
|
|
179
302
|
|
|
180
303
|
@property
|
|
181
304
|
def events(self) -> typing.List[events.Event]:
|
|
@@ -188,7 +311,7 @@ class ReadMeetingQueryHandler(RequestHandler[ReadMeetingQuery, ReadMeetingQueryR
|
|
|
188
311
|
```
|
|
189
312
|
|
|
190
313
|
A complete example can be found in
|
|
191
|
-
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/request_handler.py)
|
|
314
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/request_handler.py)
|
|
192
315
|
|
|
193
316
|
### Streaming Request Handler
|
|
194
317
|
|
|
@@ -224,7 +347,7 @@ class ProcessFilesCommandHandler(StreamingRequestHandler[ProcessFilesCommand, Fi
|
|
|
224
347
|
```
|
|
225
348
|
|
|
226
349
|
A complete example can be found in
|
|
227
|
-
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/streaming_handler_parallel_events.py)
|
|
350
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/streaming_handler_parallel_events.py)
|
|
228
351
|
|
|
229
352
|
### Chain of Responsibility Request Handler
|
|
230
353
|
|
|
@@ -287,7 +410,7 @@ def payment_mapper(mapper: cqrs.RequestMap) -> None:
|
|
|
287
410
|
```
|
|
288
411
|
|
|
289
412
|
A complete example can be found in
|
|
290
|
-
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/cor_request_handler.py)
|
|
413
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/cor_request_handler.py)
|
|
291
414
|
|
|
292
415
|
#### Mermaid Diagram Generation
|
|
293
416
|
|
|
@@ -307,62 +430,169 @@ sequence_diagram = generator.sequence()
|
|
|
307
430
|
class_diagram = generator.class_diagram()
|
|
308
431
|
```
|
|
309
432
|
|
|
310
|
-
Complete example: [CoR Mermaid Diagrams](https://github.com/vadikko2/cqrs/blob/master/examples/cor_mermaid.py)
|
|
433
|
+
Complete example: [CoR Mermaid Diagrams](https://github.com/vadikko2/python-cqrs/blob/master/examples/cor_mermaid.py)
|
|
311
434
|
|
|
312
|
-
##
|
|
435
|
+
## Mapping
|
|
313
436
|
|
|
314
|
-
|
|
437
|
+
To bind commands, queries and events with specific handlers, you can use the registries `EventMap`, `RequestMap`, and `SagaMap`.
|
|
438
|
+
|
|
439
|
+
**Commands, queries and events:**
|
|
315
440
|
|
|
316
441
|
```python
|
|
317
|
-
import
|
|
442
|
+
from cqrs import requests, events
|
|
318
443
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
email: str
|
|
444
|
+
from app import commands, command_handlers
|
|
445
|
+
from app import queries, query_handlers
|
|
446
|
+
from app import events as event_models, event_handlers
|
|
323
447
|
|
|
324
|
-
class UserResponse(cqrs.Response):
|
|
325
|
-
user_id: str
|
|
326
|
-
username: str
|
|
327
448
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
class CreateProductCommand(cqrs.DCRequest):
|
|
331
|
-
name: str
|
|
332
|
-
price: float
|
|
449
|
+
def init_commands(mapper: requests.RequestMap) -> None:
|
|
450
|
+
mapper.bind(commands.JoinMeetingCommand, command_handlers.JoinMeetingCommandHandler)
|
|
333
451
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
product_id: str
|
|
337
|
-
name: str
|
|
452
|
+
def init_queries(mapper: requests.RequestMap) -> None:
|
|
453
|
+
mapper.bind(queries.ReadMeetingQuery, query_handlers.ReadMeetingQueryHandler)
|
|
338
454
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
self.action = action
|
|
455
|
+
def init_events(mapper: events.EventMap) -> None:
|
|
456
|
+
mapper.bind(events.NotificationEvent[event_models.NotificationMeetingRoomClosed], event_handlers.MeetingRoomClosedNotificationHandler)
|
|
457
|
+
mapper.bind(events.NotificationEvent[event_models.ECSTMeetingRoomClosed], event_handlers.UpdateMeetingRoomReadModelHandler)
|
|
458
|
+
```
|
|
344
459
|
|
|
345
|
-
|
|
346
|
-
return {"user_id": self.user_id, "action": self.action}
|
|
460
|
+
**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):
|
|
347
461
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
462
|
+
```python
|
|
463
|
+
def payment_mapper(mapper: cqrs.RequestMap) -> None:
|
|
464
|
+
mapper.bind(
|
|
465
|
+
ProcessPaymentCommand,
|
|
466
|
+
[
|
|
467
|
+
CreditCardPaymentHandler,
|
|
468
|
+
PayPalPaymentHandler,
|
|
469
|
+
DefaultPaymentHandler, # Fallback
|
|
470
|
+
],
|
|
471
|
+
)
|
|
472
|
+
```
|
|
351
473
|
|
|
352
|
-
|
|
353
|
-
def __init__(self, result: str, status: int):
|
|
354
|
-
self.result = result
|
|
355
|
-
self.status = status
|
|
474
|
+
**Streaming handler** — bind a command to a `StreamingRequestHandler` (results are yielded as they become available):
|
|
356
475
|
|
|
357
|
-
|
|
358
|
-
|
|
476
|
+
```python
|
|
477
|
+
def commands_mapper(mapper: cqrs.RequestMap) -> None:
|
|
478
|
+
mapper.bind(ProcessOrdersCommand, ProcessOrdersCommandHandler) # StreamingRequestHandler
|
|
479
|
+
```
|
|
359
480
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
481
|
+
**Saga (including with fallback)** — bind the saga context type to the saga class in `SagaMap`:
|
|
482
|
+
|
|
483
|
+
```python
|
|
484
|
+
def saga_mapper(mapper: cqrs.SagaMap) -> None:
|
|
485
|
+
mapper.bind(OrderContext, OrderSaga)
|
|
486
|
+
mapper.bind(OrderContext, OrderSagaWithFallback)
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
## DI container
|
|
490
|
+
|
|
491
|
+
Use the following example to set up dependency injection in your command, query and event handlers. This will make
|
|
492
|
+
dependency management simpler.
|
|
493
|
+
|
|
494
|
+
The package supports two DI container libraries:
|
|
495
|
+
|
|
496
|
+
### di library
|
|
497
|
+
|
|
498
|
+
```python
|
|
499
|
+
import di
|
|
500
|
+
...
|
|
501
|
+
|
|
502
|
+
def setup_di() -> di.Container:
|
|
503
|
+
"""
|
|
504
|
+
Binds implementations to dependencies
|
|
505
|
+
"""
|
|
506
|
+
container = di.Container()
|
|
507
|
+
container.bind(
|
|
508
|
+
di.bind_by_type(
|
|
509
|
+
dependent.Dependent(cqrs.SqlAlchemyOutboxedEventRepository, scope="request"),
|
|
510
|
+
cqrs.OutboxedEventRepository
|
|
511
|
+
)
|
|
512
|
+
)
|
|
513
|
+
container.bind(
|
|
514
|
+
di.bind_by_type(
|
|
515
|
+
dependent.Dependent(MeetingAPIImplementaion, scope="request"),
|
|
516
|
+
MeetingAPIProtocol
|
|
517
|
+
)
|
|
518
|
+
)
|
|
519
|
+
return container
|
|
363
520
|
```
|
|
364
521
|
|
|
365
|
-
A complete example can be found in
|
|
522
|
+
A complete example can be found in
|
|
523
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/dependency_injection.py)
|
|
524
|
+
|
|
525
|
+
### dependency-injector library
|
|
526
|
+
|
|
527
|
+
The package also supports [dependency-injector](https://github.com/ets-labs/python-dependency-injector) library.
|
|
528
|
+
You can use `DependencyInjectorCQRSContainer` adapter to integrate dependency-injector containers with python-cqrs.
|
|
529
|
+
|
|
530
|
+
```python
|
|
531
|
+
from dependency_injector import containers, providers
|
|
532
|
+
from cqrs.container.dependency_injector import DependencyInjectorCQRSContainer
|
|
533
|
+
|
|
534
|
+
class ApplicationContainer(containers.DeclarativeContainer):
|
|
535
|
+
# Define your providers
|
|
536
|
+
service = providers.Factory(ServiceImplementation)
|
|
537
|
+
|
|
538
|
+
# Create CQRS container adapter
|
|
539
|
+
cqrs_container = DependencyInjectorCQRSContainer(ApplicationContainer())
|
|
540
|
+
|
|
541
|
+
# Use with bootstrap
|
|
542
|
+
mediator = bootstrap.bootstrap(
|
|
543
|
+
di_container=cqrs_container,
|
|
544
|
+
commands_mapper=commands_mapper,
|
|
545
|
+
...
|
|
546
|
+
)
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
Complete examples can be found in:
|
|
550
|
+
- [Simple example](https://github.com/vadikko2/python-cqrs/blob/master/examples/dependency_injector_integration_simple_example.py)
|
|
551
|
+
- [Practical example with FastAPI](https://github.com/vadikko2/python-cqrs/blob/master/examples/dependency_injector_integration_practical_example.py)
|
|
552
|
+
|
|
553
|
+
## Bootstrap
|
|
554
|
+
|
|
555
|
+
The `python-cqrs` package implements a set of bootstrap utilities designed to simplify the initial configuration of an
|
|
556
|
+
application.
|
|
557
|
+
|
|
558
|
+
```python
|
|
559
|
+
import functools
|
|
560
|
+
|
|
561
|
+
from cqrs.events import bootstrap as event_bootstrap
|
|
562
|
+
from cqrs.requests import bootstrap as request_bootstrap
|
|
563
|
+
|
|
564
|
+
from app import dependencies, mapping, orm
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
@functools.lru_cache
|
|
568
|
+
def mediator_factory():
|
|
569
|
+
return request_bootstrap.bootstrap(
|
|
570
|
+
di_container=dependencies.setup_di(),
|
|
571
|
+
commands_mapper=mapping.init_commands,
|
|
572
|
+
queries_mapper=mapping.init_queries,
|
|
573
|
+
domain_events_mapper=mapping.init_events,
|
|
574
|
+
on_startup=[orm.init_store_event_mapper],
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
@functools.lru_cache
|
|
579
|
+
def event_mediator_factory():
|
|
580
|
+
return event_bootstrap.bootstrap(
|
|
581
|
+
di_container=dependencies.setup_di(),
|
|
582
|
+
events_mapper=mapping.init_events,
|
|
583
|
+
on_startup=[orm.init_store_event_mapper],
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
@functools.lru_cache
|
|
588
|
+
def saga_mediator_factory():
|
|
589
|
+
return saga_bootstrap.bootstrap(
|
|
590
|
+
di_container=dependencies.setup_di(),
|
|
591
|
+
sagas_mapper=mapping.init_sagas,
|
|
592
|
+
domain_events_mapper=mapping.init_events,
|
|
593
|
+
saga_storage=MemorySagaStorage(),
|
|
594
|
+
)
|
|
595
|
+
```
|
|
366
596
|
|
|
367
597
|
## Saga Pattern
|
|
368
598
|
|
|
@@ -515,68 +745,7 @@ sequence_diagram = generator.sequence()
|
|
|
515
745
|
class_diagram = generator.class_diagram()
|
|
516
746
|
```
|
|
517
747
|
|
|
518
|
-
Complete example: [Saga Mermaid Diagrams](https://github.com/vadikko2/cqrs/blob/master/examples/saga_mermaid.py)
|
|
519
|
-
|
|
520
|
-
## Event Handlers
|
|
521
|
-
|
|
522
|
-
Event handlers are designed to process `Notification` and `ECST` events that are consumed from the broker.
|
|
523
|
-
To configure event handling, you need to implement a broker consumer on the side of your application.
|
|
524
|
-
Below is an example of `Kafka event consuming` that can be used in the Presentation Layer.
|
|
525
|
-
|
|
526
|
-
```python
|
|
527
|
-
class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
|
|
528
|
-
def __init__(self):
|
|
529
|
-
self._events = []
|
|
530
|
-
|
|
531
|
-
@property
|
|
532
|
-
def events(self):
|
|
533
|
-
return self._events
|
|
534
|
-
|
|
535
|
-
async def handle(self, request: JoinMeetingCommand) -> None:
|
|
536
|
-
STORAGE[request.meeting_id].append(request.user_id)
|
|
537
|
-
self._events.append(
|
|
538
|
-
UserJoined(user_id=request.user_id, meeting_id=request.meeting_id),
|
|
539
|
-
)
|
|
540
|
-
print(f"User {request.user_id} joined meeting {request.meeting_id}")
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
class UserJoinedEventHandler(cqrs.EventHandler[UserJoined]):
|
|
544
|
-
async def handle(self, event: UserJoined) -> None:
|
|
545
|
-
print(f"Handle user {event.user_id} joined meeting {event.meeting_id} event")
|
|
546
|
-
```
|
|
547
|
-
|
|
548
|
-
A complete example can be found in
|
|
549
|
-
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/domain_event_handler.py)
|
|
550
|
-
|
|
551
|
-
### Parallel Event Processing
|
|
552
|
-
|
|
553
|
-
Both `RequestMediator` and `StreamingRequestMediator` support parallel processing of domain events. You can control
|
|
554
|
-
the number of event handlers that run simultaneously using the `max_concurrent_event_handlers` parameter.
|
|
555
|
-
|
|
556
|
-
This feature is especially useful when:
|
|
557
|
-
- Multiple event handlers need to process events independently
|
|
558
|
-
- You want to improve performance by processing events concurrently
|
|
559
|
-
- You need to limit resource consumption by controlling concurrency
|
|
560
|
-
|
|
561
|
-
**Configuration:**
|
|
562
|
-
|
|
563
|
-
```python
|
|
564
|
-
from cqrs.requests import bootstrap
|
|
565
|
-
|
|
566
|
-
mediator = bootstrap.bootstrap_streaming(
|
|
567
|
-
di_container=container,
|
|
568
|
-
commands_mapper=commands_mapper,
|
|
569
|
-
domain_events_mapper=domain_events_mapper,
|
|
570
|
-
message_broker=broker,
|
|
571
|
-
max_concurrent_event_handlers=3, # Process up to 3 events in parallel
|
|
572
|
-
concurrent_event_handle_enable=True, # Enable parallel processing
|
|
573
|
-
)
|
|
574
|
-
```
|
|
575
|
-
|
|
576
|
-
> [!TIP]
|
|
577
|
-
> - Set `max_concurrent_event_handlers` to limit the number of simultaneously running event handlers
|
|
578
|
-
> - Set `concurrent_event_handle_enable=False` to disable parallel processing and process events sequentially
|
|
579
|
-
> - The default value for `max_concurrent_event_handlers` is `10` for `StreamingRequestMediator` and `1` for `RequestMediator`
|
|
748
|
+
Complete example: [Saga Mermaid Diagrams](https://github.com/vadikko2/python-cqrs/blob/master/examples/saga_mermaid.py)
|
|
580
749
|
|
|
581
750
|
## Producing Notification Events
|
|
582
751
|
|
|
@@ -616,7 +785,7 @@ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
|
|
|
616
785
|
```
|
|
617
786
|
|
|
618
787
|
A complete example can be found in
|
|
619
|
-
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/event_producing.py)
|
|
788
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/event_producing.py)
|
|
620
789
|
|
|
621
790
|
After processing the command/request, if there are any Notification/ECST events,
|
|
622
791
|
the EventEmitter is invoked to produce the events via the message broker.
|
|
@@ -651,13 +820,6 @@ The package implements the [Transactional Outbox](https://microservices.io/patte
|
|
|
651
820
|
pattern, which ensures that messages are produced to the broker according to the at-least-once semantics.
|
|
652
821
|
|
|
653
822
|
```python
|
|
654
|
-
def do_some_logic(meeting_room_id: int, session: sql_session.AsyncSession):
|
|
655
|
-
"""
|
|
656
|
-
Make changes to the database
|
|
657
|
-
"""
|
|
658
|
-
session.add(...)
|
|
659
|
-
|
|
660
|
-
|
|
661
823
|
class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
|
|
662
824
|
def __init__(self, outbox: cqrs.OutboxedEventRepository):
|
|
663
825
|
self.outbox = outbox
|
|
@@ -668,35 +830,33 @@ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
|
|
|
668
830
|
|
|
669
831
|
async def handle(self, request: JoinMeetingCommand) -> None:
|
|
670
832
|
print(f"User {request.user_id} joined meeting {request.meeting_id}")
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
meeting_id=request.meeting_id,
|
|
681
|
-
),
|
|
833
|
+
# Outbox repository is bound to a session (e.g. via DI request scope).
|
|
834
|
+
# add() takes only the event; commit() persists the outbox and your changes.
|
|
835
|
+
self.outbox.add(
|
|
836
|
+
cqrs.NotificationEvent[UserJoinedNotificationPayload](
|
|
837
|
+
event_name="UserJoined",
|
|
838
|
+
topic="user_notification_events",
|
|
839
|
+
payload=UserJoinedNotificationPayload(
|
|
840
|
+
user_id=request.user_id,
|
|
841
|
+
meeting_id=request.meeting_id,
|
|
682
842
|
),
|
|
683
|
-
)
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
),
|
|
843
|
+
),
|
|
844
|
+
)
|
|
845
|
+
self.outbox.add(
|
|
846
|
+
cqrs.NotificationEvent[UserJoinedECSTPayload](
|
|
847
|
+
event_name="UserJoined",
|
|
848
|
+
topic="user_ecst_events",
|
|
849
|
+
payload=UserJoinedECSTPayload(
|
|
850
|
+
user_id=request.user_id,
|
|
851
|
+
meeting_id=request.meeting_id,
|
|
693
852
|
),
|
|
694
|
-
)
|
|
695
|
-
|
|
853
|
+
),
|
|
854
|
+
)
|
|
855
|
+
await self.outbox.commit()
|
|
696
856
|
```
|
|
697
857
|
|
|
698
858
|
A complete example can be found in
|
|
699
|
-
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/save_events_into_outbox.py)
|
|
859
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/save_events_into_outbox.py)
|
|
700
860
|
|
|
701
861
|
> [!TIP]
|
|
702
862
|
> You can specify the name of the Outbox table using the environment variable `OUTBOX_SQLA_TABLE`.
|
|
@@ -704,8 +864,8 @@ the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/save_e
|
|
|
704
864
|
|
|
705
865
|
> [!TIP]
|
|
706
866
|
> If you use the protobuf events you should specify `OutboxedEventRepository`
|
|
707
|
-
> by [protobuf serialize](https://github.com/vadikko2/cqrs/blob/master/src/cqrs/serializers/protobuf.py). A complete example can be found in
|
|
708
|
-
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/save_proto_events_into_outbox.py)
|
|
867
|
+
> by [protobuf serialize](https://github.com/vadikko2/python-cqrs/blob/master/src/cqrs/serializers/protobuf.py). A complete example can be found in
|
|
868
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/save_proto_events_into_outbox.py)
|
|
709
869
|
|
|
710
870
|
## Producing Events from Outbox to Kafka
|
|
711
871
|
|
|
@@ -731,23 +891,20 @@ broker = kafka.KafkaMessageBroker(
|
|
|
731
891
|
producer=kafka_adapters.kafka_producer_factory(dsn="localhost:9092"),
|
|
732
892
|
)
|
|
733
893
|
|
|
734
|
-
|
|
894
|
+
# SqlAlchemyOutboxedEventRepository expects (session, compressor), not session_factory.
|
|
895
|
+
async with session_factory() as session:
|
|
896
|
+
repository = cqrs.SqlAlchemyOutboxedEventRepository(session, zlib.ZlibCompressor())
|
|
897
|
+
producer = cqrs.EventProducer(broker, repository)
|
|
735
898
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
await producer.repository.commit()
|
|
742
|
-
await asyncio.sleep(10)
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
loop = asyncio.get_event_loop()
|
|
746
|
-
loop.run_until_complete(periodically_task())
|
|
899
|
+
async for messages in producer.event_batch_generator():
|
|
900
|
+
for message in messages:
|
|
901
|
+
await producer.send_message(message)
|
|
902
|
+
await producer.repository.commit()
|
|
903
|
+
await asyncio.sleep(10)
|
|
747
904
|
```
|
|
748
905
|
|
|
749
906
|
A complete example can be found in
|
|
750
|
-
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/kafka_outboxed_event_producing.py)
|
|
907
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/kafka_outboxed_event_producing.py)
|
|
751
908
|
|
|
752
909
|
## Transaction log tailing
|
|
753
910
|
|
|
@@ -761,129 +918,71 @@ The current version of the python-cqrs package does not support the implementati
|
|
|
761
918
|
> which allows you to produce all newly created events within the Outbox storage directly to the corresponding topic in
|
|
762
919
|
> Kafka (or any other broker).
|
|
763
920
|
|
|
764
|
-
##
|
|
765
|
-
|
|
766
|
-
Use the following example to set up dependency injection in your command, query and event handlers. This will make
|
|
767
|
-
dependency management simpler.
|
|
768
|
-
|
|
769
|
-
The package supports two DI container libraries:
|
|
921
|
+
## Event Handlers
|
|
770
922
|
|
|
771
|
-
|
|
923
|
+
Event handlers are designed to process `Notification` and `ECST` events that are consumed from the broker.
|
|
924
|
+
To configure event handling, you need to implement a broker consumer on the side of your application.
|
|
925
|
+
Below is an example of `Kafka event consuming` that can be used in the Presentation Layer.
|
|
772
926
|
|
|
773
927
|
```python
|
|
774
|
-
|
|
775
|
-
|
|
928
|
+
class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
|
|
929
|
+
def __init__(self):
|
|
930
|
+
self._events = []
|
|
776
931
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
cqrs.OutboxedEventRepository
|
|
786
|
-
)
|
|
787
|
-
)
|
|
788
|
-
container.bind(
|
|
789
|
-
di.bind_by_type(
|
|
790
|
-
dependent.Dependent(MeetingAPIImplementaion, scope="request"),
|
|
791
|
-
MeetingAPIProtocol
|
|
932
|
+
@property
|
|
933
|
+
def events(self):
|
|
934
|
+
return self._events
|
|
935
|
+
|
|
936
|
+
async def handle(self, request: JoinMeetingCommand) -> None:
|
|
937
|
+
STORAGE[request.meeting_id].append(request.user_id)
|
|
938
|
+
self._events.append(
|
|
939
|
+
UserJoined(user_id=request.user_id, meeting_id=request.meeting_id),
|
|
792
940
|
)
|
|
793
|
-
|
|
794
|
-
|
|
941
|
+
print(f"User {request.user_id} joined meeting {request.meeting_id}")
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
class UserJoinedEventHandler(cqrs.EventHandler[UserJoined]):
|
|
945
|
+
async def handle(self, event: UserJoined) -> None:
|
|
946
|
+
print(f"Handle user {event.user_id} joined meeting {event.meeting_id} event")
|
|
795
947
|
```
|
|
796
948
|
|
|
797
949
|
A complete example can be found in
|
|
798
|
-
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/
|
|
950
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/domain_event_handler.py)
|
|
799
951
|
|
|
800
|
-
###
|
|
952
|
+
### Parallel Event Processing
|
|
801
953
|
|
|
802
|
-
|
|
803
|
-
|
|
954
|
+
Both `RequestMediator` and `StreamingRequestMediator` support parallel processing of domain events. You can control
|
|
955
|
+
the number of event handlers that run simultaneously using the `max_concurrent_event_handlers` parameter.
|
|
804
956
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
957
|
+
This feature is especially useful when:
|
|
958
|
+
- Multiple event handlers need to process events independently
|
|
959
|
+
- You want to improve performance by processing events concurrently
|
|
960
|
+
- You need to limit resource consumption by controlling concurrency
|
|
808
961
|
|
|
809
|
-
|
|
810
|
-
# Define your providers
|
|
811
|
-
service = providers.Factory(ServiceImplementation)
|
|
962
|
+
**Configuration:**
|
|
812
963
|
|
|
813
|
-
|
|
814
|
-
|
|
964
|
+
```python
|
|
965
|
+
from cqrs.requests import bootstrap
|
|
815
966
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
di_container=cqrs_container,
|
|
967
|
+
mediator = bootstrap.bootstrap_streaming(
|
|
968
|
+
di_container=container,
|
|
819
969
|
commands_mapper=commands_mapper,
|
|
820
|
-
|
|
970
|
+
domain_events_mapper=domain_events_mapper,
|
|
971
|
+
message_broker=broker,
|
|
972
|
+
max_concurrent_event_handlers=3, # Process up to 3 events in parallel
|
|
973
|
+
concurrent_event_handle_enable=True, # Enable parallel processing
|
|
821
974
|
)
|
|
822
975
|
```
|
|
823
976
|
|
|
824
|
-
|
|
825
|
-
-
|
|
826
|
-
-
|
|
827
|
-
|
|
828
|
-
## Mapping
|
|
829
|
-
|
|
830
|
-
To bind commands, queries and events with specific handlers, you can use the registries `EventMap` and `RequestMap`.
|
|
831
|
-
|
|
832
|
-
```python
|
|
833
|
-
from cqrs import requests, events
|
|
834
|
-
|
|
835
|
-
from app import commands, command_handlers
|
|
836
|
-
from app import queries, query_handlers
|
|
837
|
-
from app import events as event_models, event_handlers
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
def init_commands(mapper: requests.RequestMap) -> None:
|
|
841
|
-
mapper.bind(commands.JoinMeetingCommand, command_handlers.JoinMeetingCommandHandler)
|
|
842
|
-
|
|
843
|
-
def init_queries(mapper: requests.RequestMap) -> None:
|
|
844
|
-
mapper.bind(queries.ReadMeetingQuery, query_handlers.ReadMeetingQueryHandler)
|
|
845
|
-
|
|
846
|
-
def init_events(mapper: events.EventMap) -> None:
|
|
847
|
-
mapper.bind(events.NotificationEvent[events_models.NotificationMeetingRoomClosed], event_handlers.MeetingRoomClosedNotificationHandler)
|
|
848
|
-
mapper.bind(events.NotificationEvent[event_models.ECSTMeetingRoomClosed], event_handlers.UpdateMeetingRoomReadModelHandler)
|
|
849
|
-
```
|
|
850
|
-
|
|
851
|
-
## Bootstrap
|
|
852
|
-
|
|
853
|
-
The `python-cqrs` package implements a set of bootstrap utilities designed to simplify the initial configuration of an
|
|
854
|
-
application.
|
|
855
|
-
|
|
856
|
-
```python
|
|
857
|
-
import functools
|
|
858
|
-
|
|
859
|
-
from cqrs.events import bootstrap as event_bootstrap
|
|
860
|
-
from cqrs.requests import bootstrap as request_bootstrap
|
|
861
|
-
|
|
862
|
-
from app import dependencies, mapping, orm
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
@functools.lru_cache
|
|
866
|
-
def mediator_factory():
|
|
867
|
-
return request_bootstrap.bootstrap(
|
|
868
|
-
di_container=dependencies.setup_di(),
|
|
869
|
-
commands_mapper=mapping.init_commands,
|
|
870
|
-
queries_mapper=mapping.init_queries,
|
|
871
|
-
domain_events_mapper=mapping.init_events,
|
|
872
|
-
on_startup=[orm.init_store_event_mapper],
|
|
873
|
-
)
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
@functools.lru_cache
|
|
877
|
-
def event_mediator_factory():
|
|
878
|
-
return event_bootstrap.bootstrap(
|
|
879
|
-
di_container=dependencies.setup_di(),
|
|
880
|
-
events_mapper=mapping.init_events,
|
|
881
|
-
on_startup=[orm.init_store_event_mapper],
|
|
882
|
-
)
|
|
883
|
-
```
|
|
977
|
+
> [!TIP]
|
|
978
|
+
> - Set `max_concurrent_event_handlers` to limit the number of simultaneously running event handlers
|
|
979
|
+
> - Set `concurrent_event_handle_enable=False` to disable parallel processing and process events sequentially
|
|
980
|
+
> - The default value for `max_concurrent_event_handlers` is `10` for `StreamingRequestMediator` and `1` for `RequestMediator`
|
|
884
981
|
|
|
885
982
|
## Integration with presentation layers
|
|
886
983
|
|
|
984
|
+
The framework is ready for integration with **FastAPI** and **FastStream**.
|
|
985
|
+
|
|
887
986
|
> [!TIP]
|
|
888
987
|
> I recommend reading the useful
|
|
889
988
|
> paper [Onion Architecture Used in Software Development](https://www.researchgate.net/publication/371006360_Onion_Architecture_Used_in_Software_Development).
|
|
@@ -900,7 +999,7 @@ In this case you can use python-cqrs to route requests to the appropriate handle
|
|
|
900
999
|
import fastapi
|
|
901
1000
|
import pydantic
|
|
902
1001
|
|
|
903
|
-
from app import
|
|
1002
|
+
from app import dependencies, commands
|
|
904
1003
|
|
|
905
1004
|
router = fastapi.APIRouter(prefix="/meetings")
|
|
906
1005
|
|
|
@@ -916,7 +1015,7 @@ async def join_metting(
|
|
|
916
1015
|
```
|
|
917
1016
|
|
|
918
1017
|
A complete example can be found in
|
|
919
|
-
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/fastapi_integration.py)
|
|
1018
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/fastapi_integration.py)
|
|
920
1019
|
|
|
921
1020
|
### Kafka events consuming
|
|
922
1021
|
|
|
@@ -1012,12 +1111,71 @@ async def process_files_stream(
|
|
|
1012
1111
|
```
|
|
1013
1112
|
|
|
1014
1113
|
A complete example can be found in
|
|
1015
|
-
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/fastapi_sse_streaming.py)
|
|
1114
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/fastapi_sse_streaming.py)
|
|
1016
1115
|
|
|
1017
1116
|
## Protobuf messaging
|
|
1018
1117
|
|
|
1019
|
-
The `python-cqrs` package supports integration with [protobuf](https://developers.google.com/protocol-buffers/)
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1118
|
+
The `python-cqrs` package supports integration with [protobuf](https://developers.google.com/protocol-buffers/).
|
|
1119
|
+
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.
|
|
1120
|
+
|
|
1121
|
+
Example (assuming generated `user_joined_pb2` from your `.proto` with fields `event_id`, `event_timestamp`, `event_name`, `payload`):
|
|
1122
|
+
|
|
1123
|
+
```python
|
|
1124
|
+
import uuid
|
|
1125
|
+
from datetime import datetime
|
|
1126
|
+
|
|
1127
|
+
import cqrs
|
|
1128
|
+
from app.generated import user_joined_pb2 # generated from .proto
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
class UserJoinedPayload(cqrs.Response):
|
|
1132
|
+
user_id: str
|
|
1133
|
+
meeting_id: str
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
class UserJoinedNotificationEvent(cqrs.NotificationEvent[UserJoinedPayload]):
|
|
1137
|
+
"""Event with Protobuf serialization support."""
|
|
1138
|
+
|
|
1139
|
+
event_name: str = "UserJoined"
|
|
1140
|
+
|
|
1141
|
+
def proto(self):
|
|
1142
|
+
msg = user_joined_pb2.UserJoinedNotification()
|
|
1143
|
+
msg.event_id = str(self.event_id)
|
|
1144
|
+
msg.event_timestamp = self.event_timestamp.isoformat()
|
|
1145
|
+
msg.event_name = self.event_name
|
|
1146
|
+
msg.payload.user_id = self.payload.user_id
|
|
1147
|
+
msg.payload.meeting_id = self.payload.meeting_id
|
|
1148
|
+
return msg
|
|
1149
|
+
|
|
1150
|
+
@classmethod
|
|
1151
|
+
def from_proto(cls, proto_msg):
|
|
1152
|
+
return cls(
|
|
1153
|
+
event_id=uuid.UUID(proto_msg.event_id),
|
|
1154
|
+
event_timestamp=datetime.fromisoformat(proto_msg.event_timestamp),
|
|
1155
|
+
event_name=proto_msg.event_name,
|
|
1156
|
+
topic="user_notification_events",
|
|
1157
|
+
payload=UserJoinedPayload(
|
|
1158
|
+
user_id=proto_msg.payload.user_id,
|
|
1159
|
+
meeting_id=proto_msg.payload.meeting_id,
|
|
1160
|
+
),
|
|
1161
|
+
)
|
|
1162
|
+
```
|
|
1163
|
+
|
|
1164
|
+
## Contributing
|
|
1165
|
+
|
|
1166
|
+
Contributions are welcome. To develop locally:
|
|
1167
|
+
|
|
1168
|
+
1. Clone the repository and create a virtual environment.
|
|
1169
|
+
2. Install dev dependencies: `pip install -e ".[dev]"`.
|
|
1170
|
+
3. Run tests: `pytest`.
|
|
1171
|
+
4. Install pre-commit and run hooks: `pre-commit install && pre-commit run --all-files`.
|
|
1172
|
+
|
|
1173
|
+
The project uses [ruff](https://docs.astral.sh/ruff/) for linting and [pyright](https://microsoft.github.io/pyright/) for type checking.
|
|
1174
|
+
|
|
1175
|
+
## Changelog
|
|
1176
|
+
|
|
1177
|
+
Release notes and migration guides are published on [GitHub Releases](https://github.com/vadikko2/python-cqrs/releases).
|
|
1178
|
+
|
|
1179
|
+
## License
|
|
1180
|
+
|
|
1181
|
+
This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details.
|