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.
Files changed (93) hide show
  1. {python_cqrs-4.8.1/src/python_cqrs.egg-info → python_cqrs-4.10.0}/PKG-INFO +449 -291
  2. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/README.md +447 -290
  3. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/pyproject.toml +2 -1
  4. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/__init__.py +6 -0
  5. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/adapters/circuit_breaker.py +32 -44
  6. python_cqrs-4.10.0/src/cqrs/circuit_breaker.py +77 -0
  7. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/deserializers/json.py +1 -2
  8. python_cqrs-4.10.0/src/cqrs/dispatcher/event.py +96 -0
  9. python_cqrs-4.10.0/src/cqrs/dispatcher/request.py +139 -0
  10. python_cqrs-4.10.0/src/cqrs/dispatcher/streaming.py +146 -0
  11. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/events/__init__.py +3 -0
  12. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/events/event_emitter.py +57 -5
  13. python_cqrs-4.10.0/src/cqrs/events/fallback.py +92 -0
  14. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/events/map.py +10 -11
  15. python_cqrs-4.10.0/src/cqrs/generic_utils.py +43 -0
  16. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/middlewares/base.py +1 -1
  17. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/outbox/mock.py +1 -3
  18. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/outbox/sqlalchemy.py +3 -9
  19. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/requests/cor_request_handler.py +1 -1
  20. python_cqrs-4.10.0/src/cqrs/requests/fallback.py +98 -0
  21. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/requests/map.py +2 -0
  22. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/requests/mermaid.py +5 -15
  23. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/requests/request.py +10 -1
  24. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/requests/request_handler.py +1 -1
  25. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/compensation.py +38 -22
  26. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/execution.py +23 -27
  27. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/fallback.py +3 -3
  28. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/mermaid.py +7 -25
  29. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/recovery.py +1 -2
  30. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/saga.py +149 -74
  31. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/storage/__init__.py +2 -1
  32. python_cqrs-4.10.0/src/cqrs/saga/storage/memory.py +357 -0
  33. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/storage/protocol.py +141 -0
  34. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/storage/sqlalchemy.py +286 -15
  35. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/validation.py +4 -12
  36. python_cqrs-4.10.0/src/cqrs/types.py +11 -0
  37. {python_cqrs-4.8.1 → python_cqrs-4.10.0/src/python_cqrs.egg-info}/PKG-INFO +449 -291
  38. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/python_cqrs.egg-info/SOURCES.txt +4 -0
  39. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/python_cqrs.egg-info/requires.txt +1 -0
  40. python_cqrs-4.8.1/src/cqrs/dispatcher/event.py +0 -45
  41. python_cqrs-4.8.1/src/cqrs/dispatcher/request.py +0 -78
  42. python_cqrs-4.8.1/src/cqrs/dispatcher/streaming.py +0 -85
  43. python_cqrs-4.8.1/src/cqrs/saga/storage/memory.py +0 -169
  44. python_cqrs-4.8.1/src/cqrs/types.py +0 -18
  45. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/LICENSE +0 -0
  46. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/setup.cfg +0 -0
  47. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/adapters/__init__.py +0 -0
  48. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/adapters/amqp.py +0 -0
  49. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/adapters/kafka.py +0 -0
  50. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/adapters/protocol.py +0 -0
  51. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/compressors/__init__.py +0 -0
  52. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/compressors/protocol.py +0 -0
  53. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/compressors/zlib.py +0 -0
  54. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/container/__init__.py +0 -0
  55. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/container/dependency_injector.py +0 -0
  56. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/container/di.py +0 -0
  57. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/container/protocol.py +0 -0
  58. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/deserializers/__init__.py +0 -0
  59. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/deserializers/exceptions.py +0 -0
  60. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/dispatcher/__init__.py +0 -0
  61. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/dispatcher/exceptions.py +0 -0
  62. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/dispatcher/models.py +0 -0
  63. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/dispatcher/saga.py +0 -0
  64. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/events/bootstrap.py +0 -0
  65. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/events/event.py +0 -0
  66. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/events/event_handler.py +0 -0
  67. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/events/event_processor.py +0 -0
  68. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/mediator.py +0 -0
  69. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/message_brokers/__init__.py +0 -0
  70. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/message_brokers/amqp.py +0 -0
  71. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/message_brokers/devnull.py +0 -0
  72. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/message_brokers/kafka.py +0 -0
  73. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/message_brokers/protocol.py +0 -0
  74. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/middlewares/__init__.py +0 -0
  75. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/middlewares/logging.py +0 -0
  76. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/outbox/__init__.py +0 -0
  77. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/outbox/map.py +0 -0
  78. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/outbox/repository.py +0 -0
  79. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/producer.py +0 -0
  80. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/requests/__init__.py +0 -0
  81. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/requests/bootstrap.py +0 -0
  82. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/response.py +0 -0
  83. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/__init__.py +0 -0
  84. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/bootstrap.py +0 -0
  85. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/circuit_breaker.py +0 -0
  86. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/models.py +0 -0
  87. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/step.py +0 -0
  88. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/storage/enums.py +0 -0
  89. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/saga/storage/models.py +0 -0
  90. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/serializers/__init__.py +0 -0
  91. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/cqrs/serializers/default.py +0 -0
  92. {python_cqrs-4.8.1 → python_cqrs-4.10.0}/src/python_cqrs.egg-info/dependency_links.txt +0 -0
  93. {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.8.1
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%; width: 800px; border-radius: 16px; box-shadow: 0 8px 32px rgba(0, 102, 204, 0.2); display: block; margin: 2rem auto;"
66
+ style="max-width: 80%;"
67
67
  >
68
- </div>
69
68
  <h1>Python CQRS</h1>
70
- <h3>Event-Driven Architecture Framework for Distributed Systems</h3>
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
- This is a package for implementing the CQRS (Command Query Responsibility Segregation) pattern in Python applications.
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
- 1. Support for Pydantic [v2.*](https://docs.pydantic.dev/2.8/);
114
- 2. `Kafka` support using [aiokafka](https://github.com/aio-libs/aiokafka);
115
- 3. Added `EventMediator` for handling `Notification` and `ECST` events coming from the bus;
116
- 4. Redesigned the event and request mapping mechanism to handlers;
117
- 5. Added `bootstrap` for easy setup;
118
- 6. Added support for [Transaction Outbox](https://microservices.io/patterns/data/transactional-outbox.html), ensuring
119
- that `Notification` and `ECST` events are sent to the broker;
120
- 7. FastAPI supporting;
121
- 8. FastStream supporting;
122
- 9. [Protobuf](https://protobuf.dev/) events supporting;
123
- 10. `StreamingRequestMediator` and `StreamingRequestHandler` for handling streaming requests with real-time progress updates;
124
- 11. Parallel event processing with configurable concurrency limits;
125
- 12. Chain of Responsibility pattern support with `CORRequestHandler` for processing requests through multiple handlers in sequence;
126
- 13. Orchestrated Saga pattern support for managing distributed transactions with automatic compensation and recovery mechanisms;
127
- 14. Built-in Mermaid diagram generation, enabling automatic generation of Sequence and Class diagrams for documentation and visualization;
128
- 15. Flexible Request and Response types support - use Pydantic-based or Dataclass-based implementations, with the ability to mix and match types based on your needs.
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.events: list[Event] = []
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.events: list[Event] = []
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
- ## Request and Response Types
435
+ ## Mapping
313
436
 
314
- 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.
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 dataclasses
442
+ from cqrs import requests, events
318
443
 
319
- # Pydantic-based (default)
320
- class CreateUserCommand(cqrs.Request):
321
- username: str
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
- # Dataclass-based
329
- @dataclasses.dataclass
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
- @dataclasses.dataclass
335
- class ProductResponse(cqrs.DCResponse):
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
- # Custom implementation
340
- class CustomRequest(cqrs.IRequest):
341
- def __init__(self, user_id: str, action: str):
342
- self.user_id = user_id
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
- def to_dict(self) -> dict:
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
- @classmethod
349
- def from_dict(cls, **kwargs) -> "CustomRequest":
350
- return cls(user_id=kwargs["user_id"], action=kwargs["action"])
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
- class CustomResponse(cqrs.IResponse):
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
- def to_dict(self) -> dict:
358
- return {"result": self.result, "status": self.status}
476
+ ```python
477
+ def commands_mapper(mapper: cqrs.RequestMap) -> None:
478
+ mapper.bind(ProcessOrdersCommand, ProcessOrdersCommandHandler) # StreamingRequestHandler
479
+ ```
359
480
 
360
- @classmethod
361
- def from_dict(cls, **kwargs) -> "CustomResponse":
362
- return cls(result=kwargs["result"], status=kwargs["status"])
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 [request_response_types.py](https://github.com/vadikko2/cqrs/blob/master/examples/request_response_types.py)
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
- async with self.outbox as session:
672
- do_some_logic(request.meeting_id, session) # business logic
673
- self.outbox.add(
674
- session,
675
- cqrs.NotificationEvent[UserJoinedNotificationPayload](
676
- event_name="UserJoined",
677
- topic="user_notification_events",
678
- payload=UserJoinedNotificationPayload(
679
- user_id=request.user_id,
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
- self.outbox.add(
685
- session,
686
- cqrs.NotificationEvent[UserJoinedECSTPayload](
687
- event_name="UserJoined",
688
- topic="user_ecst_events",
689
- payload=UserJoinedECSTPayload(
690
- user_id=request.user_id,
691
- meeting_id=request.meeting_id,
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
- await self.outbox.commit(session)
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
- producer = cqrs.EventProducer(broker, cqrs.SqlAlchemyOutboxedEventRepository(session_factory, zlib.ZlibCompressor()))
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
- async def periodically_task():
738
- async for messages in producer.event_batch_generator():
739
- for message in messages:
740
- await producer.send_message(message)
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
- ## DI container
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
- ### di library
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
- import di
775
- ...
928
+ class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
929
+ def __init__(self):
930
+ self._events = []
776
931
 
777
- def setup_di() -> di.Container:
778
- """
779
- Binds implementations to dependencies
780
- """
781
- container = di.Container()
782
- container.bind(
783
- di.bind_by_type(
784
- dependent.Dependent(cqrs.SqlAlchemyOutboxedEventRepository, scope="request"),
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
- return container
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/dependency_injection.py)
950
+ the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/domain_event_handler.py)
799
951
 
800
- ### dependency-injector library
952
+ ### Parallel Event Processing
801
953
 
802
- The package also supports [dependency-injector](https://github.com/ets-labs/python-dependency-injector) library.
803
- You can use `DependencyInjectorCQRSContainer` adapter to integrate dependency-injector containers with python-cqrs.
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
- ```python
806
- from dependency_injector import containers, providers
807
- from cqrs.container.dependency_injector import DependencyInjectorCQRSContainer
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
- class ApplicationContainer(containers.DeclarativeContainer):
810
- # Define your providers
811
- service = providers.Factory(ServiceImplementation)
962
+ **Configuration:**
812
963
 
813
- # Create CQRS container adapter
814
- cqrs_container = DependencyInjectorCQRSContainer(ApplicationContainer())
964
+ ```python
965
+ from cqrs.requests import bootstrap
815
966
 
816
- # Use with bootstrap
817
- mediator = bootstrap.bootstrap(
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
- Complete examples can be found in:
825
- - [Simple example](https://github.com/vadikko2/cqrs/blob/master/examples/dependency_injector_integration_simple_example.py)
826
- - [Practical example with FastAPI](https://github.com/vadikko2/cqrs/blob/master/examples/dependency_injector_integration_practical_example.py)
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 dependecies, commands
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
- Protocol buffers are Google's language-neutral, platform-neutral, extensible mechanism for serializing structured data
1021
- think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use
1022
- special generated source code to easily write and read your structured data to and from a variety of data streams and
1023
- using a variety of languages.
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.