python-cqrs 0.2.0__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/PKG-INFO +199 -99
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/README.md +190 -97
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/pyproject.toml +16 -4
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/__init__.py +11 -7
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/adapters/kafka.py +11 -17
- python_cqrs-2.0.0/src/cqrs/compressors/__init__.py +4 -0
- python_cqrs-2.0.0/src/cqrs/deserializers/protobuf.py +52 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/events/__init__.py +1 -2
- python_cqrs-2.0.0/src/cqrs/events/event.py +59 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/events/event_emitter.py +30 -48
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/message_brokers/amqp.py +4 -5
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/message_brokers/devnull.py +4 -1
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/message_brokers/kafka.py +7 -3
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/message_brokers/protocol.py +6 -4
- python_cqrs-2.0.0/src/cqrs/outbox/map.py +24 -0
- python_cqrs-2.0.0/src/cqrs/outbox/mock.py +55 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/outbox/repository.py +12 -18
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/outbox/sqlalchemy.py +57 -100
- {python_cqrs-0.2.0/src/cqrs/outbox → python_cqrs-2.0.0/src/cqrs}/producer.py +26 -32
- python_cqrs-2.0.0/src/cqrs/serializers/__init__.py +0 -0
- python_cqrs-2.0.0/src/cqrs/serializers/default.py +8 -0
- python_cqrs-2.0.0/src/cqrs/serializers/protobuf.py +39 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/python_cqrs.egg-info/PKG-INFO +199 -99
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/python_cqrs.egg-info/SOURCES.txt +8 -2
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/python_cqrs.egg-info/requires.txt +10 -1
- python_cqrs-0.2.0/src/cqrs/events/event.py +0 -90
- python_cqrs-0.2.0/src/cqrs/outbox/protocol.py +0 -56
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/LICENSE +0 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/setup.cfg +0 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/adapters/__init__.py +0 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/adapters/amqp.py +0 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/compressors/protocol.py +0 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/compressors/zlib.py +0 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/container/__init__.py +0 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/container/di.py +0 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/container/protocol.py +0 -0
- {python_cqrs-0.2.0/src/cqrs/compressors → python_cqrs-2.0.0/src/cqrs/deserializers}/__init__.py +0 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/dispatcher/__init__.py +0 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/dispatcher/dispatcher.py +0 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/events/bootstrap.py +0 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/events/event_handler.py +0 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/events/map.py +0 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/mediator.py +0 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/message_brokers/__init__.py +0 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/middlewares/__init__.py +0 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/middlewares/base.py +0 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/middlewares/logging.py +0 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/outbox/__init__.py +0 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/requests/__init__.py +0 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/requests/bootstrap.py +0 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/requests/map.py +0 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/requests/request.py +0 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/requests/request_handler.py +0 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/cqrs/response.py +0 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/python_cqrs.egg-info/dependency_links.txt +0 -0
- {python_cqrs-0.2.0 → python_cqrs-2.0.0}/src/python_cqrs.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-cqrs
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Python CQRS pattern implementation
|
|
5
5
|
Author-email: Vadim Kozyrevskiy <vadikko2@mail.ru>
|
|
6
6
|
Maintainer-email: Vadim Kozyrevskiy <vadikko2@mail.ru>
|
|
@@ -21,6 +21,7 @@ Requires-Dist: aio-pika==9.3.0
|
|
|
21
21
|
Requires-Dist: di[anyio]==0.79.2
|
|
22
22
|
Requires-Dist: sqlalchemy[asyncio]==2.0.*
|
|
23
23
|
Requires-Dist: retry-async==0.1.4
|
|
24
|
+
Requires-Dist: python-dotenv==1.0.1
|
|
24
25
|
Provides-Extra: dev
|
|
25
26
|
Requires-Dist: pre-commit==3.8.0; extra == "dev"
|
|
26
27
|
Requires-Dist: pyright==1.1.377; extra == "dev"
|
|
@@ -29,28 +30,39 @@ Requires-Dist: aiokafka==0.10.0; extra == "dev"
|
|
|
29
30
|
Requires-Dist: pytest~=7.4.2; extra == "dev"
|
|
30
31
|
Requires-Dist: pytest-asyncio~=0.21.1; extra == "dev"
|
|
31
32
|
Requires-Dist: pytest-env==0.6.2; extra == "dev"
|
|
32
|
-
Requires-Dist: python-dotenv==1.0.1; extra == "dev"
|
|
33
33
|
Requires-Dist: cryptography==42.0.2; extra == "dev"
|
|
34
34
|
Requires-Dist: asyncmy==0.2.9; extra == "dev"
|
|
35
|
+
Provides-Extra: examples
|
|
36
|
+
Requires-Dist: fastapi==0.109.*; extra == "examples"
|
|
37
|
+
Requires-Dist: uvicorn==0.32.0; extra == "examples"
|
|
38
|
+
Requires-Dist: faststream[kafka]==0.5.28; extra == "examples"
|
|
35
39
|
Provides-Extra: kafka
|
|
36
40
|
Requires-Dist: aiokafka==0.10.0; extra == "kafka"
|
|
41
|
+
Requires-Dist: confluent-kafka==2.6.0; extra == "kafka"
|
|
42
|
+
Provides-Extra: protobuf
|
|
43
|
+
Requires-Dist: protobuf==5.0; extra == "protobuf"
|
|
37
44
|
|
|
38
|
-
# Python CQRS pattern
|
|
45
|
+
# Python CQRS pattern implementation with Transaction Outbox supporting
|
|
39
46
|
|
|
40
47
|
## Overview
|
|
41
48
|
|
|
42
49
|
This is a package for implementing the CQRS (Command Query Responsibility Segregation) pattern in Python applications.
|
|
43
|
-
It provides a set of abstractions and utilities to help separate read and write use cases, ensuring better scalability,
|
|
50
|
+
It provides a set of abstractions and utilities to help separate read and write use cases, ensuring better scalability,
|
|
51
|
+
performance, and maintainability of the application.
|
|
44
52
|
|
|
45
|
-
This package is a fork of the [diator](https://github.com/akhundMurad/diator)
|
|
53
|
+
This package is a fork of the [diator](https://github.com/akhundMurad/diator)
|
|
54
|
+
project ([documentation](https://akhundmurad.github.io/diator/)) with several enhancements:
|
|
46
55
|
|
|
47
56
|
1. Support for Pydantic [v2.*](https://docs.pydantic.dev/2.8/);
|
|
48
57
|
2. `Kafka` support using [aiokafka](https://github.com/aio-libs/aiokafka);
|
|
49
58
|
3. Added `EventMediator` for handling `Notification` and `ECST` events coming from the bus;
|
|
50
59
|
4. Redesigned the event and request mapping mechanism to handlers;
|
|
51
60
|
5. Added `bootstrap` for easy setup;
|
|
52
|
-
6. Added support for [Transaction Outbox](https://microservices.io/patterns/data/transactional-outbox.html), ensuring
|
|
53
|
-
|
|
61
|
+
6. Added support for [Transaction Outbox](https://microservices.io/patterns/data/transactional-outbox.html), ensuring
|
|
62
|
+
that `Notification` and `ECST` events are sent to the broker;
|
|
63
|
+
7. FastAPI supporting;
|
|
64
|
+
8. FastStream supporting;
|
|
65
|
+
9. [Protobuf](https://protobuf.dev/) events supporting.
|
|
54
66
|
|
|
55
67
|
## Request Handlers
|
|
56
68
|
|
|
@@ -58,7 +70,8 @@ Request handlers can be divided into two main types:
|
|
|
58
70
|
|
|
59
71
|
### Command Handler
|
|
60
72
|
|
|
61
|
-
Command Handler executes the received command. The logic of the handler may include, for example, modifying the state of
|
|
73
|
+
Command Handler executes the received command. The logic of the handler may include, for example, modifying the state of
|
|
74
|
+
the domain model.
|
|
62
75
|
As a result of executing the command, an event may be produced to the broker.
|
|
63
76
|
> [!TIP]
|
|
64
77
|
> By default, the command handler does not return any result, but it is not mandatory.
|
|
@@ -96,9 +109,13 @@ class SyncJoinMeetingCommandHandler(SyncRequestHandler[JoinMeetingCommand, None]
|
|
|
96
109
|
...
|
|
97
110
|
```
|
|
98
111
|
|
|
112
|
+
A complete example can be found in
|
|
113
|
+
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/request_handler.py)
|
|
114
|
+
|
|
99
115
|
### Query handler
|
|
100
116
|
|
|
101
|
-
Query Handler returns a representation of the requested data, for example, from
|
|
117
|
+
Query Handler returns a representation of the requested data, for example, from
|
|
118
|
+
the [read model](https://radekmaziarka.pl/2018/01/08/cqrs-third-step-simple-read-model/#simple-read-model---to-the-rescue).
|
|
102
119
|
> [!TIP]
|
|
103
120
|
> The read model can be constructed based on domain events produced by the `Command Handler`.
|
|
104
121
|
|
|
@@ -122,6 +139,8 @@ class ReadMeetingQueryHandler(RequestHandler[ReadMeetingQuery, ReadMeetingQueryR
|
|
|
122
139
|
|
|
123
140
|
```
|
|
124
141
|
|
|
142
|
+
A complete example can be found in
|
|
143
|
+
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/request_handler.py)
|
|
125
144
|
|
|
126
145
|
## Event Handlers
|
|
127
146
|
|
|
@@ -130,59 +149,81 @@ To configure event handling, you need to implement a broker consumer on the side
|
|
|
130
149
|
Below is an example of `Kafka event consuming` that can be used in the Presentation Layer.
|
|
131
150
|
|
|
132
151
|
```python
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
152
|
+
class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
|
|
153
|
+
def __init__(self):
|
|
154
|
+
self._events = []
|
|
136
155
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
async def handle(self, event: UserJoinedEventHandler) -> None:
|
|
141
|
-
await self._meetings_api.notify_room(event.meeting_id, "New user joined!")
|
|
156
|
+
@property
|
|
157
|
+
def events(self):
|
|
158
|
+
return self._events
|
|
142
159
|
|
|
143
|
-
|
|
160
|
+
async def handle(self, request: JoinMeetingCommand) -> None:
|
|
161
|
+
STORAGE[request.meeting_id].append(request.user_id)
|
|
162
|
+
self._events.append(
|
|
163
|
+
UserJoined(user_id=request.user_id, meeting_id=request.meeting_id),
|
|
164
|
+
)
|
|
165
|
+
print(f"User {request.user_id} joined meeting {request.meeting_id}")
|
|
144
166
|
|
|
145
|
-
def __init__(self, meetings_api: MeetingAPIProtocol) -> None:
|
|
146
|
-
self._meetings_api = meetings_api
|
|
147
167
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
168
|
+
class UserJoinedEventHandler(cqrs.EventHandler[UserJoined]):
|
|
169
|
+
async def handle(self, event: UserJoined) -> None:
|
|
170
|
+
print(f"Handle user {event.user_id} joined meeting {event.meeting_id} event")
|
|
151
171
|
```
|
|
152
172
|
|
|
173
|
+
A complete example can be found in
|
|
174
|
+
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/domain_event_handler.py)
|
|
175
|
+
|
|
153
176
|
## Producing Notification/ECST Events
|
|
154
177
|
|
|
155
|
-
During the handling of a command event, messages of type `cqrs.NotificationEvent` or `cqrs.ECSTEvent` may be generated
|
|
178
|
+
During the handling of a command event, messages of type `cqrs.NotificationEvent` or `cqrs.ECSTEvent` may be generated
|
|
179
|
+
and then sent to the broker.
|
|
156
180
|
|
|
157
181
|
```python
|
|
158
|
-
class
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
self._events: typing.List[events.Event] = []
|
|
182
|
+
class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
|
|
183
|
+
def __init__(self):
|
|
184
|
+
self._events = []
|
|
162
185
|
|
|
163
186
|
@property
|
|
164
|
-
def events(self)
|
|
187
|
+
def events(self):
|
|
165
188
|
return self._events
|
|
166
189
|
|
|
167
|
-
async def handle(self, request:
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
190
|
+
async def handle(self, request: JoinMeetingCommand) -> None:
|
|
191
|
+
print(f"User {request.user_id} joined meeting {request.meeting_id}")
|
|
192
|
+
self._events.append(
|
|
193
|
+
cqrs.NotificationEvent[UserJoinedNotificationPayload](
|
|
194
|
+
event_name="UserJoined",
|
|
195
|
+
topic="user_notification_events",
|
|
196
|
+
payload=UserJoinedNotificationPayload(
|
|
197
|
+
user_id=request.user_id,
|
|
198
|
+
meeting_id=request.meeting_id,
|
|
199
|
+
),
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
self._events.append(
|
|
203
|
+
cqrs.ECSTEvent[UserJoinedECSTPayload](
|
|
204
|
+
event_name="UserJoined",
|
|
205
|
+
topic="user_ecst_events",
|
|
206
|
+
payload=UserJoinedECSTPayload(
|
|
207
|
+
user_id=request.user_id,
|
|
208
|
+
meeting_id=request.meeting_id,
|
|
209
|
+
),
|
|
210
|
+
)
|
|
175
211
|
)
|
|
176
|
-
self._events.append(event)
|
|
177
212
|
```
|
|
178
213
|
|
|
214
|
+
A complete example can be found in
|
|
215
|
+
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/event_producing.py)
|
|
216
|
+
|
|
179
217
|
After processing the command/request, if there are any Notification/ECST events,
|
|
180
218
|
the EventEmitter is invoked to produce the events via the message broker.
|
|
181
219
|
|
|
182
220
|
> [!WARNING]
|
|
183
|
-
> It is important to note that producing events using the events property parameter does not guarantee message delivery
|
|
184
|
-
>
|
|
185
|
-
>
|
|
221
|
+
> It is important to note that producing events using the events property parameter does not guarantee message delivery
|
|
222
|
+
> to the broker.
|
|
223
|
+
> In the event of broker unavailability or an exception occurring during message formation or sending, the message may
|
|
224
|
+
> be lost.
|
|
225
|
+
> This issue can potentially be addressed by configuring retry attempts for sending messages to the broker, but we
|
|
226
|
+
> recommend using the [Transaction Outbox](https://microservices.io/patterns/data/transactional-outbox.html) pattern,
|
|
186
227
|
> which is implemented in the current version of the python-cqrs package for this purpose.
|
|
187
228
|
|
|
188
229
|
## Kafka broker
|
|
@@ -202,13 +243,10 @@ await broker.send_message(...)
|
|
|
202
243
|
|
|
203
244
|
## Transactional Outbox
|
|
204
245
|
|
|
205
|
-
The package implements the [Transactional Outbox](https://microservices.io/patterns/data/transactional-outbox.html)
|
|
206
|
-
|
|
246
|
+
The package implements the [Transactional Outbox](https://microservices.io/patterns/data/transactional-outbox.html)
|
|
247
|
+
pattern, which ensures that messages are produced to the broker according to the at-least-once semantics.
|
|
207
248
|
|
|
208
249
|
```python
|
|
209
|
-
from sqlalchemy.ext.asyncio import session as sql_session
|
|
210
|
-
from cqrs import events
|
|
211
|
-
|
|
212
250
|
def do_some_logic(meeting_room_id: int, session: sql_session.AsyncSession):
|
|
213
251
|
"""
|
|
214
252
|
Make changes to the database
|
|
@@ -216,33 +254,59 @@ def do_some_logic(meeting_room_id: int, session: sql_session.AsyncSession):
|
|
|
216
254
|
session.add(...)
|
|
217
255
|
|
|
218
256
|
|
|
219
|
-
class
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
self._repository = repository
|
|
223
|
-
self._events: typing.List[events.Event] = []
|
|
257
|
+
class JoinMeetingCommandHandler(cqrs.RequestHandler[JoinMeetingCommand, None]):
|
|
258
|
+
def __init__(self, outbox: cqrs.OutboxedEventRepository):
|
|
259
|
+
self.outbox = outbox
|
|
224
260
|
|
|
225
261
|
@property
|
|
226
262
|
def events(self):
|
|
227
|
-
return
|
|
228
|
-
|
|
229
|
-
async def handle(self, request:
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
263
|
+
return []
|
|
264
|
+
|
|
265
|
+
async def handle(self, request: JoinMeetingCommand) -> None:
|
|
266
|
+
print(f"User {request.user_id} joined meeting {request.meeting_id}")
|
|
267
|
+
async with self.outbox as session:
|
|
268
|
+
do_some_logic(request.meeting_id, session) # business logic
|
|
269
|
+
self.outbox.add(
|
|
270
|
+
session,
|
|
271
|
+
cqrs.NotificationEvent[UserJoinedNotificationPayload](
|
|
272
|
+
event_name="UserJoined",
|
|
273
|
+
topic="user_notification_events",
|
|
274
|
+
payload=UserJoinedNotificationPayload(
|
|
275
|
+
user_id=request.user_id,
|
|
276
|
+
meeting_id=request.meeting_id,
|
|
277
|
+
),
|
|
278
|
+
),
|
|
279
|
+
)
|
|
280
|
+
self.outbox.add(
|
|
281
|
+
session,
|
|
282
|
+
cqrs.ECSTEvent[UserJoinedECSTPayload](
|
|
283
|
+
event_name="UserJoined",
|
|
284
|
+
topic="user_ecst_events",
|
|
285
|
+
payload=UserJoinedECSTPayload(
|
|
286
|
+
user_id=request.user_id,
|
|
287
|
+
meeting_id=request.meeting_id,
|
|
288
|
+
),
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
await self.outbox.commit(session)
|
|
240
292
|
```
|
|
241
293
|
|
|
294
|
+
A complete example can be found in
|
|
295
|
+
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/save_events_into_outbox.py)
|
|
296
|
+
|
|
297
|
+
> [!TIP]
|
|
298
|
+
> You can specify the name of the Outbox table using the environment variable `OUTBOX_SQLA_TABLE`.
|
|
299
|
+
> By default, it is set to `outbox`.
|
|
300
|
+
|
|
301
|
+
> [!TIP]
|
|
302
|
+
> If you use the protobuf events you should specify `OutboxedEventRepository`
|
|
303
|
+
> by [protobuf serialize](https://github.com/vadikko2/cqrs/blob/master/src/cqrs/serializers/protobuf.py). A complete example can be found in
|
|
304
|
+
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/save_proto_events_into_outbox.py)
|
|
242
305
|
|
|
243
306
|
## Producing Events from Outbox to Kafka
|
|
244
307
|
|
|
245
|
-
As an implementation of the Transactional Outbox pattern, the SqlAlchemyOutboxedEventRepository is available for use as
|
|
308
|
+
As an implementation of the Transactional Outbox pattern, the SqlAlchemyOutboxedEventRepository is available for use as
|
|
309
|
+
an access repository to the Outbox storage.
|
|
246
310
|
It can be utilized in conjunction with the KafkaMessageBroker.
|
|
247
311
|
|
|
248
312
|
```python
|
|
@@ -257,12 +321,8 @@ session_factory = async_sessionmaker(
|
|
|
257
321
|
)
|
|
258
322
|
)
|
|
259
323
|
|
|
260
|
-
broker =
|
|
261
|
-
|
|
262
|
-
dsn="localhost:9094",
|
|
263
|
-
topics=["test.topic1", "test.topic2"],
|
|
264
|
-
),
|
|
265
|
-
"DEBUG"
|
|
324
|
+
broker = kafka.KafkaMessageBroker(
|
|
325
|
+
producer=kafka_adapters.kafka_producer_factory(dsn="localhost:9092"),
|
|
266
326
|
)
|
|
267
327
|
|
|
268
328
|
producer = cqrs.EventProducer(cqrs.SqlAlchemyOutboxedEventRepository(session_factory, zlib.ZlibCompressor()), broker)
|
|
@@ -270,20 +330,25 @@ loop = asyncio.get_event_loop()
|
|
|
270
330
|
loop.run_until_complete(app.periodically_task())
|
|
271
331
|
```
|
|
272
332
|
|
|
333
|
+
A complete example can be found in
|
|
334
|
+
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/kafka_outboxed_event_producing.py)
|
|
273
335
|
|
|
274
336
|
## Transaction log tailing
|
|
275
337
|
|
|
276
|
-
If the Outbox polling strategy does not suit your needs, I recommend exploring
|
|
338
|
+
If the Outbox polling strategy does not suit your needs, I recommend exploring
|
|
339
|
+
the [Transaction Log Tailing](https://microservices.io/patterns/data/transaction-log-tailing.html) pattern.
|
|
277
340
|
The current version of the python-cqrs package does not support the implementation of this pattern.
|
|
278
341
|
|
|
279
342
|
> [!TIP]
|
|
280
|
-
> However, it can be implemented
|
|
281
|
-
>
|
|
282
|
-
|
|
343
|
+
> However, it can be implemented
|
|
344
|
+
> using [Debezium + Kafka Connect](https://debezium.io/documentation/reference/stable/architecture.html),
|
|
345
|
+
> which allows you to produce all newly created events within the Outbox storage directly to the corresponding topic in
|
|
346
|
+
> Kafka (or any other broker).
|
|
283
347
|
|
|
284
348
|
## DI container
|
|
285
349
|
|
|
286
|
-
Use the following example to set up dependency injection in your command, query and event handlers. This will make
|
|
350
|
+
Use the following example to set up dependency injection in your command, query and event handlers. This will make
|
|
351
|
+
dependency management simpler.
|
|
287
352
|
|
|
288
353
|
```python
|
|
289
354
|
import di
|
|
@@ -309,6 +374,8 @@ def setup_di() -> di.Container:
|
|
|
309
374
|
return container
|
|
310
375
|
```
|
|
311
376
|
|
|
377
|
+
A complete example can be found in
|
|
378
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/dependency_injection.py)
|
|
312
379
|
|
|
313
380
|
## Mapping
|
|
314
381
|
|
|
@@ -333,10 +400,11 @@ def init_events(mapper: events.EventMap) -> None:
|
|
|
333
400
|
mapper.bind(events.ECSTEvent[event_models.ECSTMeetingRoomClosed], event_handlers.UpdateMeetingRoomReadModelHandler)
|
|
334
401
|
```
|
|
335
402
|
|
|
336
|
-
|
|
337
403
|
## Bootstrap
|
|
338
404
|
|
|
339
|
-
The `python-cqrs` package implements a set of bootstrap utilities designed to simplify the initial configuration of an
|
|
405
|
+
The `python-cqrs` package implements a set of bootstrap utilities designed to simplify the initial configuration of an
|
|
406
|
+
application.
|
|
407
|
+
|
|
340
408
|
```python
|
|
341
409
|
import functools
|
|
342
410
|
|
|
@@ -345,6 +413,7 @@ from cqrs.requests import bootstrap as request_bootstrap
|
|
|
345
413
|
|
|
346
414
|
from app import dependencies, mapping, orm
|
|
347
415
|
|
|
416
|
+
|
|
348
417
|
@functools.lru_cache
|
|
349
418
|
def mediator_factory():
|
|
350
419
|
return request_bootstrap.bootstrap(
|
|
@@ -365,12 +434,15 @@ def event_mediator_factory():
|
|
|
365
434
|
)
|
|
366
435
|
```
|
|
367
436
|
|
|
368
|
-
##
|
|
437
|
+
## Integration with presentation layers
|
|
369
438
|
|
|
370
|
-
>[!TIP]
|
|
371
|
-
> I recommend reading the useful
|
|
439
|
+
> [!TIP]
|
|
440
|
+
> I recommend reading the useful
|
|
441
|
+
>
|
|
442
|
+
paper [Onion Architecture Used in Software Development](https://www.researchgate.net/publication/371006360_Onion_Architecture_Used_in_Software_Development).
|
|
372
443
|
> Separating user interaction and use-cases into Application and Presentation layers is a good practice.
|
|
373
|
-
> This can improve the `Testability`, `Maintainability`, `Scalability` of the application. It also provides benefits
|
|
444
|
+
> This can improve the `Testability`, `Maintainability`, `Scalability` of the application. It also provides benefits
|
|
445
|
+
> such as `Separation of Concerns`.
|
|
374
446
|
|
|
375
447
|
### FastAPI requests handling
|
|
376
448
|
|
|
@@ -394,34 +466,62 @@ async def join_metting(
|
|
|
394
466
|
):
|
|
395
467
|
await mediator.send(commands.JoinMeetingCommand(meeting_id=meeting_id, user_id=user_id))
|
|
396
468
|
return {"result": "ok"}
|
|
397
|
-
|
|
398
469
|
```
|
|
399
470
|
|
|
471
|
+
A complete example can be found in
|
|
472
|
+
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/fastapi_integration.py)
|
|
473
|
+
|
|
400
474
|
### Kafka events consuming
|
|
401
475
|
|
|
402
|
-
If you build interaction by events over
|
|
476
|
+
If you build interaction by events over broker like `Kafka`, you can to implement an event consumer on your
|
|
477
|
+
application's side,
|
|
403
478
|
which will call the appropriate handler for each event.
|
|
404
479
|
An example of handling events from `Kafka` is provided below.
|
|
405
480
|
|
|
406
481
|
```python
|
|
407
|
-
import aiokafka
|
|
408
482
|
import cqrs
|
|
409
|
-
import orjson
|
|
410
483
|
|
|
411
|
-
|
|
484
|
+
import pydantic
|
|
485
|
+
import faststream
|
|
486
|
+
from faststream import kafka
|
|
487
|
+
|
|
488
|
+
broker = kafka.KafkaBroker(bootstrap_servers=["localhost:9092"])
|
|
489
|
+
app = faststream.FastStream(broker)
|
|
412
490
|
|
|
413
|
-
class OnEvent:
|
|
414
491
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
):
|
|
419
|
-
self._event_mediator = event_mediator
|
|
492
|
+
class HelloWorldPayload(pydantic.BaseModel):
|
|
493
|
+
hello: str = pydantic.Field(default="Hello")
|
|
494
|
+
world: str = pydantic.Field(default="World")
|
|
420
495
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
496
|
+
|
|
497
|
+
class HelloWorldECSTEventHandler(cqrs.EventHandler[cqrs.ECSTEvent[HelloWorldPayload]]):
|
|
498
|
+
async def handle(self, event: cqrs.ECSTEvent[HelloWorldPayload]) -> None:
|
|
499
|
+
print(f"{event.payload.hello} {event.payload.world}") # type: ignore
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
@broker.subscriber(
|
|
503
|
+
"hello_world",
|
|
504
|
+
group_id="examples",
|
|
505
|
+
auto_commit=False,
|
|
506
|
+
value_deserializer=value_deserializer,
|
|
507
|
+
)
|
|
508
|
+
async def hello_world_event_handler(
|
|
509
|
+
body: cqrs.ECSTEvent[HelloWorldPayload] | None,
|
|
510
|
+
msg: kafka.KafkaMessage,
|
|
511
|
+
mediator: cqrs.EventMediator = faststream.Depends(mediator_factory),
|
|
512
|
+
):
|
|
513
|
+
if body is not None:
|
|
514
|
+
await mediator.send(body)
|
|
515
|
+
await msg.ack()
|
|
427
516
|
```
|
|
517
|
+
|
|
518
|
+
A complete example can be found in
|
|
519
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/kafka_event_consuming.py)
|
|
520
|
+
|
|
521
|
+
## Protobuf messaging
|
|
522
|
+
|
|
523
|
+
The `python-cqrs` package supports integration with [protobuf](https://developers.google.com/protocol-buffers/).\
|
|
524
|
+
Protocol buffers are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data –
|
|
525
|
+
think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use
|
|
526
|
+
special generated source code to easily write and read your structured data to and from a variety of data streams and
|
|
527
|
+
using a variety of languages.
|