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.
Files changed (61) hide show
  1. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/PKG-INFO +71 -4
  2. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/README.md +68 -2
  3. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/pyproject.toml +4 -2
  4. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/dispatcher/dispatcher.py +33 -3
  5. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/requests/bootstrap.py +2 -2
  6. python_cqrs-4.3.1/src/cqrs/requests/cor_request_handler.py +124 -0
  7. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/requests/map.py +6 -2
  8. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/requests/request_handler.py +8 -0
  9. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/python_cqrs.egg-info/PKG-INFO +71 -4
  10. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/python_cqrs.egg-info/SOURCES.txt +1 -3
  11. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/python_cqrs.egg-info/requires.txt +3 -1
  12. python_cqrs-4.2.0/src/cqrs/decoders/kafka/__init__.py +0 -5
  13. python_cqrs-4.2.0/src/cqrs/decoders/kafka/empty_message.py +0 -24
  14. python_cqrs-4.2.0/src/cqrs/outbox/__init__.py +0 -0
  15. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/LICENSE +0 -0
  16. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/setup.cfg +0 -0
  17. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/__init__.py +0 -0
  18. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/adapters/__init__.py +0 -0
  19. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/adapters/amqp.py +0 -0
  20. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/adapters/kafka.py +0 -0
  21. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/adapters/protocol.py +0 -0
  22. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/compressors/__init__.py +0 -0
  23. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/compressors/protocol.py +0 -0
  24. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/compressors/zlib.py +0 -0
  25. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/container/__init__.py +0 -0
  26. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/container/dependency_injector.py +0 -0
  27. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/container/di.py +0 -0
  28. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/container/protocol.py +0 -0
  29. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/deserializers/__init__.py +0 -0
  30. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/deserializers/json.py +0 -0
  31. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/deserializers/protobuf.py +0 -0
  32. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/dispatcher/__init__.py +0 -0
  33. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/events/__init__.py +0 -0
  34. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/events/bootstrap.py +0 -0
  35. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/events/event.py +0 -0
  36. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/events/event_emitter.py +0 -0
  37. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/events/event_handler.py +0 -0
  38. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/events/map.py +0 -0
  39. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/mediator.py +0 -0
  40. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/message_brokers/__init__.py +0 -0
  41. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/message_brokers/amqp.py +0 -0
  42. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/message_brokers/devnull.py +0 -0
  43. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/message_brokers/kafka.py +0 -0
  44. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/message_brokers/protocol.py +0 -0
  45. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/middlewares/__init__.py +0 -0
  46. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/middlewares/base.py +0 -0
  47. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/middlewares/logging.py +0 -0
  48. {python_cqrs-4.2.0/src/cqrs/decoders → python_cqrs-4.3.1/src/cqrs/outbox}/__init__.py +0 -0
  49. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/outbox/map.py +0 -0
  50. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/outbox/mock.py +0 -0
  51. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/outbox/repository.py +0 -0
  52. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/outbox/sqlalchemy.py +0 -0
  53. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/producer.py +0 -0
  54. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/requests/__init__.py +0 -0
  55. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/requests/request.py +0 -0
  56. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/response.py +0 -0
  57. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/serializers/__init__.py +0 -0
  58. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/serializers/default.py +0 -0
  59. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/cqrs/serializers/protobuf.py +0 -0
  60. {python_cqrs-4.2.0 → python_cqrs-4.3.1}/src/python_cqrs.egg-info/dependency_links.txt +0 -0
  61. {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.2.0
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.2.0"
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 | request_handler.SyncRequestHandler
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 is None:
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._container.resolve(handler_type)
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.2.0
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,6 +1,5 @@
1
1
  pydantic==2.*
2
2
  orjson==3.9.15
3
- aio-pika==9.3.0
4
3
  di[anyio]==0.79.2
5
4
  sqlalchemy[asyncio]==2.0.*
6
5
  retry-async==0.1.4
@@ -32,3 +31,6 @@ confluent-kafka==2.6.0
32
31
 
33
32
  [protobuf]
34
33
  protobuf==4.25.5
34
+
35
+ [rabbit]
36
+ aio-pika==9.3.0
@@ -1,5 +0,0 @@
1
- from cqrs.decoders.kafka.empty_message import empty_message_decoder
2
-
3
- __all__ = (
4
- "empty_message_decoder",
5
- )
@@ -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