python-cqrs 4.2.0__tar.gz → 4.3.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.
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/PKG-INFO +71 -4
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/README.md +68 -2
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/pyproject.toml +4 -2
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/dispatcher/dispatcher.py +33 -3
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/requests/bootstrap.py +2 -2
- python_cqrs-4.3.1/src/cqrs/requests/cor_request_handler.py +124 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/requests/map.py +6 -2
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/requests/request_handler.py +8 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/python_cqrs.egg-info/PKG-INFO +71 -4
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/python_cqrs.egg-info/SOURCES.txt +1 -3
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/python_cqrs.egg-info/requires.txt +3 -1
- python_cqrs-4.2.0/src/cqrs/decoders/kafka/__init__.py +0 -5
- python_cqrs-4.2.0/src/cqrs/decoders/kafka/empty_message.py +0 -24
- python_cqrs-4.2.0/src/cqrs/outbox/__init__.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/LICENSE +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/setup.cfg +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/__init__.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/adapters/__init__.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/adapters/amqp.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/adapters/kafka.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/adapters/protocol.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/compressors/__init__.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/compressors/protocol.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/compressors/zlib.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/container/__init__.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/container/dependency_injector.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/container/di.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/container/protocol.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/deserializers/__init__.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/deserializers/json.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/deserializers/protobuf.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/dispatcher/__init__.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/events/__init__.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/events/bootstrap.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/events/event.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/events/event_emitter.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/events/event_handler.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/events/map.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/mediator.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/message_brokers/__init__.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/message_brokers/amqp.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/message_brokers/devnull.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/message_brokers/kafka.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/message_brokers/protocol.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/middlewares/__init__.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/middlewares/base.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/middlewares/logging.py +0 -0
- {python_cqrs-4.2.0/src/cqrs/decoders → python_cqrs-4.3.1/src/cqrs/outbox}/__init__.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/outbox/map.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/outbox/mock.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/outbox/repository.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/outbox/sqlalchemy.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/producer.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/requests/__init__.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/requests/request.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/response.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/serializers/__init__.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/serializers/default.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/serializers/protobuf.py +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/python_cqrs.egg-info/dependency_links.txt +0 -0
- {python_cqrs-4.2.0 → python_cqrs-4.3.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.
|
|
3
|
+
Version: 4.3.1
|
|
4
4
|
Summary: Python CQRS pattern implementation
|
|
5
5
|
Author-email: Vadim Kozyrevskiy <vadikko2@mail.ru>, Dmitry Kutlubaev <kutlubaev00@mail.ru>
|
|
6
6
|
Maintainer-email: Vadim Kozyrevskiy <vadikko2@mail.ru>
|
|
@@ -18,7 +18,6 @@ Description-Content-Type: text/markdown
|
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
Requires-Dist: pydantic==2.*
|
|
20
20
|
Requires-Dist: orjson==3.9.15
|
|
21
|
-
Requires-Dist: aio-pika==9.3.0
|
|
22
21
|
Requires-Dist: di[anyio]==0.79.2
|
|
23
22
|
Requires-Dist: sqlalchemy[asyncio]==2.0.*
|
|
24
23
|
Requires-Dist: retry-async==0.1.4
|
|
@@ -46,6 +45,8 @@ Requires-Dist: aiokafka==0.10.0; extra == "kafka"
|
|
|
46
45
|
Requires-Dist: confluent-kafka==2.6.0; extra == "kafka"
|
|
47
46
|
Provides-Extra: protobuf
|
|
48
47
|
Requires-Dist: protobuf==4.25.5; extra == "protobuf"
|
|
48
|
+
Provides-Extra: rabbit
|
|
49
|
+
Requires-Dist: aio-pika==9.3.0; extra == "rabbit"
|
|
49
50
|
Dynamic: license-file
|
|
50
51
|
|
|
51
52
|
# Python CQRS pattern implementation with Transaction Outbox supporting
|
|
@@ -70,7 +71,8 @@ project ([documentation](https://akhundmurad.github.io/diator/)) with several en
|
|
|
70
71
|
8. FastStream supporting;
|
|
71
72
|
9. [Protobuf](https://protobuf.dev/) events supporting;
|
|
72
73
|
10. `StreamingRequestMediator` and `StreamingRequestHandler` for handling streaming requests with real-time progress updates;
|
|
73
|
-
11. Parallel event processing with configurable concurrency limits
|
|
74
|
+
11. Parallel event processing with configurable concurrency limits;
|
|
75
|
+
12. Chain of Responsibility pattern support with `CORRequestHandler` for processing requests through multiple handlers in sequence.
|
|
74
76
|
|
|
75
77
|
## Request Handlers
|
|
76
78
|
|
|
@@ -171,6 +173,9 @@ class ProcessFilesCommandHandler(StreamingRequestHandler[ProcessFilesCommand, Fi
|
|
|
171
173
|
def events(self) -> list[Event]:
|
|
172
174
|
return self._events.copy()
|
|
173
175
|
|
|
176
|
+
def clear_events(self) -> None:
|
|
177
|
+
self._events.clear()
|
|
178
|
+
|
|
174
179
|
async def handle(self, request: ProcessFilesCommand) -> typing.AsyncIterator[FileProcessedResult]:
|
|
175
180
|
for file_id in request.file_ids:
|
|
176
181
|
# Process file
|
|
@@ -178,12 +183,74 @@ class ProcessFilesCommandHandler(StreamingRequestHandler[ProcessFilesCommand, Fi
|
|
|
178
183
|
# Emit events
|
|
179
184
|
self._events.append(FileProcessedEvent(file_id=file_id, ...))
|
|
180
185
|
yield result
|
|
181
|
-
self._events.clear()
|
|
182
186
|
```
|
|
183
187
|
|
|
184
188
|
A complete example can be found in
|
|
185
189
|
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/streaming_handler_parallel_events.py)
|
|
186
190
|
|
|
191
|
+
### Chain of Responsibility Request Handler
|
|
192
|
+
|
|
193
|
+
Chain of Responsibility Request Handler implements the chain of responsibility pattern, allowing multiple handlers
|
|
194
|
+
to process a request in sequence until one successfully handles it. This pattern is particularly useful when you have
|
|
195
|
+
multiple processing strategies or need to implement fallback mechanisms.
|
|
196
|
+
|
|
197
|
+
Each handler in the chain decides whether to process the request or pass it to the next handler. The chain stops
|
|
198
|
+
when a handler successfully processes the request or when all handlers have been exhausted.
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
import typing
|
|
202
|
+
from cqrs.requests.cor_request_handler import CORRequestHandler
|
|
203
|
+
from cqrs.events.event import Event
|
|
204
|
+
|
|
205
|
+
class CreditCardPaymentHandler(CORRequestHandler[ProcessPaymentCommand, PaymentResult]):
|
|
206
|
+
def __init__(self, payment_service: PaymentServiceProtocol) -> None:
|
|
207
|
+
self._payment_service = payment_service
|
|
208
|
+
self._events: typing.List[Event] = []
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def events(self) -> typing.List[Event]:
|
|
212
|
+
return self._events
|
|
213
|
+
|
|
214
|
+
async def handle(self, request: ProcessPaymentCommand) -> PaymentResult | None:
|
|
215
|
+
if request.payment_method == "credit_card":
|
|
216
|
+
# Process credit card payment
|
|
217
|
+
result = await self._payment_service.process_credit_card(request)
|
|
218
|
+
self._events.append(PaymentProcessedEvent(...))
|
|
219
|
+
return PaymentResult(success=True, transaction_id=result.id)
|
|
220
|
+
|
|
221
|
+
# Pass to next handler
|
|
222
|
+
return await self.next(request)
|
|
223
|
+
|
|
224
|
+
class PayPalPaymentHandler(CORRequestHandler[ProcessPaymentCommand, PaymentResult]):
|
|
225
|
+
def __init__(self, paypal_service: PayPalServiceProtocol) -> None:
|
|
226
|
+
self._paypal_service = paypal_service
|
|
227
|
+
self._events: typing.List[Event] = []
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def events(self) -> typing.List[Event]:
|
|
231
|
+
return self._events
|
|
232
|
+
|
|
233
|
+
async def handle(self, request: ProcessPaymentCommand) -> PaymentResult | None:
|
|
234
|
+
if request.payment_method == "paypal":
|
|
235
|
+
# Process PayPal payment
|
|
236
|
+
result = await self._paypal_service.process_payment(request)
|
|
237
|
+
return PaymentResult(success=True, transaction_id=result.id)
|
|
238
|
+
|
|
239
|
+
# Pass to next handler
|
|
240
|
+
return await self.next(request)
|
|
241
|
+
|
|
242
|
+
# Chain registration
|
|
243
|
+
def payment_mapper(mapper: cqrs.RequestMap) -> None:
|
|
244
|
+
mapper.bind(ProcessPaymentCommand, [
|
|
245
|
+
CreditCardPaymentHandler,
|
|
246
|
+
PayPalPaymentHandler,
|
|
247
|
+
DefaultPaymentHandler # Fallback handler
|
|
248
|
+
])
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
A complete example can be found in
|
|
252
|
+
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/cor_request_handler.py)
|
|
253
|
+
|
|
187
254
|
## Event Handlers
|
|
188
255
|
|
|
189
256
|
Event handlers are designed to process `Notification` and `ECST` events that are consumed from the broker.
|
|
@@ -20,7 +20,8 @@ project ([documentation](https://akhundmurad.github.io/diator/)) with several en
|
|
|
20
20
|
8. FastStream supporting;
|
|
21
21
|
9. [Protobuf](https://protobuf.dev/) events supporting;
|
|
22
22
|
10. `StreamingRequestMediator` and `StreamingRequestHandler` for handling streaming requests with real-time progress updates;
|
|
23
|
-
11. Parallel event processing with configurable concurrency limits
|
|
23
|
+
11. Parallel event processing with configurable concurrency limits;
|
|
24
|
+
12. Chain of Responsibility pattern support with `CORRequestHandler` for processing requests through multiple handlers in sequence.
|
|
24
25
|
|
|
25
26
|
## Request Handlers
|
|
26
27
|
|
|
@@ -121,6 +122,9 @@ class ProcessFilesCommandHandler(StreamingRequestHandler[ProcessFilesCommand, Fi
|
|
|
121
122
|
def events(self) -> list[Event]:
|
|
122
123
|
return self._events.copy()
|
|
123
124
|
|
|
125
|
+
def clear_events(self) -> None:
|
|
126
|
+
self._events.clear()
|
|
127
|
+
|
|
124
128
|
async def handle(self, request: ProcessFilesCommand) -> typing.AsyncIterator[FileProcessedResult]:
|
|
125
129
|
for file_id in request.file_ids:
|
|
126
130
|
# Process file
|
|
@@ -128,12 +132,74 @@ class ProcessFilesCommandHandler(StreamingRequestHandler[ProcessFilesCommand, Fi
|
|
|
128
132
|
# Emit events
|
|
129
133
|
self._events.append(FileProcessedEvent(file_id=file_id, ...))
|
|
130
134
|
yield result
|
|
131
|
-
self._events.clear()
|
|
132
135
|
```
|
|
133
136
|
|
|
134
137
|
A complete example can be found in
|
|
135
138
|
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/streaming_handler_parallel_events.py)
|
|
136
139
|
|
|
140
|
+
### Chain of Responsibility Request Handler
|
|
141
|
+
|
|
142
|
+
Chain of Responsibility Request Handler implements the chain of responsibility pattern, allowing multiple handlers
|
|
143
|
+
to process a request in sequence until one successfully handles it. This pattern is particularly useful when you have
|
|
144
|
+
multiple processing strategies or need to implement fallback mechanisms.
|
|
145
|
+
|
|
146
|
+
Each handler in the chain decides whether to process the request or pass it to the next handler. The chain stops
|
|
147
|
+
when a handler successfully processes the request or when all handlers have been exhausted.
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
import typing
|
|
151
|
+
from cqrs.requests.cor_request_handler import CORRequestHandler
|
|
152
|
+
from cqrs.events.event import Event
|
|
153
|
+
|
|
154
|
+
class CreditCardPaymentHandler(CORRequestHandler[ProcessPaymentCommand, PaymentResult]):
|
|
155
|
+
def __init__(self, payment_service: PaymentServiceProtocol) -> None:
|
|
156
|
+
self._payment_service = payment_service
|
|
157
|
+
self._events: typing.List[Event] = []
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def events(self) -> typing.List[Event]:
|
|
161
|
+
return self._events
|
|
162
|
+
|
|
163
|
+
async def handle(self, request: ProcessPaymentCommand) -> PaymentResult | None:
|
|
164
|
+
if request.payment_method == "credit_card":
|
|
165
|
+
# Process credit card payment
|
|
166
|
+
result = await self._payment_service.process_credit_card(request)
|
|
167
|
+
self._events.append(PaymentProcessedEvent(...))
|
|
168
|
+
return PaymentResult(success=True, transaction_id=result.id)
|
|
169
|
+
|
|
170
|
+
# Pass to next handler
|
|
171
|
+
return await self.next(request)
|
|
172
|
+
|
|
173
|
+
class PayPalPaymentHandler(CORRequestHandler[ProcessPaymentCommand, PaymentResult]):
|
|
174
|
+
def __init__(self, paypal_service: PayPalServiceProtocol) -> None:
|
|
175
|
+
self._paypal_service = paypal_service
|
|
176
|
+
self._events: typing.List[Event] = []
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def events(self) -> typing.List[Event]:
|
|
180
|
+
return self._events
|
|
181
|
+
|
|
182
|
+
async def handle(self, request: ProcessPaymentCommand) -> PaymentResult | None:
|
|
183
|
+
if request.payment_method == "paypal":
|
|
184
|
+
# Process PayPal payment
|
|
185
|
+
result = await self._paypal_service.process_payment(request)
|
|
186
|
+
return PaymentResult(success=True, transaction_id=result.id)
|
|
187
|
+
|
|
188
|
+
# Pass to next handler
|
|
189
|
+
return await self.next(request)
|
|
190
|
+
|
|
191
|
+
# Chain registration
|
|
192
|
+
def payment_mapper(mapper: cqrs.RequestMap) -> None:
|
|
193
|
+
mapper.bind(ProcessPaymentCommand, [
|
|
194
|
+
CreditCardPaymentHandler,
|
|
195
|
+
PayPalPaymentHandler,
|
|
196
|
+
DefaultPaymentHandler # Fallback handler
|
|
197
|
+
])
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
A complete example can be found in
|
|
201
|
+
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/cor_request_handler.py)
|
|
202
|
+
|
|
137
203
|
## Event Handlers
|
|
138
204
|
|
|
139
205
|
Event handlers are designed to process `Notification` and `ECST` events that are consumed from the broker.
|
|
@@ -18,7 +18,6 @@ classifiers = [
|
|
|
18
18
|
dependencies = [
|
|
19
19
|
"pydantic==2.*",
|
|
20
20
|
"orjson==3.9.15",
|
|
21
|
-
"aio-pika==9.3.0",
|
|
22
21
|
"di[anyio]==0.79.2",
|
|
23
22
|
"sqlalchemy[asyncio]==2.0.*",
|
|
24
23
|
"retry-async==0.1.4",
|
|
@@ -30,7 +29,7 @@ maintainers = [{name = "Vadim Kozyrevskiy", email = "vadikko2@mail.ru"}]
|
|
|
30
29
|
name = "python-cqrs"
|
|
31
30
|
readme = "README.md"
|
|
32
31
|
requires-python = ">=3.10"
|
|
33
|
-
version = "4.
|
|
32
|
+
version = "4.3.1"
|
|
34
33
|
|
|
35
34
|
[project.optional-dependencies]
|
|
36
35
|
dev = [
|
|
@@ -60,6 +59,9 @@ kafka = [
|
|
|
60
59
|
"confluent-kafka==2.6.0"
|
|
61
60
|
]
|
|
62
61
|
protobuf = ["protobuf==4.25.5"]
|
|
62
|
+
rabbit = [
|
|
63
|
+
"aio-pika==9.3.0"
|
|
64
|
+
]
|
|
63
65
|
|
|
64
66
|
[project.urls]
|
|
65
67
|
Documentation = "https://vadikko2.github.io/python-cqrs-mkdocs/"
|
|
@@ -3,6 +3,7 @@ import functools
|
|
|
3
3
|
import inspect
|
|
4
4
|
import logging
|
|
5
5
|
import typing
|
|
6
|
+
from collections import abc
|
|
6
7
|
|
|
7
8
|
import pydantic
|
|
8
9
|
|
|
@@ -15,13 +16,17 @@ from cqrs import (
|
|
|
15
16
|
)
|
|
16
17
|
from cqrs.events import event_handler
|
|
17
18
|
from cqrs.requests import request_handler
|
|
19
|
+
from cqrs.requests import cor_request_handler
|
|
18
20
|
|
|
19
21
|
logger = logging.getLogger("cqrs")
|
|
20
22
|
|
|
21
23
|
_Resp = typing.TypeVar("_Resp", resp.Response, None, contravariant=True)
|
|
22
24
|
|
|
23
25
|
_RequestHandler: typing.TypeAlias = (
|
|
24
|
-
request_handler.RequestHandler
|
|
26
|
+
request_handler.RequestHandler
|
|
27
|
+
| request_handler.SyncRequestHandler
|
|
28
|
+
| cor_request_handler.CORRequestHandler
|
|
29
|
+
| cor_request_handler.SyncCORRequestHandler
|
|
25
30
|
)
|
|
26
31
|
_StreamingRequestHandler: typing.TypeAlias = (
|
|
27
32
|
request_handler.StreamingRequestHandler
|
|
@@ -36,6 +41,10 @@ class RequestHandlerDoesNotExist(Exception):
|
|
|
36
41
|
pass
|
|
37
42
|
|
|
38
43
|
|
|
44
|
+
class RequestHandlerTypeError(Exception):
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
39
48
|
class RequestDispatchResult(pydantic.BaseModel, typing.Generic[_Resp]):
|
|
40
49
|
response: _Resp = pydantic.Field(default=None)
|
|
41
50
|
events: typing.List[cqrs_events.Event] = pydantic.Field(default_factory=list)
|
|
@@ -52,13 +61,34 @@ class RequestDispatcher:
|
|
|
52
61
|
self._container = container
|
|
53
62
|
self._middleware_chain = middleware_chain or middlewares.MiddlewareChain()
|
|
54
63
|
|
|
64
|
+
async def _resolve_handler(self, handler_type: typing.Type[_RequestHandler] | typing.List[typing.Type[_RequestHandler]]) -> _RequestHandler:
|
|
65
|
+
"""
|
|
66
|
+
Resolve a single handler or build a chain from multiple handlers.
|
|
67
|
+
|
|
68
|
+
For single handlers, resolves them using the DI container.
|
|
69
|
+
For lists of handlers, validates they are COR handlers and builds a chain.
|
|
70
|
+
"""
|
|
71
|
+
if isinstance(handler_type, abc.Iterable):
|
|
72
|
+
if not all(issubclass(handler_cls, (
|
|
73
|
+
cor_request_handler.CORRequestHandler,
|
|
74
|
+
cor_request_handler.SyncCORRequestHandler
|
|
75
|
+
)) for handler_cls in handler_type):
|
|
76
|
+
raise RequestHandlerTypeError(f"COR handler must be type CORRequestHandler or SyncCORRequestHandler")
|
|
77
|
+
|
|
78
|
+
async with asyncio.TaskGroup() as tg:
|
|
79
|
+
tasks = [tg.create_task(self._container.resolve(h)) for h in handler_type]
|
|
80
|
+
handlers = [task.result() for task in tasks]
|
|
81
|
+
return cor_request_handler.build_chain(handlers)
|
|
82
|
+
|
|
83
|
+
return await self._container.resolve(handler_type)
|
|
84
|
+
|
|
55
85
|
async def dispatch(self, request: requests.Request) -> RequestDispatchResult:
|
|
56
86
|
handler_type = self._request_map.get(type(request), None)
|
|
57
|
-
if handler_type
|
|
87
|
+
if not handler_type:
|
|
58
88
|
raise RequestHandlerDoesNotExist(
|
|
59
89
|
f"RequestHandler not found matching Request type {type(request)}",
|
|
60
90
|
)
|
|
61
|
-
handler: _RequestHandler = await self.
|
|
91
|
+
handler: _RequestHandler = await self._resolve_handler(handler_type)
|
|
62
92
|
if asyncio.iscoroutinefunction(handler.handle):
|
|
63
93
|
wrapped_handle = self._middleware_chain.wrap(handler.handle)
|
|
64
94
|
else:
|
|
@@ -18,7 +18,7 @@ def setup_event_emitter(
|
|
|
18
18
|
container: di_container_impl.DIContainer,
|
|
19
19
|
domain_events_mapper: typing.Callable[[events.EventMap], None] | None = None,
|
|
20
20
|
message_broker: protocol.MessageBroker | None = None,
|
|
21
|
-
): ...
|
|
21
|
+
) -> events.EventEmitter: ...
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
@overload
|
|
@@ -33,7 +33,7 @@ def setup_event_emitter(
|
|
|
33
33
|
container: di_container_impl.DIContainer | CQRSContainer,
|
|
34
34
|
domain_events_mapper: typing.Callable[[events.EventMap], None] | None = None,
|
|
35
35
|
message_broker: protocol.MessageBroker | None = None,
|
|
36
|
-
):
|
|
36
|
+
) -> events.EventEmitter:
|
|
37
37
|
if message_broker is None:
|
|
38
38
|
message_broker = DEFAULT_MESSAGE_BROKER
|
|
39
39
|
event_mapper = events.EventMap()
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import abc
|
|
3
|
+
import functools
|
|
4
|
+
import typing
|
|
5
|
+
|
|
6
|
+
from cqrs import response
|
|
7
|
+
from cqrs.events import event
|
|
8
|
+
from cqrs.requests import request as r
|
|
9
|
+
|
|
10
|
+
_Req = typing.TypeVar("_Req", bound=r.Request, contravariant=True)
|
|
11
|
+
_Resp = typing.TypeVar("_Resp", response.Response, None, covariant=True)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CORRequestHandler(abc.ABC, typing.Generic[_Req, _Resp]):
|
|
15
|
+
"""
|
|
16
|
+
The chain of responsibility request handler interface.
|
|
17
|
+
|
|
18
|
+
Implements the chain of responsibility pattern, allowing multiple handlers
|
|
19
|
+
to process a request in sequence until one handles it successfully.
|
|
20
|
+
|
|
21
|
+
Chain handler example::
|
|
22
|
+
|
|
23
|
+
class AuthenticationHandler(CORRequestHandler[LoginCommand, None]):
|
|
24
|
+
def __init__(self, auth_service: AuthServiceProtocol) -> None:
|
|
25
|
+
self._auth_service = auth_service
|
|
26
|
+
self.events: typing.List[Event] = []
|
|
27
|
+
|
|
28
|
+
async def handle(self, request: LoginCommand) -> None | None:
|
|
29
|
+
if self._auth_service.can_authenticate(request):
|
|
30
|
+
return await self._auth_service.authenticate(request)
|
|
31
|
+
return await super().handle(request)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
_next_handler: "CORRequestHandler" = None
|
|
35
|
+
|
|
36
|
+
def set_next(self, handler: "CORRequestHandler") -> "CORRequestHandler":
|
|
37
|
+
if self._next_handler is None:
|
|
38
|
+
self._next_handler = handler
|
|
39
|
+
|
|
40
|
+
return self._next_handler
|
|
41
|
+
|
|
42
|
+
async def next(self, request: _Req) -> _Resp:
|
|
43
|
+
if self._next_handler:
|
|
44
|
+
return await self._next_handler.handle(request)
|
|
45
|
+
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
@abc.abstractmethod
|
|
50
|
+
def events(self) -> typing.List[event.Event]:
|
|
51
|
+
raise NotImplementedError
|
|
52
|
+
|
|
53
|
+
@abc.abstractmethod
|
|
54
|
+
async def handle(self, request: _Req) -> _Resp:
|
|
55
|
+
raise NotImplementedError
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SyncCORRequestHandler(abc.ABC, typing.Generic[_Req, _Resp]):
|
|
59
|
+
"""
|
|
60
|
+
The synchronous chain of responsibility request handler interface.
|
|
61
|
+
|
|
62
|
+
Implements the chain of responsibility pattern, allowing multiple handlers
|
|
63
|
+
to process a request in sequence until one handles it successfully.
|
|
64
|
+
|
|
65
|
+
Chain handler example::
|
|
66
|
+
|
|
67
|
+
class AuthenticationHandler(SyncCORRequestHandler[LoginCommand, None]):
|
|
68
|
+
def __init__(self, auth_service: AuthServiceProtocol) -> None:
|
|
69
|
+
self._auth_service = auth_service
|
|
70
|
+
self.events: typing.List[Event] = []
|
|
71
|
+
|
|
72
|
+
def handle(self, request: LoginCommand) -> None | None:
|
|
73
|
+
if self._auth_service.can_authenticate(request):
|
|
74
|
+
return self._auth_service.authenticate(request)
|
|
75
|
+
return super().handle(request)
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
_next_handler: "SyncCORRequestHandler" = None
|
|
79
|
+
|
|
80
|
+
def set_next(self, handler: "SyncCORRequestHandler") -> "SyncCORRequestHandler":
|
|
81
|
+
if self._next_handler is None:
|
|
82
|
+
self._next_handler = handler
|
|
83
|
+
|
|
84
|
+
return self._next_handler
|
|
85
|
+
|
|
86
|
+
def next(self, request: _Req) -> _Resp:
|
|
87
|
+
if self._next_handler:
|
|
88
|
+
return self._next_handler.handle(request)
|
|
89
|
+
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
@abc.abstractmethod
|
|
94
|
+
def events(self) -> typing.List[event.Event]:
|
|
95
|
+
raise NotImplementedError
|
|
96
|
+
|
|
97
|
+
@abc.abstractmethod
|
|
98
|
+
def handle(self, request: _Req) -> _Resp:
|
|
99
|
+
raise NotImplementedError
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
_RequestHandler: typing.TypeAlias = CORRequestHandler | SyncCORRequestHandler
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def build_chain(handlers: typing.List[_RequestHandler]) -> _RequestHandler:
|
|
106
|
+
"""
|
|
107
|
+
Build a chain of responsibility from a list of handlers.
|
|
108
|
+
|
|
109
|
+
Links handlers together in the order they appear in the list,
|
|
110
|
+
with each handler pointing to the next one in sequence.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
handlers: List of handlers to be linked together
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
The first handler in the chain
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def link(handler1, handler2):
|
|
120
|
+
handler1.set_next(handler2)
|
|
121
|
+
return handler2
|
|
122
|
+
|
|
123
|
+
functools.reduce(link, handlers)
|
|
124
|
+
return handlers[0]
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import typing
|
|
2
2
|
|
|
3
|
-
from cqrs.requests import request, request_handler
|
|
3
|
+
from cqrs.requests import request, request_handler, cor_request_handler
|
|
4
4
|
|
|
5
5
|
_KT = typing.TypeVar("_KT", bound=typing.Type[request.Request])
|
|
6
6
|
_VT = typing.TypeVar(
|
|
7
7
|
"_VT",
|
|
8
8
|
bound=typing.Type[
|
|
9
9
|
request_handler.RequestHandler | request_handler.SyncRequestHandler
|
|
10
|
-
]
|
|
10
|
+
]
|
|
11
|
+
| typing.List[typing.Type[cor_request_handler.CORRequestHandler]]
|
|
12
|
+
| typing.List[typing.Type[cor_request_handler.SyncCORRequestHandler]]
|
|
13
|
+
| typing.Type[request_handler.StreamingRequestHandler]
|
|
14
|
+
| typing.Type[request_handler.SyncStreamingRequestHandler],
|
|
11
15
|
)
|
|
12
16
|
|
|
13
17
|
|
|
@@ -105,6 +105,9 @@ class StreamingRequestHandler(abc.ABC, typing.Generic[_Req, _Resp]):
|
|
|
105
105
|
def events(self) -> list[Event]:
|
|
106
106
|
return self._events.copy()
|
|
107
107
|
|
|
108
|
+
def clear_events(self) -> None:
|
|
109
|
+
self._events.clear()
|
|
110
|
+
|
|
108
111
|
async def handle(self, request: ProcessItemsCommand) -> typing.AsyncIterator[ProcessItemResult]:
|
|
109
112
|
for item_id in request.item_ids:
|
|
110
113
|
result = await self._items_api.process_item(item_id)
|
|
@@ -121,6 +124,7 @@ class StreamingRequestHandler(abc.ABC, typing.Generic[_Req, _Resp]):
|
|
|
121
124
|
async def handle(self, request: _Req) -> typing.AsyncIterator[_Resp]:
|
|
122
125
|
raise NotImplementedError
|
|
123
126
|
|
|
127
|
+
@abc.abstractmethod
|
|
124
128
|
def clear_events(self) -> None:
|
|
125
129
|
"""
|
|
126
130
|
Clear events that have been processed.
|
|
@@ -150,6 +154,9 @@ class SyncStreamingRequestHandler(abc.ABC, typing.Generic[_Req, _Resp]):
|
|
|
150
154
|
def events(self) -> list[Event]:
|
|
151
155
|
return self._events.copy()
|
|
152
156
|
|
|
157
|
+
def clear_events(self) -> None:
|
|
158
|
+
self._events.clear()
|
|
159
|
+
|
|
153
160
|
def handle(self, request: ProcessItemsCommand) -> typing.Iterator[ProcessItemResult]:
|
|
154
161
|
for item_id in request.item_ids:
|
|
155
162
|
result = self._items_api.process_item(item_id)
|
|
@@ -166,6 +173,7 @@ class SyncStreamingRequestHandler(abc.ABC, typing.Generic[_Req, _Resp]):
|
|
|
166
173
|
def handle(self, request: _Req) -> typing.Iterator[_Resp]:
|
|
167
174
|
raise NotImplementedError
|
|
168
175
|
|
|
176
|
+
@abc.abstractmethod
|
|
169
177
|
def clear_events(self) -> None:
|
|
170
178
|
"""
|
|
171
179
|
Clear events that have been processed.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-cqrs
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.3.1
|
|
4
4
|
Summary: Python CQRS pattern implementation
|
|
5
5
|
Author-email: Vadim Kozyrevskiy <vadikko2@mail.ru>, Dmitry Kutlubaev <kutlubaev00@mail.ru>
|
|
6
6
|
Maintainer-email: Vadim Kozyrevskiy <vadikko2@mail.ru>
|
|
@@ -18,7 +18,6 @@ Description-Content-Type: text/markdown
|
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
Requires-Dist: pydantic==2.*
|
|
20
20
|
Requires-Dist: orjson==3.9.15
|
|
21
|
-
Requires-Dist: aio-pika==9.3.0
|
|
22
21
|
Requires-Dist: di[anyio]==0.79.2
|
|
23
22
|
Requires-Dist: sqlalchemy[asyncio]==2.0.*
|
|
24
23
|
Requires-Dist: retry-async==0.1.4
|
|
@@ -46,6 +45,8 @@ Requires-Dist: aiokafka==0.10.0; extra == "kafka"
|
|
|
46
45
|
Requires-Dist: confluent-kafka==2.6.0; extra == "kafka"
|
|
47
46
|
Provides-Extra: protobuf
|
|
48
47
|
Requires-Dist: protobuf==4.25.5; extra == "protobuf"
|
|
48
|
+
Provides-Extra: rabbit
|
|
49
|
+
Requires-Dist: aio-pika==9.3.0; extra == "rabbit"
|
|
49
50
|
Dynamic: license-file
|
|
50
51
|
|
|
51
52
|
# Python CQRS pattern implementation with Transaction Outbox supporting
|
|
@@ -70,7 +71,8 @@ project ([documentation](https://akhundmurad.github.io/diator/)) with several en
|
|
|
70
71
|
8. FastStream supporting;
|
|
71
72
|
9. [Protobuf](https://protobuf.dev/) events supporting;
|
|
72
73
|
10. `StreamingRequestMediator` and `StreamingRequestHandler` for handling streaming requests with real-time progress updates;
|
|
73
|
-
11. Parallel event processing with configurable concurrency limits
|
|
74
|
+
11. Parallel event processing with configurable concurrency limits;
|
|
75
|
+
12. Chain of Responsibility pattern support with `CORRequestHandler` for processing requests through multiple handlers in sequence.
|
|
74
76
|
|
|
75
77
|
## Request Handlers
|
|
76
78
|
|
|
@@ -171,6 +173,9 @@ class ProcessFilesCommandHandler(StreamingRequestHandler[ProcessFilesCommand, Fi
|
|
|
171
173
|
def events(self) -> list[Event]:
|
|
172
174
|
return self._events.copy()
|
|
173
175
|
|
|
176
|
+
def clear_events(self) -> None:
|
|
177
|
+
self._events.clear()
|
|
178
|
+
|
|
174
179
|
async def handle(self, request: ProcessFilesCommand) -> typing.AsyncIterator[FileProcessedResult]:
|
|
175
180
|
for file_id in request.file_ids:
|
|
176
181
|
# Process file
|
|
@@ -178,12 +183,74 @@ class ProcessFilesCommandHandler(StreamingRequestHandler[ProcessFilesCommand, Fi
|
|
|
178
183
|
# Emit events
|
|
179
184
|
self._events.append(FileProcessedEvent(file_id=file_id, ...))
|
|
180
185
|
yield result
|
|
181
|
-
self._events.clear()
|
|
182
186
|
```
|
|
183
187
|
|
|
184
188
|
A complete example can be found in
|
|
185
189
|
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/streaming_handler_parallel_events.py)
|
|
186
190
|
|
|
191
|
+
### Chain of Responsibility Request Handler
|
|
192
|
+
|
|
193
|
+
Chain of Responsibility Request Handler implements the chain of responsibility pattern, allowing multiple handlers
|
|
194
|
+
to process a request in sequence until one successfully handles it. This pattern is particularly useful when you have
|
|
195
|
+
multiple processing strategies or need to implement fallback mechanisms.
|
|
196
|
+
|
|
197
|
+
Each handler in the chain decides whether to process the request or pass it to the next handler. The chain stops
|
|
198
|
+
when a handler successfully processes the request or when all handlers have been exhausted.
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
import typing
|
|
202
|
+
from cqrs.requests.cor_request_handler import CORRequestHandler
|
|
203
|
+
from cqrs.events.event import Event
|
|
204
|
+
|
|
205
|
+
class CreditCardPaymentHandler(CORRequestHandler[ProcessPaymentCommand, PaymentResult]):
|
|
206
|
+
def __init__(self, payment_service: PaymentServiceProtocol) -> None:
|
|
207
|
+
self._payment_service = payment_service
|
|
208
|
+
self._events: typing.List[Event] = []
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def events(self) -> typing.List[Event]:
|
|
212
|
+
return self._events
|
|
213
|
+
|
|
214
|
+
async def handle(self, request: ProcessPaymentCommand) -> PaymentResult | None:
|
|
215
|
+
if request.payment_method == "credit_card":
|
|
216
|
+
# Process credit card payment
|
|
217
|
+
result = await self._payment_service.process_credit_card(request)
|
|
218
|
+
self._events.append(PaymentProcessedEvent(...))
|
|
219
|
+
return PaymentResult(success=True, transaction_id=result.id)
|
|
220
|
+
|
|
221
|
+
# Pass to next handler
|
|
222
|
+
return await self.next(request)
|
|
223
|
+
|
|
224
|
+
class PayPalPaymentHandler(CORRequestHandler[ProcessPaymentCommand, PaymentResult]):
|
|
225
|
+
def __init__(self, paypal_service: PayPalServiceProtocol) -> None:
|
|
226
|
+
self._paypal_service = paypal_service
|
|
227
|
+
self._events: typing.List[Event] = []
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def events(self) -> typing.List[Event]:
|
|
231
|
+
return self._events
|
|
232
|
+
|
|
233
|
+
async def handle(self, request: ProcessPaymentCommand) -> PaymentResult | None:
|
|
234
|
+
if request.payment_method == "paypal":
|
|
235
|
+
# Process PayPal payment
|
|
236
|
+
result = await self._paypal_service.process_payment(request)
|
|
237
|
+
return PaymentResult(success=True, transaction_id=result.id)
|
|
238
|
+
|
|
239
|
+
# Pass to next handler
|
|
240
|
+
return await self.next(request)
|
|
241
|
+
|
|
242
|
+
# Chain registration
|
|
243
|
+
def payment_mapper(mapper: cqrs.RequestMap) -> None:
|
|
244
|
+
mapper.bind(ProcessPaymentCommand, [
|
|
245
|
+
CreditCardPaymentHandler,
|
|
246
|
+
PayPalPaymentHandler,
|
|
247
|
+
DefaultPaymentHandler # Fallback handler
|
|
248
|
+
])
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
A complete example can be found in
|
|
252
|
+
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/cor_request_handler.py)
|
|
253
|
+
|
|
187
254
|
## Event Handlers
|
|
188
255
|
|
|
189
256
|
Event handlers are designed to process `Notification` and `ECST` events that are consumed from the broker.
|
|
@@ -16,9 +16,6 @@ src/cqrs/container/__init__.py
|
|
|
16
16
|
src/cqrs/container/dependency_injector.py
|
|
17
17
|
src/cqrs/container/di.py
|
|
18
18
|
src/cqrs/container/protocol.py
|
|
19
|
-
src/cqrs/decoders/__init__.py
|
|
20
|
-
src/cqrs/decoders/kafka/__init__.py
|
|
21
|
-
src/cqrs/decoders/kafka/empty_message.py
|
|
22
19
|
src/cqrs/deserializers/__init__.py
|
|
23
20
|
src/cqrs/deserializers/json.py
|
|
24
21
|
src/cqrs/deserializers/protobuf.py
|
|
@@ -45,6 +42,7 @@ src/cqrs/outbox/repository.py
|
|
|
45
42
|
src/cqrs/outbox/sqlalchemy.py
|
|
46
43
|
src/cqrs/requests/__init__.py
|
|
47
44
|
src/cqrs/requests/bootstrap.py
|
|
45
|
+
src/cqrs/requests/cor_request_handler.py
|
|
48
46
|
src/cqrs/requests/map.py
|
|
49
47
|
src/cqrs/requests/request.py
|
|
50
48
|
src/cqrs/requests/request_handler.py
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import typing
|
|
2
|
-
|
|
3
|
-
try:
|
|
4
|
-
from faststream import kafka, types
|
|
5
|
-
except ImportError:
|
|
6
|
-
print(
|
|
7
|
-
'Please install faststream with kafka supporting (pip install "faststream[kafka]") for use kafka message decoder',
|
|
8
|
-
)
|
|
9
|
-
raise
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
async def empty_message_decoder(
|
|
13
|
-
msg: kafka.KafkaMessage,
|
|
14
|
-
original_decoder: typing.Callable[
|
|
15
|
-
[kafka.KafkaMessage],
|
|
16
|
-
typing.Awaitable[types.DecodedMessage],
|
|
17
|
-
],
|
|
18
|
-
) -> types.DecodedMessage | None:
|
|
19
|
-
"""
|
|
20
|
-
Decode a kafka message and return it if it is not empty.
|
|
21
|
-
"""
|
|
22
|
-
if not msg.body:
|
|
23
|
-
return None
|
|
24
|
-
return await original_decoder(msg)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|