python-cqrs 4.9.0__tar.gz → 4.10.1__tar.gz

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