python-cqrs 0.0.14__tar.gz → 0.0.17__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 (51) hide show
  1. python_cqrs-0.0.17/PKG-INFO +403 -0
  2. python_cqrs-0.0.17/README.md +365 -0
  3. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/pyproject.toml +3 -3
  4. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/__init__.py +7 -1
  5. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/container/di.py +0 -3
  6. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/events/bootstrap.py +4 -3
  7. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/requests/bootstrap.py +5 -4
  8. python_cqrs-0.0.17/src/python_cqrs.egg-info/PKG-INFO +403 -0
  9. python_cqrs-0.0.14/PKG-INFO +0 -237
  10. python_cqrs-0.0.14/README.md +0 -199
  11. python_cqrs-0.0.14/src/python_cqrs.egg-info/PKG-INFO +0 -237
  12. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/LICENSE +0 -0
  13. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/setup.cfg +0 -0
  14. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/adapters/__init__.py +0 -0
  15. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/adapters/amqp.py +0 -0
  16. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/adapters/kafka.py +0 -0
  17. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/compressors/__init__.py +0 -0
  18. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/compressors/protocol.py +0 -0
  19. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/compressors/zlib.py +0 -0
  20. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/container/__init__.py +0 -0
  21. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/container/protocol.py +0 -0
  22. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/dispatcher/__init__.py +0 -0
  23. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/dispatcher/dispatcher.py +0 -0
  24. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/events/__init__.py +0 -0
  25. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/events/event.py +0 -0
  26. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/events/event_emitter.py +0 -0
  27. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/events/event_handler.py +0 -0
  28. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/events/map.py +0 -0
  29. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/mediator.py +0 -0
  30. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/message_brokers/__init__.py +0 -0
  31. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/message_brokers/amqp.py +0 -0
  32. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/message_brokers/devnull.py +0 -0
  33. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/message_brokers/kafka.py +0 -0
  34. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/message_brokers/protocol.py +0 -0
  35. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/middlewares/__init__.py +0 -0
  36. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/middlewares/base.py +0 -0
  37. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/middlewares/logging.py +0 -0
  38. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/outbox/__init__.py +0 -0
  39. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/outbox/producer.py +0 -0
  40. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/outbox/protocol.py +0 -0
  41. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/outbox/repository.py +0 -0
  42. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/outbox/sqlalchemy.py +0 -0
  43. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/requests/__init__.py +0 -0
  44. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/requests/map.py +0 -0
  45. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/requests/request.py +0 -0
  46. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/requests/request_handler.py +0 -0
  47. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/cqrs/response.py +0 -0
  48. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/python_cqrs.egg-info/SOURCES.txt +0 -0
  49. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/python_cqrs.egg-info/dependency_links.txt +0 -0
  50. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/python_cqrs.egg-info/requires.txt +0 -0
  51. {python_cqrs-0.0.14 → python_cqrs-0.0.17}/src/python_cqrs.egg-info/top_level.txt +0 -0
@@ -0,0 +1,403 @@
1
+ Metadata-Version: 2.1
2
+ Name: python-cqrs
3
+ Version: 0.0.17
4
+ Summary: Python CQRS pattern implementation
5
+ Author: Nikita Kunov
6
+ Author-email: Dmitriy Kutlubaev <kutlubaev00@mail.ru>, Vadim Kozyrevskiy <vadikko2@mail.ru>
7
+ Maintainer-email: Vadim Kozyrevskiy <vadikko2@mail.ru>
8
+ Project-URL: Issues, https://github.com/vadikko2/python-cqrs/issues
9
+ Project-URL: Repository, https://github.com/vadikko2/python-cqrs
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: pydantic==2.*
20
+ Requires-Dist: orjson==3.9.15
21
+ Requires-Dist: aio-pika==9.3.0
22
+ Requires-Dist: di[anyio]==0.79.2
23
+ Requires-Dist: sqlalchemy[asyncio]==2.0.*
24
+ Requires-Dist: retry-async==0.1.4
25
+ Provides-Extra: dev
26
+ Requires-Dist: pre-commit==3.8.0; extra == "dev"
27
+ Requires-Dist: pyright==1.1.377; extra == "dev"
28
+ Requires-Dist: ruff==0.6.2; extra == "dev"
29
+ Requires-Dist: aiokafka==0.10.0; extra == "dev"
30
+ Requires-Dist: pytest~=7.4.2; extra == "dev"
31
+ Requires-Dist: pytest-asyncio~=0.21.1; extra == "dev"
32
+ Requires-Dist: pytest-env==0.6.2; extra == "dev"
33
+ Requires-Dist: python-dotenv==1.0.1; extra == "dev"
34
+ Requires-Dist: cryptography==42.0.2; extra == "dev"
35
+ Requires-Dist: asyncmy==0.2.9; extra == "dev"
36
+ Provides-Extra: kafka
37
+ Requires-Dist: aiokafka==0.10.0; extra == "kafka"
38
+
39
+ # Python CQRS pattern implementaion with Transaction Outbox supporting
40
+
41
+ ## Overview
42
+
43
+ This is a package for implementing the CQRS (Command Query Responsibility Segregation) pattern in Python applications.
44
+ It provides a set of abstractions and utilities to help separate read and write use cases, ensuring better scalability, performance, and maintainability of the application.
45
+
46
+ This package is a fork of the [diator](https://github.com/akhundMurad/diator) project ([documentation](https://akhundmurad.github.io/diator/)) with several enhancements:
47
+
48
+ 1. Support for Pydantic [v2.*](https://docs.pydantic.dev/2.8/);
49
+ 2. `Kafka` support using [aiokafka](https://github.com/aio-libs/aiokafka);
50
+ 3. Added `EventMediator` for handling `Notification` and `ECST` events coming from the bus;
51
+ 4. Redesigned the event and request mapping mechanism to handlers;
52
+ 5. Added `bootstrap` for easy setup;
53
+ 6. Added support for [Transaction Outbox](https://microservices.io/patterns/data/transactional-outbox.html), ensuring that `Notification` and `ECST` events are sent to the broker.
54
+
55
+
56
+ ## Request Handlers
57
+
58
+ Request handlers can be divided into two main types:
59
+
60
+ ### Command Handler
61
+
62
+ Command Handler executes the received command. The logic of the handler may include, for example, modifying the state of the domain model.
63
+ As a result of executing the command, an event may be produced to the broker.
64
+ > [!TIP]
65
+ > By default, the command handler does not return any result, but it is not mandatory.
66
+
67
+ ```python
68
+ from cqrs.requests.request_handler import RequestHandler
69
+ from cqrs.events.event import Event
70
+
71
+ class JoinMeetingCommandHandler(RequestHandler[JoinMeetingCommand, None]):
72
+
73
+ def __init__(self, meetings_api: MeetingAPIProtocol) -> None:
74
+ self._meetings_api = meetings_api
75
+ self.events: list[Event] = []
76
+
77
+ @property
78
+ def events(self) -> typing.List[events.Event]:
79
+ return self._events
80
+
81
+ async def handle(self, request: JoinMeetingCommand) -> None:
82
+ await self._meetings_api.join_user(request.user_id, request.meeting_id)
83
+ ```
84
+
85
+ ### Query handler
86
+
87
+ Query Handler returns a representation of the requested data, for example, from the [read model](https://radekmaziarka.pl/2018/01/08/cqrs-third-step-simple-read-model/#simple-read-model---to-the-rescue).
88
+ > [!TIP]
89
+ > The read model can be constructed based on domain events produced by the `Command Handler`.
90
+
91
+ ```python
92
+ from cqrs.requests.request_handler import RequestHandler
93
+ from cqrs.events.event import Event
94
+
95
+ class ReadMeetingQueryHandler(RequestHandler[ReadMeetingQuery, ReadMeetingQueryResult]):
96
+
97
+ def __init__(self, meetings_api: MeetingAPIProtocol) -> None:
98
+ self._meetings_api = meetings_api
99
+ self.events: list[Event] = []
100
+
101
+ @property
102
+ def events(self) -> typing.List[events.Event]:
103
+ return self._events
104
+
105
+ async def handle(self, request: ReadMeetingQuery) -> ReadMeetingQueryResult:
106
+ link = await self._meetings_api.get_link(request.meeting_id)
107
+ return ReadMeetingQueryResult(link=link, meeting_id=request.meeting_id)
108
+ ```
109
+
110
+
111
+ ## Event Handlers
112
+
113
+ Event handlers are designed to process `Notification` and `ECST` events that are consumed from the broker.
114
+ To configure event handling, you need to implement a broker consumer on the side of your application.
115
+ Below is an example of `Kafka event consuming` that can be used in the Presentation Layer.
116
+
117
+ ```python
118
+ from cqrs.events import EventHandler
119
+
120
+ class UserJoinedEventHandler(EventHandler[UserJoinedEventHandler]):
121
+
122
+ def __init__(self, meetings_api: MeetingAPIProtocol) -> None:
123
+ self._meetings_api = meetings_api
124
+
125
+ async def handle(self, event: UserJoinedEventHandler) -> None:
126
+ await self._meetings_api.notify_room(event.meeting_id, "New user joined!")
127
+ ```
128
+
129
+ ## Producing Notification/ECST Events
130
+
131
+ During the handling of a command event, messages of type `cqrs.NotificationEvent` or `cqrs.ECSTEvent` may be generated and then sent to the broker.
132
+
133
+ ```python
134
+ class CloseMeetingRoomCommandHandler(requests.RequestHandler[CloseMeetingRoomCommand, None]):
135
+
136
+ def __init__(self) -> None:
137
+ self._events: typing.List[events.Event] = []
138
+
139
+ @property
140
+ def events(self) -> typing.List[events.Event]:
141
+ return self._events
142
+
143
+ async def handle(self, request: CloseMeetingRoomCommand) -> None:
144
+ # some process
145
+ event = events.NotificationEvent(
146
+ event_topic="meeting_room_notifications",
147
+ event_name="meeteng_room_closed",
148
+ payload=dict(
149
+ meeting_room_id=request.meeting_room_id,
150
+ ),
151
+ )
152
+ self._events.append(event)
153
+ ```
154
+
155
+ After processing the command/request, if there are any Notification/ECST events,
156
+ the EventEmitter is invoked to produce the events via the message broker.
157
+
158
+ > [!WARNING]
159
+ > It is important to note that producing events using the events property parameter does not guarantee message delivery to the broker.
160
+ > In the event of broker unavailability or an exception occurring during message formation or sending, the message may be lost.
161
+ > This issue can potentially be addressed by configuring retry attempts for sending messages to the broker, but we recommend using the [Transaction Outbox](https://microservices.io/patterns/data/transactional-outbox.html) pattern,
162
+ > which is implemented in the current version of the python-cqrs package for this purpose.
163
+
164
+ ## Kafka broker
165
+
166
+ ```python
167
+ from cqrs.adapters import kafka as kafka_adapter
168
+ from cqrs.message_brokers import kafka as kafka_broker
169
+
170
+
171
+ producer = kafka_adapter.kafka_producer_factory(
172
+ dsn="localhost:9094",
173
+ topics=["test.topic1", "test.topic2"],
174
+ )
175
+ broker = kafka_broker.KafkaMessageBroker(producer)
176
+ await broker.send_message(...)
177
+ ```
178
+
179
+ ## Transactional Outbox
180
+
181
+ The package implements the [Transactional Outbox](https://microservices.io/patterns/data/transactional-outbox.html) pattern, which ensures that messages are produced to the broker according to the at-least-once semantics.
182
+
183
+
184
+ ```python
185
+ from sqlalchemy.ext.asyncio import session as sql_session
186
+ from cqrs import events
187
+
188
+ def do_some_logic(meeting_room_id: int, session: sql_session.AsyncSession):
189
+ """
190
+ Make changes to the database
191
+ """
192
+ session.add(...)
193
+
194
+
195
+ class CloseMeetingRoomCommandHandler(requests.RequestHandler[CloseMeetingRoomCommand, None]):
196
+
197
+ def __init__(self, repository: cqrs.SqlAlchemyOutboxedEventRepository):
198
+ self._repository = repository
199
+ self._events: typing.List[events.Event] = []
200
+
201
+ @property
202
+ def events(self):
203
+ return self._events
204
+
205
+ async def handle(self, request: CloseMeetingRoomCommand) -> None:
206
+ async with self._repository as session:
207
+ do_some_logic(request.meeting_room_id, session)
208
+ self.repository.add(
209
+ session,
210
+ events.ECSTEvent(
211
+ event_name="MeetingRoomClosed",
212
+ payload=dict(message="foo"),
213
+ ),
214
+ )
215
+ await self.repository.commit(session)
216
+ ```
217
+
218
+
219
+ ## Producing Events from Outbox to Kafka
220
+
221
+ As an implementation of the Transactional Outbox pattern, the SqlAlchemyOutboxedEventRepository is available for use as an access repository to the Outbox storage.
222
+ It can be utilized in conjunction with the KafkaMessageBroker.
223
+
224
+ ```python
225
+ import asyncio
226
+ import cqrs
227
+ from cqrs.message_brokers import kafka as kafka_broker
228
+
229
+ session_factory = async_sessionmaker(
230
+ create_async_engine(
231
+ f"mysql+asyncmy://{USER}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}",
232
+ isolation_level="REPEATABLE READ",
233
+ )
234
+ )
235
+
236
+ broker = kafka_broker.KafkaMessageBroker(
237
+ kafka_adapter.kafka_producer_factory(
238
+ dsn="localhost:9094",
239
+ topics=["test.topic1", "test.topic2"],
240
+ ),
241
+ "DEBUG"
242
+ )
243
+
244
+ producer = cqrs.EventProducer(cqrs.SqlAlchemyOutboxedEventRepository(session_factory, zlib.ZlibCompressor()), broker)
245
+ loop = asyncio.get_event_loop()
246
+ loop.run_until_complete(app.periodically_task())
247
+ ```
248
+
249
+
250
+ ## Transaction log tailing
251
+
252
+ If the Outbox polling strategy does not suit your needs, I recommend exploring the [Transaction Log Tailing](https://microservices.io/patterns/data/transaction-log-tailing.html) pattern.
253
+ The current version of the python-cqrs package does not support the implementation of this pattern.
254
+
255
+ > [!TIP]
256
+ > However, it can be implemented using [Debezium + Kafka Connect](https://debezium.io/documentation/reference/stable/architecture.html),
257
+ > which allows you to produce all newly created events within the Outbox storage directly to the corresponding topic in Kafka (or any other broker).
258
+
259
+
260
+ ## DI container
261
+
262
+ Use the following example to set up dependency injection in your command, query and event handlers. This will make dependency management simpler.
263
+
264
+ ```python
265
+ import di
266
+ ...
267
+
268
+ def setup_di() -> di.Container:
269
+ """
270
+ Binds implementations to dependencies
271
+ """
272
+ container = di.Container()
273
+ container.bind(
274
+ di.bind_by_type(
275
+ dependent.Dependent(cqrs.SqlAlchemyOutboxedEventRepository, scope="request"),
276
+ cqrs.OutboxedEventRepository
277
+ )
278
+ )
279
+ container.bind(
280
+ di.bind_by_type(
281
+ dependent.Dependent(MeetingAPIImplementaion, scope="request"),
282
+ MeetingAPIProtocol
283
+ )
284
+ )
285
+ return container
286
+ ```
287
+
288
+
289
+ ## Mapping
290
+
291
+ To bind commands, queries and events with specific handlers, you can use the registries `EventMap` and `RequestMap`.
292
+
293
+ ```python
294
+ from cqrs import requests, events
295
+
296
+ from app import commands, command_handlers
297
+ from app import queries, query_handlers
298
+ from app import events as event_models, event_handlers
299
+
300
+
301
+ def init_commands(mapper: requests.RequestMap) -> None:
302
+ mapper.bind(commands.JoinMeetingCommand, command_handlers.JoinMeetingCommandHandler)
303
+
304
+ def init_queries(mapper: requests.RequestMap) -> None:
305
+ mapper.bind(queries.ReadMeetingQuery, query_handlers.ReadMeetingQueryHandler)
306
+
307
+ def init_events(mapper: events.EventMap) -> None:
308
+ mapper.bind(events.NotificationEvent[events_models.NotificationMeetingRoomClosed], event_handlers.MeetingRoomClosedNotificationHandler)
309
+ mapper.bind(events.ECSTEvent[event_models.ECSTMeetingRoomClosed], event_handlers.UpdateMeetingRoomReadModelHandler)
310
+ ```
311
+
312
+
313
+ ## Bootstrap
314
+
315
+ The `python-cqrs` package implements a set of bootstrap utilities designed to simplify the initial configuration of an application.
316
+ ```python
317
+ import functools
318
+
319
+ from cqrs.events import bootstrap as event_bootstrap
320
+ from cqrs.requests import bootstrap as request_bootstrap
321
+
322
+ from app import dependencies, mapping, orm
323
+
324
+ @functools.lru_cache
325
+ def mediator_factory():
326
+ return request_bootstrap.bootstrap(
327
+ di_container=dependencies.setup_di(),
328
+ commands_mapper=mapping.init_commands,
329
+ queries_mapper=mapping.init_queries,
330
+ domain_events_mapper=mapping.init_events,
331
+ on_startup=[orm.init_store_event_mapper],
332
+ )
333
+
334
+
335
+ @functools.lru_cache
336
+ def event_mediator_factory():
337
+ return event_bootstrap.bootstrap(
338
+ di_container=dependencies.setup_di(),
339
+ events_mapper=mapping.init_events,
340
+ on_startup=[orm.init_store_event_mapper],
341
+ )
342
+ ```
343
+
344
+ ## Integaration with presentation layers
345
+
346
+ >[!TIP]
347
+ > I recommend reading the useful paper [Onion Architecture Used in Software Development](https://www.researchgate.net/publication/371006360_Onion_Architecture_Used_in_Software_Development).
348
+ > Separating user interaction and use-cases into Application and Presentation layers is a good practice.
349
+ > This can improve the `Testability`, `Maintainability`, `Scalability` of the application. It also provides benefits such as `Separation of Concerns`.
350
+
351
+ ### FastAPI requests handling
352
+
353
+ If your application uses FastAPI (or any other asynchronous framework for creating APIs).
354
+ In this case you can use python-cqrs to route requests to the appropriate handlers implementing specific use-cases.
355
+
356
+ ```python
357
+ import fastapi
358
+ import pydantic
359
+
360
+ from app import dependecies, commands
361
+
362
+ router = fastapi.APIRouter(prefix="/meetings")
363
+
364
+
365
+ @router.put("/{meeting_id}/{user_id}", status_code=status.HTTP_200_OK)
366
+ async def join_metting(
367
+ meeting_id: pydantic.PositiveInt,
368
+ user_id: typing.Text,
369
+ mediator: cqrs.RequestMediator = fastapi.Depends(dependencies.mediator_factory),
370
+ ):
371
+ await mediator.send(commands.JoinMeetingCommand(meeting_id=meeting_id, user_id=user_id))
372
+ return {"result": "ok"}
373
+
374
+ ```
375
+
376
+ ### Kafka events consuming
377
+
378
+ If you build interaction by events over brocker like `Kafka`, you can to implement an event consumer on your application's side,
379
+ which will call the appropriate handler for each event.
380
+ An example of handling events from `Kafka` is provided below.
381
+
382
+ ```python
383
+ import aiokafka
384
+ import cqrs
385
+ import orjson
386
+
387
+ from app import events
388
+
389
+ class OnEvent:
390
+
391
+ def __init__(
392
+ self,
393
+ event_mediator: cqrs.EventMediator
394
+ ):
395
+ self._event_mediator = event_mediator
396
+
397
+ async def __call__(self, kafka_message: aiokafka.ConsumerRecord) -> None:
398
+ event = cqrs.ECSTEvent[events.ECSTMeetingRoomClosed].model_validate(
399
+ orjson.loads(kafka_message.value),
400
+ context={"assume_validated": True},
401
+ )
402
+ await self._event_mediator.send(event)
403
+ ```