python-cqrs 5.0.0__py3-none-any.whl → 5.2.0__py3-none-any.whl
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.
- cqrs/__init__.py +85 -0
- cqrs/adapters/__init__.py +10 -0
- cqrs/adapters/circuit_breaker.py +6 -0
- cqrs/adapters/kafka.py +10 -0
- cqrs/bootstrap/__init__.py +17 -1
- cqrs/bootstrap/events.py +13 -2
- cqrs/bootstrap/requests.py +41 -0
- cqrs/bootstrap/saga.py +14 -1
- cqrs/circuit_breaker.py +10 -0
- cqrs/compressors/protocol.py +21 -2
- cqrs/compressors/zlib.py +9 -0
- cqrs/container/__init__.py +12 -0
- cqrs/container/dependency_injector.py +6 -0
- cqrs/container/di.py +7 -0
- cqrs/container/dishka.py +14 -1
- cqrs/container/protocol.py +18 -0
- cqrs/container/scope.py +29 -1
- cqrs/dispatcher/__init__.py +9 -0
- cqrs/events/__init__.py +9 -1
- cqrs/events/event_emitter.py +14 -0
- cqrs/events/fallback.py +6 -0
- cqrs/generic_utils.py +76 -0
- cqrs/handlers/__init__.py +11 -0
- cqrs/handlers/cor.py +59 -1
- cqrs/handlers/event.py +17 -0
- cqrs/handlers/request.py +69 -1
- cqrs/handlers/saga.py +30 -0
- cqrs/mapping/events.py +13 -0
- cqrs/mapping/outbox.py +40 -0
- cqrs/mapping/requests.py +37 -0
- cqrs/mediators/event.py +12 -0
- cqrs/mediators/request.py +19 -10
- cqrs/mediators/saga.py +16 -1
- cqrs/mediators/streaming.py +23 -3
- cqrs/mermaid/cor.py +13 -0
- cqrs/mermaid/saga.py +11 -0
- cqrs/message_brokers/__init__.py +11 -0
- cqrs/message_brokers/amqp.py +9 -0
- cqrs/message_brokers/kafka.py +9 -0
- cqrs/message_brokers/producer.py +27 -1
- cqrs/message_brokers/protocol.py +17 -0
- cqrs/middlewares/__init__.py +11 -0
- cqrs/middlewares/base.py +29 -0
- cqrs/middlewares/logging.py +7 -0
- cqrs/models/event.py +54 -0
- cqrs/models/pydantic.py +34 -4
- cqrs/models/request.py +38 -4
- cqrs/models/response.py +20 -0
- cqrs/outbox/repository.py +60 -5
- cqrs/outbox/sqlalchemy.py +33 -5
- cqrs/outbox/sqlmodel.py +200 -0
- cqrs/py.typed +0 -0
- cqrs/requests/fallback.py +8 -0
- cqrs/saga/fallback.py +15 -1
- cqrs/saga/models.py +15 -0
- cqrs/saga/recovery.py +18 -0
- cqrs/saga/saga.py +37 -14
- cqrs/saga/storage/__init__.py +12 -0
- cqrs/saga/storage/memory.py +18 -1
- cqrs/saga/storage/protocol.py +8 -0
- cqrs/saga/storage/sqlalchemy.py +88 -45
- cqrs/saga/storage/sqlmodel.py +215 -0
- cqrs/serializers/json.py +8 -1
- cqrs/serializers/protobuf.py +9 -1
- cqrs/serializers/protocol.py +41 -7
- cqrs/sqlmodel_base.py +45 -0
- {python_cqrs-5.0.0.dist-info → python_cqrs-5.2.0.dist-info}/METADATA +88 -77
- python_cqrs-5.2.0.dist-info/RECORD +106 -0
- {python_cqrs-5.0.0.dist-info → python_cqrs-5.2.0.dist-info}/WHEEL +1 -1
- python_cqrs-5.0.0.dist-info/RECORD +0 -102
- {python_cqrs-5.0.0.dist-info → python_cqrs-5.2.0.dist-info}/licenses/LICENSE +0 -0
- {python_cqrs-5.0.0.dist-info → python_cqrs-5.2.0.dist-info}/top_level.txt +0 -0
cqrs/__init__.py
CHANGED
|
@@ -1,3 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
python-cqrs.
|
|
3
|
+
|
|
4
|
+
[AI Instruction]
|
|
5
|
+
Import the library as ``cqrs``. Names listed in ``__all__`` below are the
|
|
6
|
+
stable surface. Commands and queries are both ``cqrs.Request``. Wire a
|
|
7
|
+
request with ``RequestMap.bind``, an event with ``EventMap.bind``, and a
|
|
8
|
+
saga with ``SagaMap.bind``. There is no ``Command``, ``Query``, or
|
|
9
|
+
``CommandBus`` type, and no ``register_handler`` decorator.
|
|
10
|
+
|
|
11
|
+
Startup is not on this package. Import the submodule:
|
|
12
|
+
|
|
13
|
+
- ``cqrs.bootstrap.requests.bootstrap`` builds ``RequestMediator``.
|
|
14
|
+
- ``cqrs.bootstrap.requests.bootstrap_streaming`` builds
|
|
15
|
+
``StreamingRequestMediator``.
|
|
16
|
+
- ``cqrs.bootstrap.events.bootstrap`` builds ``EventMediator``.
|
|
17
|
+
- ``cqrs.bootstrap.saga.bootstrap`` builds ``SagaMediator``.
|
|
18
|
+
|
|
19
|
+
Requests and handlers on ``cqrs``: ``Request``, ``DCRequest``, ``Response``,
|
|
20
|
+
``RequestHandler``, ``StreamingRequestHandler``, ``RequestMediator``,
|
|
21
|
+
``StreamingRequestMediator``, ``RequestMap``, ``RequestHandlerFallback``.
|
|
22
|
+
``PydanticRequest`` and ``PydanticResponse`` are lazy and need the
|
|
23
|
+
``pydantic`` extra. A chain of handlers is ``cqrs.handlers.CORRequestHandler``.
|
|
24
|
+
``build_chain`` is ``cqrs.handlers.cor.build_chain``.
|
|
25
|
+
|
|
26
|
+
Events on ``cqrs``: ``DomainEvent`` for this process, ``NotificationEvent``
|
|
27
|
+
for another service, plus ``EventHandler``, ``EventMap``, ``EventMediator``,
|
|
28
|
+
``EventEmitter``, ``EventHandlerFallback``. Pydantic event classes are lazy
|
|
29
|
+
and need the ``pydantic`` extra.
|
|
30
|
+
|
|
31
|
+
Sagas on ``cqrs``: ``Saga``, ``SagaStepHandler``, ``SagaStepResult``,
|
|
32
|
+
``SagaMediator``, ``SagaMap``. Import the rest from their modules.
|
|
33
|
+
``SagaContext`` is ``cqrs.saga.models.SagaContext``. A fallback step is
|
|
34
|
+
``cqrs.saga.fallback.Fallback``. Resume a run with
|
|
35
|
+
``cqrs.saga.recovery.recover_saga``. The storage protocol is
|
|
36
|
+
``cqrs.saga.storage.ISagaStorage``. Implementations are
|
|
37
|
+
``cqrs.saga.storage.memory.MemorySagaStorage``,
|
|
38
|
+
``cqrs.saga.storage.sqlalchemy.SqlAlchemySagaStorage`` (extra
|
|
39
|
+
``sqlalchemy``), and ``cqrs.saga.storage.sqlmodel.SqlModelSagaStorage``
|
|
40
|
+
(extra ``sqlmodel``). Those storage classes are not attributes of ``cqrs``.
|
|
41
|
+
|
|
42
|
+
Outbox on ``cqrs``: ``OutboxedEventRepository``, ``OutboxedEventMap``,
|
|
43
|
+
``EventProducer``, ``EventStatus``. ``SqlAlchemyOutboxedEventRepository``,
|
|
44
|
+
``rebind_outbox_model``, and the dialect types are lazy and need the
|
|
45
|
+
``sqlalchemy`` extra. ``SqlModelOutboxedEventRepository`` is lazy and needs
|
|
46
|
+
the ``sqlmodel`` extra. Codecs on ``cqrs``: ``JsonEventSerializer``,
|
|
47
|
+
``ProtobufEventSerializer`` (extra ``protobuf``).
|
|
48
|
+
|
|
49
|
+
Brokers are not on ``cqrs``. Kafka is
|
|
50
|
+
``cqrs.adapters.kafka.kafka_producer_factory`` plus
|
|
51
|
+
``cqrs.message_brokers.kafka.KafkaMessageBroker`` (extra ``kafka``). AMQP is
|
|
52
|
+
``cqrs.message_brokers.amqp.AMQPMessageBroker`` (extra ``rabbit``).
|
|
53
|
+
|
|
54
|
+
Containers: pass one into bootstrap. ``cqrs.DIContainer`` wraps the ``di``
|
|
55
|
+
package, and bootstrap wraps a raw ``di.Container`` itself.
|
|
56
|
+
``cqrs.container.dependency_injector.DependencyInjectorCQRSContainer`` wraps
|
|
57
|
+
dependency-injector. ``cqrs.container.dishka.DishkaCQRSContainer`` needs the
|
|
58
|
+
``dishka`` extra. Scope helpers on ``cqrs`` are ``ScopeStrategy``,
|
|
59
|
+
``bind_scope``, ``enter_scope``, and ``current_container``.
|
|
60
|
+
|
|
61
|
+
Middleware is ``cqrs.middlewares.Middleware``. Logging is
|
|
62
|
+
``cqrs.middlewares.logging.LoggingMiddleware``; bootstrap adds it when the
|
|
63
|
+
list does not already contain it. A circuit breaker adapter is
|
|
64
|
+
``cqrs.adapters.circuit_breaker.AioBreakerAdapter`` (extra ``aiobreaker``).
|
|
65
|
+
Diagrams on ``cqrs`` are ``CoRMermaid`` and ``SagaMermaid``.
|
|
66
|
+
|
|
67
|
+
Do not import ``cqrs.dispatcher`` from application code. Mediators call it.
|
|
68
|
+
Optional integrations are extras in ``pyproject.toml``. Reading a lazy name
|
|
69
|
+
without its extra raises ``ImportError``.
|
|
70
|
+
"""
|
|
71
|
+
|
|
1
72
|
import typing
|
|
2
73
|
|
|
3
74
|
if typing.TYPE_CHECKING:
|
|
@@ -12,6 +83,7 @@ if typing.TYPE_CHECKING:
|
|
|
12
83
|
SqlAlchemyOutboxedEventRepository,
|
|
13
84
|
rebind_outbox_model,
|
|
14
85
|
)
|
|
86
|
+
from cqrs.outbox.sqlmodel import SqlModelOutboxedEventRepository
|
|
15
87
|
from cqrs.sqlalchemy_types import (
|
|
16
88
|
Binary16,
|
|
17
89
|
DialectAwareType,
|
|
@@ -129,6 +201,7 @@ __all__ = (
|
|
|
129
201
|
"PydanticResponse",
|
|
130
202
|
"OutboxedEventRepository",
|
|
131
203
|
"SqlAlchemyOutboxedEventRepository",
|
|
204
|
+
"SqlModelOutboxedEventRepository",
|
|
132
205
|
"EventProducer",
|
|
133
206
|
"EventSerializer",
|
|
134
207
|
"EventCodec",
|
|
@@ -187,6 +260,13 @@ _LAZY_SQLALCHEMY = {
|
|
|
187
260
|
"PayloadBinary": ("cqrs.sqlalchemy_types", "PayloadBinary"),
|
|
188
261
|
}
|
|
189
262
|
|
|
263
|
+
_LAZY_SQLMODEL = {
|
|
264
|
+
"SqlModelOutboxedEventRepository": (
|
|
265
|
+
"cqrs.outbox.sqlmodel",
|
|
266
|
+
"SqlModelOutboxedEventRepository",
|
|
267
|
+
),
|
|
268
|
+
}
|
|
269
|
+
|
|
190
270
|
|
|
191
271
|
def __getattr__(name: str) -> typing.Any:
|
|
192
272
|
if name in _LAZY_PYDANTIC:
|
|
@@ -201,4 +281,9 @@ def __getattr__(name: str) -> typing.Any:
|
|
|
201
281
|
# ImportError message is raised by the sqlalchemy modules themselves.
|
|
202
282
|
module = __import__(module_name, fromlist=[attr])
|
|
203
283
|
return getattr(module, attr)
|
|
284
|
+
if name in _LAZY_SQLMODEL:
|
|
285
|
+
module_name, attr = _LAZY_SQLMODEL[name]
|
|
286
|
+
# ImportError message is raised by the sqlmodel modules themselves.
|
|
287
|
+
module = __import__(module_name, fromlist=[attr])
|
|
288
|
+
return getattr(module, attr)
|
|
204
289
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
cqrs/adapters/__init__.py
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Broker and producer adapters.
|
|
3
|
+
|
|
4
|
+
[AI Instruction]
|
|
5
|
+
This package has no re-exports. Kafka is ``cqrs.adapters.kafka`` (extra
|
|
6
|
+
``kafka``). AMQP is ``cqrs.adapters.amqp`` (extra ``rabbit``). The circuit
|
|
7
|
+
breaker adapter is ``cqrs.adapters.circuit_breaker.AioBreakerAdapter``
|
|
8
|
+
(extra ``aiobreaker``). Broker classes themselves are
|
|
9
|
+
``cqrs.message_brokers.kafka`` and ``cqrs.message_brokers.amqp``.
|
|
10
|
+
"""
|
cqrs/adapters/circuit_breaker.py
CHANGED
|
@@ -136,6 +136,12 @@ class AioBreakerAdapter(ICircuitBreaker, ISagaStepCircuitBreaker):
|
|
|
136
136
|
saga_cb = AioBreakerAdapter(fail_max=3, timeout_duration=60)
|
|
137
137
|
request_cb = AioBreakerAdapter(fail_max=5, timeout_duration=30)
|
|
138
138
|
event_cb = AioBreakerAdapter(fail_max=5, timeout_duration=30)
|
|
139
|
+
|
|
140
|
+
[AI Instruction]
|
|
141
|
+
Construct one adapter per area (requests, events, saga) and pass it into
|
|
142
|
+
that area's fallback. ``fail_max`` is how many failures open the circuit.
|
|
143
|
+
``timeout_duration`` is how long it stays open. Requires ``aiobreaker``.
|
|
144
|
+
A business exception listed in ``exclude`` must not open the circuit.
|
|
139
145
|
"""
|
|
140
146
|
|
|
141
147
|
def __init__(
|
cqrs/adapters/kafka.py
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kafka producer adapter.
|
|
3
|
+
|
|
4
|
+
[AI Instruction]
|
|
5
|
+
Import ``kafka_producer_factory`` from this module and pass the result to
|
|
6
|
+
``cqrs.message_brokers.kafka.KafkaMessageBroker``. Both need the ``kafka``
|
|
7
|
+
extra. Application code does not call the producer directly when an outbox
|
|
8
|
+
or an event emitter is already publishing.
|
|
9
|
+
"""
|
|
10
|
+
|
|
1
11
|
import asyncio
|
|
2
12
|
import functools
|
|
3
13
|
import logging
|
cqrs/bootstrap/__init__.py
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""
|
|
2
|
+
Bootstrap helpers for mediators (requests, events, saga).
|
|
3
|
+
|
|
4
|
+
[AI Instruction]
|
|
5
|
+
These submodules are the application entry points. ``cqrs`` does not export
|
|
6
|
+
them.
|
|
7
|
+
|
|
8
|
+
- ``cqrs.bootstrap.requests.bootstrap`` returns ``RequestMediator``.
|
|
9
|
+
``bootstrap_streaming`` in the same module returns
|
|
10
|
+
``StreamingRequestMediator``.
|
|
11
|
+
- ``cqrs.bootstrap.events.bootstrap`` returns ``EventMediator``.
|
|
12
|
+
- ``cqrs.bootstrap.saga.bootstrap`` returns ``SagaMediator``.
|
|
13
|
+
|
|
14
|
+
Import the submodule (``from cqrs.bootstrap import requests``) and call
|
|
15
|
+
``bootstrap`` on it. ``setup_*`` functions in those modules are for callers
|
|
16
|
+
that already hold an emitter or a container.
|
|
17
|
+
"""
|
|
2
18
|
|
|
3
19
|
from cqrs.bootstrap import events, requests, saga
|
|
4
20
|
|
cqrs/bootstrap/events.py
CHANGED
|
@@ -58,7 +58,12 @@ def setup_mediator(
|
|
|
58
58
|
middlewares=[logging_middleware.LoggingMiddleware()],
|
|
59
59
|
events_mapper=bind_events,
|
|
60
60
|
)
|
|
61
|
-
await mediator.
|
|
61
|
+
await mediator.send(OrderCreatedEvent(order_id="1"))
|
|
62
|
+
|
|
63
|
+
[AI Instruction]
|
|
64
|
+
Assemble an ``EventMediator`` from a container the caller already holds.
|
|
65
|
+
Application startup should call ``bootstrap`` instead, unless it needs
|
|
66
|
+
this lower-level wiring.
|
|
62
67
|
"""
|
|
63
68
|
_events_mapper = events.EventMap()
|
|
64
69
|
if events_mapper is not None:
|
|
@@ -130,7 +135,13 @@ def bootstrap(
|
|
|
130
135
|
di_container=di.Container(),
|
|
131
136
|
events_mapper=bind_events,
|
|
132
137
|
)
|
|
133
|
-
await mediator.
|
|
138
|
+
await mediator.send(OrderCreatedEvent(order_id="1"))
|
|
139
|
+
|
|
140
|
+
[AI Instruction]
|
|
141
|
+
Build an ``EventMediator`` for events that arrive from outside this
|
|
142
|
+
process. ``events_mapper`` calls ``EventMap.bind``. The application then
|
|
143
|
+
``await mediator.send(event)``. Commands and queries use
|
|
144
|
+
``cqrs.bootstrap.requests.bootstrap``.
|
|
134
145
|
"""
|
|
135
146
|
if on_startup is None:
|
|
136
147
|
on_startup = []
|
cqrs/bootstrap/requests.py
CHANGED
|
@@ -46,6 +46,13 @@ def setup_event_emitter(
|
|
|
46
46
|
*,
|
|
47
47
|
serializer: EventSerializer | None = None,
|
|
48
48
|
) -> events.EventEmitter:
|
|
49
|
+
"""
|
|
50
|
+
[AI Instruction]
|
|
51
|
+
Build the emitter ``bootstrap`` uses to publish handler events. Call
|
|
52
|
+
``bootstrap`` from application startup. Use this only when the
|
|
53
|
+
application already owns the container and must pass a custom emitter
|
|
54
|
+
into ``setup_mediator``.
|
|
55
|
+
"""
|
|
49
56
|
if message_broker is None:
|
|
50
57
|
message_broker = DEFAULT_MESSAGE_BROKER
|
|
51
58
|
event_mapper = events.EventMap()
|
|
@@ -101,6 +108,12 @@ def setup_mediator(
|
|
|
101
108
|
concurrent_event_handle_enable: bool | None = None,
|
|
102
109
|
scope_strategy: ScopeStrategy = ScopeStrategy.NONE,
|
|
103
110
|
) -> cqrs.RequestMediator:
|
|
111
|
+
"""
|
|
112
|
+
[AI Instruction]
|
|
113
|
+
Wire maps and middleware into a ``RequestMediator``. Application startup
|
|
114
|
+
should call ``bootstrap``, which also creates the emitter and runs
|
|
115
|
+
``on_startup``. Use this when those pieces already exist.
|
|
116
|
+
"""
|
|
104
117
|
requests_mapper = RequestMap()
|
|
105
118
|
if commands_mapper:
|
|
106
119
|
commands_mapper(requests_mapper)
|
|
@@ -176,6 +189,20 @@ def bootstrap(
|
|
|
176
189
|
*,
|
|
177
190
|
serializer: EventSerializer | None = None,
|
|
178
191
|
) -> cqrs.RequestMediator:
|
|
192
|
+
"""
|
|
193
|
+
Create a ``RequestMediator``.
|
|
194
|
+
|
|
195
|
+
[AI Instruction]
|
|
196
|
+
Build the ``RequestMediator`` an application calls for commands and
|
|
197
|
+
queries. Pass ``commands_mapper`` and ``queries_mapper`` that call
|
|
198
|
+
``mapper.bind``. Both kinds of request are ``cqrs.Request``.
|
|
199
|
+
``domain_events_mapper`` registers in-process reactions to events those
|
|
200
|
+
handlers produce. Logging middleware is added when the list does not
|
|
201
|
+
already contain it. A missing broker becomes a broker that discards
|
|
202
|
+
messages; pass a real broker when notifications must leave the process.
|
|
203
|
+
Install optional integrations explicitly (``pydantic``, ``sqlalchemy``,
|
|
204
|
+
``sqlmodel``, ``kafka``, and the others in ``pyproject.toml``).
|
|
205
|
+
"""
|
|
179
206
|
if message_broker is None:
|
|
180
207
|
message_broker = DEFAULT_MESSAGE_BROKER
|
|
181
208
|
if on_startup is None:
|
|
@@ -258,6 +285,12 @@ def setup_streaming_mediator(
|
|
|
258
285
|
concurrent_event_handle_enable: bool | None = None,
|
|
259
286
|
scope_strategy: ScopeStrategy = ScopeStrategy.NONE,
|
|
260
287
|
) -> cqrs.StreamingRequestMediator:
|
|
288
|
+
"""
|
|
289
|
+
[AI Instruction]
|
|
290
|
+
Wire maps and middleware into a ``StreamingRequestMediator``. Application
|
|
291
|
+
startup should call ``bootstrap_streaming``. Use this when the emitter
|
|
292
|
+
already exists.
|
|
293
|
+
"""
|
|
261
294
|
requests_mapper = RequestMap()
|
|
262
295
|
if commands_mapper:
|
|
263
296
|
commands_mapper(requests_mapper)
|
|
@@ -333,6 +366,14 @@ def bootstrap_streaming(
|
|
|
333
366
|
*,
|
|
334
367
|
serializer: EventSerializer | None = None,
|
|
335
368
|
) -> cqrs.StreamingRequestMediator:
|
|
369
|
+
"""
|
|
370
|
+
[AI Instruction]
|
|
371
|
+
Build the mediator for handlers that yield many results. Wire streaming
|
|
372
|
+
handlers through ``commands_mapper`` or ``queries_mapper`` with
|
|
373
|
+
``mapper.bind``, the same as a normal request. The caller iterates
|
|
374
|
+
``mediator.stream``, it does not ``await send``. Use ``bootstrap`` when
|
|
375
|
+
each request returns one value.
|
|
376
|
+
"""
|
|
336
377
|
if message_broker is None:
|
|
337
378
|
message_broker = DEFAULT_MESSAGE_BROKER
|
|
338
379
|
if on_startup is None:
|
cqrs/bootstrap/saga.py
CHANGED
|
@@ -73,6 +73,11 @@ def setup_saga_mediator(
|
|
|
73
73
|
|
|
74
74
|
Returns:
|
|
75
75
|
Configured SagaMediator instance
|
|
76
|
+
|
|
77
|
+
[AI Instruction]
|
|
78
|
+
Wire an existing emitter into a ``SagaMediator``. Application startup
|
|
79
|
+
should call ``bootstrap``, which creates the emitter and runs
|
|
80
|
+
``on_startup``. Use this when those pieces already exist.
|
|
76
81
|
"""
|
|
77
82
|
saga_map = SagaMap()
|
|
78
83
|
if sagas_mapper:
|
|
@@ -179,7 +184,7 @@ def bootstrap(
|
|
|
179
184
|
|
|
180
185
|
# Execute saga (stream() returns AsyncIterator, consumed with async for)
|
|
181
186
|
async for result in mediator.execute(order_context):
|
|
182
|
-
print(f"Step: {result.
|
|
187
|
+
print(f"Step: {result.step_type.__name__}")
|
|
183
188
|
|
|
184
189
|
Args:
|
|
185
190
|
di_container: DI container (di.Container or CQRSContainer)
|
|
@@ -208,6 +213,14 @@ def bootstrap(
|
|
|
208
213
|
container = di.Container()
|
|
209
214
|
container.bind(ISagaStorage, instance=storage) # Register storage
|
|
210
215
|
mediator = bootstrap(container, saga_storage=storage, ...)
|
|
216
|
+
|
|
217
|
+
[AI Instruction]
|
|
218
|
+
Build a ``SagaMediator`` for a process whose steps must be undone across
|
|
219
|
+
services. ``sagas_mapper`` binds each context class to one ``Saga``.
|
|
220
|
+
``domain_events_mapper`` binds reactions to events those steps produce.
|
|
221
|
+
Pass a durable ``saga_storage`` when a crash must be recoverable; the
|
|
222
|
+
default keeps state in memory. The caller iterates ``mediator.execute``.
|
|
223
|
+
A single-service command uses ``cqrs.bootstrap.requests.bootstrap``.
|
|
211
224
|
"""
|
|
212
225
|
if message_broker is None:
|
|
213
226
|
message_broker = DEFAULT_MESSAGE_BROKER
|
cqrs/circuit_breaker.py
CHANGED
|
@@ -14,6 +14,12 @@ class ICircuitBreaker(typing.Protocol):
|
|
|
14
14
|
Note:
|
|
15
15
|
Implementors should use a dedicated adapter instance per domain (events,
|
|
16
16
|
requests, saga) to keep circuit breaker state isolated.
|
|
17
|
+
|
|
18
|
+
[AI Instruction]
|
|
19
|
+
Implement this, or use ``AioBreakerAdapter``, to stop calling a failing
|
|
20
|
+
dependency. Pass one instance into ``RequestHandlerFallback``,
|
|
21
|
+
``EventHandlerFallback``, or saga ``Fallback``. Use a separate instance
|
|
22
|
+
per area so one open circuit does not disable the others.
|
|
17
23
|
"""
|
|
18
24
|
|
|
19
25
|
async def call(
|
|
@@ -38,6 +44,10 @@ class ICircuitBreaker(typing.Protocol):
|
|
|
38
44
|
Raises:
|
|
39
45
|
CircuitBreakerError: If the circuit breaker is open.
|
|
40
46
|
Exception: Any exception raised by func (if circuit is closed).
|
|
47
|
+
|
|
48
|
+
[AI Instruction]
|
|
49
|
+
Run ``func`` if the circuit for ``identifier`` is closed. The
|
|
50
|
+
fallback wrappers call this. Application handlers do not.
|
|
41
51
|
"""
|
|
42
52
|
...
|
|
43
53
|
|
cqrs/compressors/protocol.py
CHANGED
|
@@ -2,10 +2,29 @@ import typing
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class Compressor(typing.Protocol):
|
|
5
|
+
"""
|
|
6
|
+
[AI Instruction]
|
|
7
|
+
Optional compression for outbox payloads. Pass an implementation, usually
|
|
8
|
+
``ZlibCompressor``, to the outbox repository. The same compressor must
|
|
9
|
+
read what it wrote. Omit it to store payloads uncompressed.
|
|
10
|
+
"""
|
|
11
|
+
|
|
5
12
|
def compress(self, value: bytes) -> bytes:
|
|
6
|
-
"""
|
|
13
|
+
"""
|
|
14
|
+
Compress value.
|
|
15
|
+
|
|
16
|
+
[AI Instruction]
|
|
17
|
+
Shrink outbox payload bytes before they are stored. The repository
|
|
18
|
+
calls this. Handlers do not.
|
|
19
|
+
"""
|
|
7
20
|
raise NotImplementedError
|
|
8
21
|
|
|
9
22
|
def decompress(self, value: bytes) -> bytes:
|
|
10
|
-
"""
|
|
23
|
+
"""
|
|
24
|
+
Decompress compressed value.
|
|
25
|
+
|
|
26
|
+
[AI Instruction]
|
|
27
|
+
Restore bytes ``compress`` wrote. The repository calls this when
|
|
28
|
+
reading a row.
|
|
29
|
+
"""
|
|
11
30
|
raise NotImplementedError
|
cqrs/compressors/zlib.py
CHANGED
|
@@ -2,6 +2,15 @@ import zlib
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class ZlibCompressor:
|
|
5
|
+
"""
|
|
6
|
+
[AI Instruction]
|
|
7
|
+
Optional compressor for outbox payloads. It is not applied unless the
|
|
8
|
+
application passes it in. Supply a ``ZlibCompressor`` to
|
|
9
|
+
``SqlAlchemyOutboxedEventRepository`` when writing compressed rows and
|
|
10
|
+
the same type when reading them. Leave the repository without a
|
|
11
|
+
compressor to store raw bytes.
|
|
12
|
+
"""
|
|
13
|
+
|
|
5
14
|
def compress(self, value: bytes) -> bytes:
|
|
6
15
|
return zlib.compress(value)
|
|
7
16
|
|
cqrs/container/__init__.py
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dependency-injection containers and scope helpers.
|
|
3
|
+
|
|
4
|
+
[AI Instruction]
|
|
5
|
+
``Container``, ``ScopeStrategy``, ``bind_scope``, ``enter_scope``, and
|
|
6
|
+
``current_container`` are also on ``cqrs``. ``cqrs.DIContainer`` lives in
|
|
7
|
+
``cqrs.container.di`` and wraps the ``di`` package. The other adapters are
|
|
8
|
+
not re-exported here: ``DependencyInjectorCQRSContainer`` is
|
|
9
|
+
``cqrs.container.dependency_injector``, and ``DishkaCQRSContainer`` is
|
|
10
|
+
``cqrs.container.dishka`` (extra ``dishka``).
|
|
11
|
+
"""
|
|
12
|
+
|
|
1
13
|
from cqrs.container.protocol import Container, SupportsScope
|
|
2
14
|
from cqrs.container.scope import (
|
|
3
15
|
ScopeAwareContainer,
|
|
@@ -49,6 +49,12 @@ class DependencyInjectorCQRSContainer(
|
|
|
49
49
|
>>> service = await cqrs_container.resolve(UserService)
|
|
50
50
|
>>> # Or resolve by abstract interface
|
|
51
51
|
>>> service = await cqrs_container.resolve(IUserService)
|
|
52
|
+
|
|
53
|
+
[AI Instruction]
|
|
54
|
+
Wrap a dependency-injector declarative container and pass this object to
|
|
55
|
+
``bootstrap``. Call ``attach_external_container`` before the first
|
|
56
|
+
resolve. Providers are discovered by type. This is not ``DIContainer``,
|
|
57
|
+
which wraps the ``di`` package.
|
|
52
58
|
"""
|
|
53
59
|
|
|
54
60
|
def __init__(self) -> None:
|
cqrs/container/di.py
CHANGED
|
@@ -18,6 +18,13 @@ class DIContainer(Container[di.Container]):
|
|
|
18
18
|
When a scope is opened via :meth:`open_scope`, generator providers stay alive
|
|
19
19
|
until the scope exits. Legacy :meth:`resolve` on the root container still
|
|
20
20
|
opens a one-shot scope (previous behaviour).
|
|
21
|
+
|
|
22
|
+
[AI Instruction]
|
|
23
|
+
Wrap a ``di.Container`` when bootstrap is not doing it. ``bootstrap``
|
|
24
|
+
wraps a raw ``di.Container`` itself. Implement ``open_scope`` behavior
|
|
25
|
+
through this class when ``ScopeStrategy`` is ``SEND`` or ``HANDLER``.
|
|
26
|
+
Other containers (``dependency-injector``, Dishka) have their own
|
|
27
|
+
adapters; do not force those through ``DIContainer``.
|
|
21
28
|
"""
|
|
22
29
|
|
|
23
30
|
def __init__(self, scope: typing.Any = "request") -> None:
|
cqrs/container/dishka.py
CHANGED
|
@@ -16,6 +16,13 @@ class DishkaCQRSContainer(Container[AsyncContainer]):
|
|
|
16
16
|
|
|
17
17
|
Request-scoped providers (``Scope.REQUEST``) are finalized when the CQRS
|
|
18
18
|
scope opened via :meth:`open_scope` exits.
|
|
19
|
+
|
|
20
|
+
[AI Instruction]
|
|
21
|
+
Wrap a Dishka ``AsyncContainer`` and pass this to ``bootstrap`` when the
|
|
22
|
+
application uses Dishka. Install ``python-cqrs[dishka]``. If FastAPI
|
|
23
|
+
middleware already opened the request scope, wrap that scoped container
|
|
24
|
+
with ``DishkaCQRSContainer.of`` and ``bind_scope`` instead of asking CQRS
|
|
25
|
+
to open another one.
|
|
19
26
|
"""
|
|
20
27
|
|
|
21
28
|
def __init__(self, container: AsyncContainer | None = None) -> None:
|
|
@@ -23,7 +30,13 @@ class DishkaCQRSContainer(Container[AsyncContainer]):
|
|
|
23
30
|
|
|
24
31
|
@classmethod
|
|
25
32
|
def of(cls, container: AsyncContainer) -> DishkaCQRSContainer:
|
|
26
|
-
"""Wrap an already-opened dishka container (e.g. from FastAPI middleware).
|
|
33
|
+
"""Wrap an already-opened dishka container (e.g. from FastAPI middleware).
|
|
34
|
+
|
|
35
|
+
[AI Instruction]
|
|
36
|
+
Use this for the container a request middleware already entered.
|
|
37
|
+
Pair it with ``bind_scope`` so handlers resolve from that scope.
|
|
38
|
+
``ScopeStrategy.SEND`` then joins it.
|
|
39
|
+
"""
|
|
27
40
|
return cls(container)
|
|
28
41
|
|
|
29
42
|
@property
|
cqrs/container/protocol.py
CHANGED
|
@@ -7,6 +7,12 @@ C = typing.TypeVar("C")
|
|
|
7
7
|
class Container(typing.Protocol[C]):
|
|
8
8
|
"""
|
|
9
9
|
The container interface.
|
|
10
|
+
|
|
11
|
+
[AI Instruction]
|
|
12
|
+
The mediator resolves handlers from this. Pass the application's
|
|
13
|
+
container to ``bootstrap``. Implement ``resolve`` so a handler type
|
|
14
|
+
returns a constructed instance with its dependencies. Scope boundaries
|
|
15
|
+
are ``ScopeStrategy`` on bootstrap, not a method the handler calls.
|
|
10
16
|
"""
|
|
11
17
|
|
|
12
18
|
@property
|
|
@@ -17,6 +23,12 @@ class Container(typing.Protocol[C]):
|
|
|
17
23
|
raise NotImplementedError
|
|
18
24
|
|
|
19
25
|
async def resolve(self, type_: typing.Type[T]) -> T:
|
|
26
|
+
"""
|
|
27
|
+
[AI Instruction]
|
|
28
|
+
Return one instance of ``type_``. Mediators call this for handlers
|
|
29
|
+
and saga steps. Application handlers receive dependencies through
|
|
30
|
+
their constructor; they do not call ``resolve`` themselves.
|
|
31
|
+
"""
|
|
20
32
|
raise NotImplementedError
|
|
21
33
|
|
|
22
34
|
|
|
@@ -39,5 +51,11 @@ class SupportsScope(typing.Protocol[C]):
|
|
|
39
51
|
The yielded object must be a **new** container instance (not ``self``)
|
|
40
52
|
so concurrent ``send()`` calls do not share the same unit of work.
|
|
41
53
|
Cleanup / finalization of generator providers must happen on exit.
|
|
54
|
+
|
|
55
|
+
[AI Instruction]
|
|
56
|
+
Implement this on a container that can open a unit of work. The
|
|
57
|
+
yielded container must be a new instance. ``ScopeStrategy.SEND`` and
|
|
58
|
+
``HANDLER`` call this. A container without it still works with
|
|
59
|
+
``ScopeStrategy.NONE``.
|
|
42
60
|
"""
|
|
43
61
|
...
|
cqrs/container/scope.py
CHANGED
|
@@ -43,6 +43,14 @@ class ScopeStrategy(enum.Enum):
|
|
|
43
43
|
|
|
44
44
|
NONE (default):
|
|
45
45
|
Do not open scopes (legacy behaviour). Fallback is one-shot like today.
|
|
46
|
+
|
|
47
|
+
[AI Instruction]
|
|
48
|
+
Choose where a unit of work opens. Leave ``NONE`` when the application
|
|
49
|
+
already manages sessions. Use ``SEND`` when one command, its events, and
|
|
50
|
+
its outbox must share one session; event handlers then run one after
|
|
51
|
+
another. Use ``HANDLER`` when each handler or saga step needs a fresh
|
|
52
|
+
session. Pass the same value to ``bootstrap``, the mediator, and
|
|
53
|
+
``recover_saga``. ``SEND`` cannot run event handlers concurrently.
|
|
46
54
|
"""
|
|
47
55
|
|
|
48
56
|
SEND = "send"
|
|
@@ -77,7 +85,14 @@ def resolve_concurrent_event_handling(
|
|
|
77
85
|
|
|
78
86
|
|
|
79
87
|
def current_container() -> Container | None:
|
|
80
|
-
"""
|
|
88
|
+
"""
|
|
89
|
+
Return the container bound to the current scope, if any.
|
|
90
|
+
|
|
91
|
+
[AI Instruction]
|
|
92
|
+
Read the container the active ``enter_scope`` or ``bind_scope`` bound.
|
|
93
|
+
Returns ``None`` outside a scope. Handlers should use constructor
|
|
94
|
+
dependencies rather than looking the container up from here.
|
|
95
|
+
"""
|
|
81
96
|
return _current_container.get()
|
|
82
97
|
|
|
83
98
|
|
|
@@ -268,6 +283,13 @@ async def enter_scope(
|
|
|
268
283
|
If a scope is already active and ``reuse_existing`` is true, joins it (no nested
|
|
269
284
|
scope). If the container does not implement :class:`SupportsScope`, yields the
|
|
270
285
|
container without opening a scope.
|
|
286
|
+
|
|
287
|
+
[AI Instruction]
|
|
288
|
+
Open a unit of work for a block the framework does not already wrap.
|
|
289
|
+
Mediators call this for ``ScopeStrategy.SEND``. Application code calls it
|
|
290
|
+
when it drives a mediator or saga outside bootstrap and must share one
|
|
291
|
+
session. A container that does not implement ``SupportsScope`` is yielded
|
|
292
|
+
unchanged.
|
|
271
293
|
"""
|
|
272
294
|
existing = _current_container.get()
|
|
273
295
|
if existing is not None and _is_scope_closed(existing):
|
|
@@ -300,6 +322,12 @@ async def bind_scope(
|
|
|
300
322
|
|
|
301
323
|
Use when FastAPI/dishka middleware (or similar) already owns the scope and
|
|
302
324
|
CQRS handlers must resolve from that same instance.
|
|
325
|
+
|
|
326
|
+
[AI Instruction]
|
|
327
|
+
Use when FastAPI, Dishka, or another middleware already opened the
|
|
328
|
+
session. Bind that scoped container for the request so CQRS resolves
|
|
329
|
+
handlers from it. ``ScopeStrategy.SEND`` joins this scope instead of
|
|
330
|
+
opening another one.
|
|
303
331
|
"""
|
|
304
332
|
token = _current_container.set(scoped_container)
|
|
305
333
|
try:
|
cqrs/dispatcher/__init__.py
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Internal dispatchers used by mediators.
|
|
3
|
+
|
|
4
|
+
[AI Instruction]
|
|
5
|
+
Application code does not import this package. Call ``send``, ``stream``, or
|
|
6
|
+
``execute`` on a mediator from ``cqrs.bootstrap``. Replace a dispatcher only
|
|
7
|
+
by passing ``dispatcher_type`` when constructing a mediator directly.
|
|
8
|
+
"""
|
|
9
|
+
|
|
1
10
|
from cqrs.dispatcher.event import EventDispatcher
|
|
2
11
|
from cqrs.dispatcher.models import RequestDispatchResult, SagaDispatchResult
|
|
3
12
|
from cqrs.dispatcher.request import RequestDispatcher
|
cqrs/events/__init__.py
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""
|
|
2
|
+
Event handlers, emitter, and map for the CQRS events layer.
|
|
3
|
+
|
|
4
|
+
[AI Instruction]
|
|
5
|
+
These names are also on ``cqrs``. Import ``DomainEvent``, ``NotificationEvent``,
|
|
6
|
+
``EventHandler``, ``EventMap``, ``EventEmitter``, and ``EventHandlerFallback``
|
|
7
|
+
from ``cqrs``. Pydantic event classes need the ``pydantic`` extra and are
|
|
8
|
+
loaded lazily.
|
|
9
|
+
"""
|
|
2
10
|
|
|
3
11
|
import typing
|
|
4
12
|
|
cqrs/events/event_emitter.py
CHANGED
|
@@ -34,6 +34,12 @@ class EventEmitter:
|
|
|
34
34
|
returns follow-up events from :attr:`~cqrs.events.event_handler.EventHandler.events`.
|
|
35
35
|
For :class:`~cqrs.events.event.INotificationEvent`: sends the event to the
|
|
36
36
|
message broker (if configured) and returns an empty sequence.
|
|
37
|
+
|
|
38
|
+
[AI Instruction]
|
|
39
|
+
The mediator owns one of these and calls ``emit`` for events a handler
|
|
40
|
+
put on ``events``. Application code constructs it only when it builds a
|
|
41
|
+
mediator by hand. A domain event runs ``EventMap`` handlers. A
|
|
42
|
+
notification event is published and is not a guaranteed outbox write.
|
|
37
43
|
"""
|
|
38
44
|
|
|
39
45
|
def __init__(
|
|
@@ -95,6 +101,14 @@ class EventEmitter:
|
|
|
95
101
|
follow_ups = await emitter.emit(OrderCreatedEvent(order_id="1"))
|
|
96
102
|
for e in follow_ups:
|
|
97
103
|
await emitter.emit(e) # or process via EventProcessor
|
|
104
|
+
|
|
105
|
+
[AI Instruction]
|
|
106
|
+
Publish one event now. Domain events return the follow-up events their
|
|
107
|
+
handlers produced; the caller decides whether to emit those too.
|
|
108
|
+
Notification events go to the broker and return nothing. Handlers
|
|
109
|
+
should not call this; they append to ``events`` and the mediator
|
|
110
|
+
emits. For delivery that must commit with the business write, use the
|
|
111
|
+
outbox instead of emitting a notification directly.
|
|
98
112
|
"""
|
|
99
113
|
return ()
|
|
100
114
|
|
cqrs/events/fallback.py
CHANGED
|
@@ -41,6 +41,12 @@ class EventHandlerFallback:
|
|
|
41
41
|
circuit_breaker=event_cb,
|
|
42
42
|
),
|
|
43
43
|
)
|
|
44
|
+
|
|
45
|
+
[AI Instruction]
|
|
46
|
+
Pass this to ``EventMap.bind`` when a failed event handler should be
|
|
47
|
+
replaced by another handler of the same event. Both classes are
|
|
48
|
+
``EventHandler`` of that event. This does not replace a request handler;
|
|
49
|
+
that is ``RequestHandlerFallback`` on ``RequestMap``.
|
|
44
50
|
"""
|
|
45
51
|
|
|
46
52
|
primary: EventHandlerT
|