python-cqrs 4.14.0__tar.gz → 4.15.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.14.0 → python_cqrs-4.15.1}/PKG-INFO +17 -15
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/README.md +13 -14
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/pyproject.toml +3 -1
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/__init__.py +16 -0
- python_cqrs-4.15.1/src/cqrs/_eol.py +30 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/adapters/kafka.py +27 -6
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/adapters/protocol.py +7 -0
- python_cqrs-4.15.1/src/cqrs/deserializers/__init__.py +11 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/deserializers/exceptions.py +16 -0
- python_cqrs-4.15.1/src/cqrs/deserializers/protobuf.py +51 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/events/event_emitter.py +16 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/message_brokers/amqp.py +10 -2
- python_cqrs-4.15.1/src/cqrs/message_brokers/kafka.py +45 -0
- python_cqrs-4.15.1/src/cqrs/message_brokers/protocol.py +74 -0
- python_cqrs-4.15.1/src/cqrs/outbox/map.py +103 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/outbox/mock.py +18 -2
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/outbox/repository.py +4 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/outbox/sqlalchemy.py +39 -11
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/producer.py +8 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/requests/bootstrap.py +22 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/saga/bootstrap.py +8 -0
- python_cqrs-4.15.1/src/cqrs/serializers/__init__.py +12 -0
- python_cqrs-4.15.1/src/cqrs/serializers/default.py +86 -0
- python_cqrs-4.15.1/src/cqrs/serializers/json.py +22 -0
- python_cqrs-4.15.1/src/cqrs/serializers/protobuf.py +48 -0
- python_cqrs-4.15.1/src/cqrs/serializers/protocol.py +21 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/python_cqrs.egg-info/PKG-INFO +17 -15
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/python_cqrs.egg-info/SOURCES.txt +5 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/python_cqrs.egg-info/requires.txt +4 -0
- python_cqrs-4.14.0/src/cqrs/deserializers/__init__.py +0 -8
- python_cqrs-4.14.0/src/cqrs/message_brokers/kafka.py +0 -18
- python_cqrs-4.14.0/src/cqrs/message_brokers/protocol.py +0 -44
- python_cqrs-4.14.0/src/cqrs/outbox/map.py +0 -24
- python_cqrs-4.14.0/src/cqrs/serializers/__init__.py +0 -3
- python_cqrs-4.14.0/src/cqrs/serializers/default.py +0 -24
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/LICENSE +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/setup.cfg +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/adapters/__init__.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/adapters/amqp.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/adapters/circuit_breaker.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/circuit_breaker.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/compressors/__init__.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/compressors/protocol.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/compressors/zlib.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/container/__init__.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/container/dependency_injector.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/container/di.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/container/dishka.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/container/protocol.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/container/scope.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/deserializers/json.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/dispatcher/__init__.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/dispatcher/event.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/dispatcher/exceptions.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/dispatcher/models.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/dispatcher/request.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/dispatcher/saga.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/dispatcher/streaming.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/events/__init__.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/events/bootstrap.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/events/event.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/events/event_handler.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/events/event_processor.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/events/fallback.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/events/map.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/generic_utils.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/mediator.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/message_brokers/__init__.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/message_brokers/devnull.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/middlewares/__init__.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/middlewares/base.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/middlewares/logging.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/outbox/__init__.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/requests/__init__.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/requests/cor_request_handler.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/requests/fallback.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/requests/map.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/requests/mermaid.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/requests/request.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/requests/request_handler.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/response.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/saga/__init__.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/saga/circuit_breaker.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/saga/compensation.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/saga/execution.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/saga/fallback.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/saga/mermaid.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/saga/models.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/saga/recovery.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/saga/saga.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/saga/step.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/saga/storage/__init__.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/saga/storage/enums.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/saga/storage/memory.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/saga/storage/models.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/saga/storage/protocol.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/saga/storage/sqlalchemy.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/saga/validation.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/sqlalchemy_types.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/cqrs/types.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.1}/src/python_cqrs.egg-info/dependency_links.txt +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.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.15.1
|
|
4
4
|
Summary: Event-Driven Architecture Framework for Distributed Systems
|
|
5
5
|
Author-email: Vadim Kozyrevskiy <vadikko2@mail.ru>, Dmitry Kutlubaev <kutlubaev00@mail.ru>
|
|
6
6
|
Maintainer-email: Vadim Kozyrevskiy <vadikko2@mail.ru>
|
|
@@ -49,6 +49,7 @@ Requires-Dist: asyncpg>=0.29.0; extra == "dev"
|
|
|
49
49
|
Requires-Dist: redis>=5.0.0; extra == "dev"
|
|
50
50
|
Requires-Dist: aiobreaker>=0.3.0; extra == "dev"
|
|
51
51
|
Requires-Dist: dishka>=1.0; extra == "dev"
|
|
52
|
+
Requires-Dist: protobuf>=4.25.8; extra == "dev"
|
|
52
53
|
Provides-Extra: examples
|
|
53
54
|
Requires-Dist: fastapi==0.109.*; extra == "examples"
|
|
54
55
|
Requires-Dist: faststream[kafka]==0.5.28; extra == "examples"
|
|
@@ -61,6 +62,8 @@ Requires-Dist: dishka>=1.0; extra == "examples"
|
|
|
61
62
|
Provides-Extra: kafka
|
|
62
63
|
Requires-Dist: aiokafka==0.10.0; extra == "kafka"
|
|
63
64
|
Requires-Dist: retry-async==0.1.*; extra == "kafka"
|
|
65
|
+
Provides-Extra: protobuf
|
|
66
|
+
Requires-Dist: protobuf>=4.25.8; extra == "protobuf"
|
|
64
67
|
Provides-Extra: rabbit
|
|
65
68
|
Requires-Dist: aio-pika==9.3.0; extra == "rabbit"
|
|
66
69
|
Provides-Extra: sqlalchemy
|
|
@@ -91,9 +94,6 @@ Dynamic: license-file
|
|
|
91
94
|
<a href="https://codecov.io/gh/vadikko2/python-cqrs">
|
|
92
95
|
<img src="https://img.shields.io/codecov/c/github/vadikko2/python-cqrs?logo=codecov&logoColor=white" alt="Coverage">
|
|
93
96
|
</a>
|
|
94
|
-
<a href="https://codspeed.io/vadikko2/python-cqrs?utm_source=badge">
|
|
95
|
-
<img src="https://img.shields.io/endpoint?url=https://codspeed.io/badge.json" alt="CodSpeed">
|
|
96
|
-
</a>
|
|
97
97
|
<a href="https://mkdocs.python-cqrs.dev/">
|
|
98
98
|
<img src="https://img.shields.io/badge/docs-mkdocs-blue?logo=readthedocs" alt="Documentation">
|
|
99
99
|
</a>
|
|
@@ -106,11 +106,6 @@ Dynamic: license-file
|
|
|
106
106
|
</p>
|
|
107
107
|
</div>
|
|
108
108
|
|
|
109
|
-
> [!WARNING]
|
|
110
|
-
> **Breaking Changes in v5.0.0**
|
|
111
|
-
>
|
|
112
|
-
> Starting with version 5.0.0, Pydantic support will become optional. The default implementations of `Request`, `Response`, `DomainEvent`, and `NotificationEvent` will be migrated to dataclasses-based implementations.
|
|
113
|
-
|
|
114
109
|
## Table of Contents
|
|
115
110
|
|
|
116
111
|
- [Overview](#overview)
|
|
@@ -189,6 +184,8 @@ pip install python-cqrs[examples] # FastAPI, FastStream, uvicorn, etc.
|
|
|
189
184
|
pip install python-cqrs[aiobreaker] # Circuit breaker for saga fallbacks
|
|
190
185
|
```
|
|
191
186
|
|
|
187
|
+
Starting with **4.15.1**, importing `cqrs` emits a package-level EOL `DeprecationWarning` toward 5.x (plus an optional log). Suppress the log with `CQRS_SUPPRESS_V4_EOL_WARNING=1`. See [SECURITY.md](SECURITY.md) for the 4.x support window and pin guidance.
|
|
188
|
+
|
|
192
189
|
## Quick Start
|
|
193
190
|
|
|
194
191
|
Define a command, a handler, bind them, and run via the mediator:
|
|
@@ -923,12 +920,9 @@ A runnable FastAPI flow (route → command → transactional outbox → publishe
|
|
|
923
920
|
> [Database Support](https://mkdocs.python-cqrs.dev/outbox/databases/) docs for the DDL, the Alembic recipe and
|
|
924
921
|
> how to register a native type for your own database.
|
|
925
922
|
> [!TIP]
|
|
926
|
-
> For Protobuf events, implement `proto()` / `from_proto()`
|
|
927
|
-
>
|
|
928
|
-
> [
|
|
929
|
-
> Outbox persistence is unchanged — use
|
|
930
|
-
> [save_events_into_outbox.py](https://github.com/vadikko2/python-cqrs/blob/master/examples/outbox/save_events_into_outbox.py);
|
|
931
|
-
> Protobuf applies when producing to the broker. Full guide:
|
|
923
|
+
> For Protobuf events, implement `proto()` / `from_proto()` and register a
|
|
924
|
+
> `ProtobufEventSerializer` on an `OutboxedEventMap`. JSON stays the default —
|
|
925
|
+
> see [Protobuf messaging](#protobuf-messaging) and the full guide:
|
|
932
926
|
> [Protobuf Integration](https://mkdocs.python-cqrs.dev/protobuf/).
|
|
933
927
|
|
|
934
928
|
## Producing Events from Outbox to Kafka
|
|
@@ -1231,6 +1225,14 @@ class UserJoinedNotificationEvent(cqrs.NotificationEvent[UserJoinedPayload]):
|
|
|
1231
1225
|
)
|
|
1232
1226
|
```
|
|
1233
1227
|
|
|
1228
|
+
JSON remains the default. Opt-in Protobuf wiring (`ProtobufEventSerializer` on an
|
|
1229
|
+
`OutboxedEventMap`, `Message.payload_bytes`, consume via `ProtobufDeserializer`) is
|
|
1230
|
+
documented in the full guide:
|
|
1231
|
+
[Protobuf Integration](https://mkdocs.python-cqrs.dev/protobuf/).
|
|
1232
|
+
Runnable example:
|
|
1233
|
+
[protobuf_outbox.py](https://github.com/vadikko2/python-cqrs/blob/master/examples/outbox/protobuf_outbox.py)
|
|
1234
|
+
(`pip install "python-cqrs[protobuf]"`).
|
|
1235
|
+
|
|
1234
1236
|
## Contributing
|
|
1235
1237
|
|
|
1236
1238
|
Contributions are welcome. To develop locally:
|
|
@@ -20,9 +20,6 @@
|
|
|
20
20
|
<a href="https://codecov.io/gh/vadikko2/python-cqrs">
|
|
21
21
|
<img src="https://img.shields.io/codecov/c/github/vadikko2/python-cqrs?logo=codecov&logoColor=white" alt="Coverage">
|
|
22
22
|
</a>
|
|
23
|
-
<a href="https://codspeed.io/vadikko2/python-cqrs?utm_source=badge">
|
|
24
|
-
<img src="https://img.shields.io/endpoint?url=https://codspeed.io/badge.json" alt="CodSpeed">
|
|
25
|
-
</a>
|
|
26
23
|
<a href="https://mkdocs.python-cqrs.dev/">
|
|
27
24
|
<img src="https://img.shields.io/badge/docs-mkdocs-blue?logo=readthedocs" alt="Documentation">
|
|
28
25
|
</a>
|
|
@@ -35,11 +32,6 @@
|
|
|
35
32
|
</p>
|
|
36
33
|
</div>
|
|
37
34
|
|
|
38
|
-
> [!WARNING]
|
|
39
|
-
> **Breaking Changes in v5.0.0**
|
|
40
|
-
>
|
|
41
|
-
> Starting with version 5.0.0, Pydantic support will become optional. The default implementations of `Request`, `Response`, `DomainEvent`, and `NotificationEvent` will be migrated to dataclasses-based implementations.
|
|
42
|
-
|
|
43
35
|
## Table of Contents
|
|
44
36
|
|
|
45
37
|
- [Overview](#overview)
|
|
@@ -118,6 +110,8 @@ pip install python-cqrs[examples] # FastAPI, FastStream, uvicorn, etc.
|
|
|
118
110
|
pip install python-cqrs[aiobreaker] # Circuit breaker for saga fallbacks
|
|
119
111
|
```
|
|
120
112
|
|
|
113
|
+
Starting with **4.15.1**, importing `cqrs` emits a package-level EOL `DeprecationWarning` toward 5.x (plus an optional log). Suppress the log with `CQRS_SUPPRESS_V4_EOL_WARNING=1`. See [SECURITY.md](SECURITY.md) for the 4.x support window and pin guidance.
|
|
114
|
+
|
|
121
115
|
## Quick Start
|
|
122
116
|
|
|
123
117
|
Define a command, a handler, bind them, and run via the mediator:
|
|
@@ -852,12 +846,9 @@ A runnable FastAPI flow (route → command → transactional outbox → publishe
|
|
|
852
846
|
> [Database Support](https://mkdocs.python-cqrs.dev/outbox/databases/) docs for the DDL, the Alembic recipe and
|
|
853
847
|
> how to register a native type for your own database.
|
|
854
848
|
> [!TIP]
|
|
855
|
-
> For Protobuf events, implement `proto()` / `from_proto()`
|
|
856
|
-
>
|
|
857
|
-
> [
|
|
858
|
-
> Outbox persistence is unchanged — use
|
|
859
|
-
> [save_events_into_outbox.py](https://github.com/vadikko2/python-cqrs/blob/master/examples/outbox/save_events_into_outbox.py);
|
|
860
|
-
> Protobuf applies when producing to the broker. Full guide:
|
|
849
|
+
> For Protobuf events, implement `proto()` / `from_proto()` and register a
|
|
850
|
+
> `ProtobufEventSerializer` on an `OutboxedEventMap`. JSON stays the default —
|
|
851
|
+
> see [Protobuf messaging](#protobuf-messaging) and the full guide:
|
|
861
852
|
> [Protobuf Integration](https://mkdocs.python-cqrs.dev/protobuf/).
|
|
862
853
|
|
|
863
854
|
## Producing Events from Outbox to Kafka
|
|
@@ -1160,6 +1151,14 @@ class UserJoinedNotificationEvent(cqrs.NotificationEvent[UserJoinedPayload]):
|
|
|
1160
1151
|
)
|
|
1161
1152
|
```
|
|
1162
1153
|
|
|
1154
|
+
JSON remains the default. Opt-in Protobuf wiring (`ProtobufEventSerializer` on an
|
|
1155
|
+
`OutboxedEventMap`, `Message.payload_bytes`, consume via `ProtobufDeserializer`) is
|
|
1156
|
+
documented in the full guide:
|
|
1157
|
+
[Protobuf Integration](https://mkdocs.python-cqrs.dev/protobuf/).
|
|
1158
|
+
Runnable example:
|
|
1159
|
+
[protobuf_outbox.py](https://github.com/vadikko2/python-cqrs/blob/master/examples/outbox/protobuf_outbox.py)
|
|
1160
|
+
(`pip install "python-cqrs[protobuf]"`).
|
|
1161
|
+
|
|
1163
1162
|
## Contributing
|
|
1164
1163
|
|
|
1165
1164
|
Contributions are welcome. To develop locally:
|
|
@@ -31,7 +31,7 @@ maintainers = [{ name = "Vadim Kozyrevskiy", email = "vadikko2@mail.ru" }]
|
|
|
31
31
|
name = "python-cqrs"
|
|
32
32
|
readme = "README.md"
|
|
33
33
|
requires-python = ">=3.10"
|
|
34
|
-
version = "4.
|
|
34
|
+
version = "4.15.1"
|
|
35
35
|
|
|
36
36
|
[project.optional-dependencies]
|
|
37
37
|
aiobreaker = ["aiobreaker>=0.3.0"]
|
|
@@ -60,6 +60,7 @@ dev = [
|
|
|
60
60
|
# Circuit breaker for tests
|
|
61
61
|
"aiobreaker>=0.3.0", # from aiobreaker
|
|
62
62
|
"dishka>=1.0", # scoped-deps dishka adapter tests / examples
|
|
63
|
+
"protobuf>=4.25.8", # protobuf codec / deserializer unit tests
|
|
63
64
|
]
|
|
64
65
|
examples = [
|
|
65
66
|
"fastapi==0.109.*",
|
|
@@ -72,6 +73,7 @@ examples = [
|
|
|
72
73
|
"dishka>=1.0",
|
|
73
74
|
]
|
|
74
75
|
kafka = ["aiokafka==0.10.0", "retry-async==0.1.*"]
|
|
76
|
+
protobuf = ["protobuf>=4.25.8"]
|
|
75
77
|
rabbit = ["aio-pika==9.3.0"]
|
|
76
78
|
sqlalchemy = ["sqlalchemy[asyncio]==2.0.*"]
|
|
77
79
|
dishka = ["dishka>=1.0"]
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from cqrs._eol import emit_v4_eol_warning
|
|
1
2
|
from cqrs.compressors import Compressor, ZlibCompressor
|
|
2
3
|
from cqrs.container.di import DIContainer
|
|
3
4
|
from cqrs.container.protocol import Container, SupportsScope
|
|
@@ -44,6 +45,13 @@ from cqrs.outbox.sqlalchemy import (
|
|
|
44
45
|
SqlAlchemyOutboxedEventRepository,
|
|
45
46
|
)
|
|
46
47
|
from cqrs.producer import EventProducer
|
|
48
|
+
from cqrs.deserializers import DeserializeProtobufError, ProtobufDeserializer
|
|
49
|
+
from cqrs.serializers import (
|
|
50
|
+
EventCodec,
|
|
51
|
+
EventSerializer,
|
|
52
|
+
JsonEventSerializer,
|
|
53
|
+
ProtobufEventSerializer,
|
|
54
|
+
)
|
|
47
55
|
from cqrs.requests.fallback import RequestHandlerFallback
|
|
48
56
|
from cqrs.requests.map import RequestMap, SagaMap
|
|
49
57
|
from cqrs.requests.mermaid import CoRMermaid
|
|
@@ -111,6 +119,12 @@ __all__ = (
|
|
|
111
119
|
"OutboxedEventRepository",
|
|
112
120
|
"SqlAlchemyOutboxedEventRepository",
|
|
113
121
|
"EventProducer",
|
|
122
|
+
"EventSerializer",
|
|
123
|
+
"EventCodec",
|
|
124
|
+
"JsonEventSerializer",
|
|
125
|
+
"ProtobufEventSerializer",
|
|
126
|
+
"ProtobufDeserializer",
|
|
127
|
+
"DeserializeProtobufError",
|
|
114
128
|
"Container",
|
|
115
129
|
"SupportsScope",
|
|
116
130
|
"DIContainer",
|
|
@@ -136,3 +150,5 @@ __all__ = (
|
|
|
136
150
|
"JSONType",
|
|
137
151
|
"PayloadBinary",
|
|
138
152
|
)
|
|
153
|
+
|
|
154
|
+
emit_v4_eol_warning()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Package-level EOL notice for the 4.x maintenance line."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import warnings
|
|
8
|
+
|
|
9
|
+
_EOL_MESSAGE = (
|
|
10
|
+
"python-cqrs 4.x is in maintenance mode after 5.0.0. "
|
|
11
|
+
"Please migrate to 5.x when ready "
|
|
12
|
+
"(docs: https://mkdocs.python-cqrs.dev/latest/). "
|
|
13
|
+
'To stay on 4.x: pip install "python-cqrs>=4,<5". '
|
|
14
|
+
"Suppress this optional log with CQRS_SUPPRESS_V4_EOL_WARNING=1 "
|
|
15
|
+
"(DeprecationWarning is still emitted)."
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
_TRUTHY = frozenset({"1", "true", "yes"})
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _suppress_log() -> bool:
|
|
22
|
+
value = os.environ.get("CQRS_SUPPRESS_V4_EOL_WARNING", "")
|
|
23
|
+
return value.strip().lower() in _TRUTHY
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def emit_v4_eol_warning() -> None:
|
|
27
|
+
"""Emit DeprecationWarning always; optional logger warning unless suppressed."""
|
|
28
|
+
warnings.warn(_EOL_MESSAGE, DeprecationWarning, stacklevel=2)
|
|
29
|
+
if not _suppress_log():
|
|
30
|
+
logging.getLogger("cqrs").warning(_EOL_MESSAGE)
|
|
@@ -5,7 +5,7 @@ import ssl
|
|
|
5
5
|
import typing
|
|
6
6
|
|
|
7
7
|
from cqrs.adapters import protocol
|
|
8
|
-
from cqrs.serializers import
|
|
8
|
+
from cqrs.serializers.default import passthrough_value_serializer
|
|
9
9
|
|
|
10
10
|
import aiokafka
|
|
11
11
|
import retry_async
|
|
@@ -53,22 +53,41 @@ class KafkaProducer(protocol.KafkaProducer):
|
|
|
53
53
|
producer: aiokafka.AIOKafkaProducer,
|
|
54
54
|
retry_count: int = 3,
|
|
55
55
|
retry_delay: int = 1,
|
|
56
|
+
legacy_value_serializer: bool = False,
|
|
56
57
|
):
|
|
57
58
|
self._producer = producer
|
|
58
59
|
self._retry_count = retry_count
|
|
59
60
|
self._retry_delay = retry_delay
|
|
61
|
+
# True when kafka_producer_factory got an explicit value_serializer so
|
|
62
|
+
# KafkaMessageBroker can pass message.payload (dict) on the non-codec path.
|
|
63
|
+
self.legacy_value_serializer = legacy_value_serializer
|
|
60
64
|
|
|
61
65
|
async def _check_connection(self):
|
|
62
66
|
node_id = self._producer.client.get_random_node()
|
|
63
67
|
if not await self._producer.client.ready(node_id=node_id):
|
|
64
68
|
await self._producer.start()
|
|
65
69
|
|
|
66
|
-
async def _produce(
|
|
70
|
+
async def _produce(
|
|
71
|
+
self,
|
|
72
|
+
topic: typing.Text,
|
|
73
|
+
message: typing.Any,
|
|
74
|
+
headers: dict[str, str] | None = None,
|
|
75
|
+
):
|
|
67
76
|
await self._check_connection()
|
|
68
77
|
logger.debug(f"produce message {message} to topic {topic}")
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
produce_kwargs: dict[str, typing.Any] = {"value": message}
|
|
79
|
+
if headers is not None:
|
|
80
|
+
produce_kwargs["headers"] = [
|
|
81
|
+
(key, value.encode("utf-8") if isinstance(value, str) else value) for key, value in headers.items()
|
|
82
|
+
]
|
|
83
|
+
await self._producer.send_and_wait(topic, **produce_kwargs)
|
|
84
|
+
|
|
85
|
+
async def produce(
|
|
86
|
+
self,
|
|
87
|
+
topic: typing.Text,
|
|
88
|
+
message: typing.Any,
|
|
89
|
+
headers: dict[str, str] | None = None,
|
|
90
|
+
):
|
|
72
91
|
"""
|
|
73
92
|
Produces event to kafka broker.
|
|
74
93
|
Tries to reconnect if connect has been lost or has not been opened.
|
|
@@ -76,6 +95,7 @@ class KafkaProducer(protocol.KafkaProducer):
|
|
|
76
95
|
await _retry(tries=self._retry_count, delay=self._retry_delay)(self._produce)(
|
|
77
96
|
topic,
|
|
78
97
|
message,
|
|
98
|
+
headers,
|
|
79
99
|
)
|
|
80
100
|
|
|
81
101
|
|
|
@@ -95,7 +115,7 @@ def kafka_producer_factory(
|
|
|
95
115
|
|
|
96
116
|
producer = aiokafka.AIOKafkaProducer(
|
|
97
117
|
bootstrap_servers=dsn,
|
|
98
|
-
value_serializer=value_serializer
|
|
118
|
+
value_serializer=passthrough_value_serializer(value_serializer),
|
|
99
119
|
security_protocol=security_protocol,
|
|
100
120
|
sasl_mechanism=sasl_mechanism,
|
|
101
121
|
sasl_plain_username=user,
|
|
@@ -107,4 +127,5 @@ def kafka_producer_factory(
|
|
|
107
127
|
producer=producer,
|
|
108
128
|
retry_count=retry_count,
|
|
109
129
|
retry_delay=retry_delay,
|
|
130
|
+
legacy_value_serializer=value_serializer is not None,
|
|
110
131
|
)
|
|
@@ -5,10 +5,17 @@ if typing.TYPE_CHECKING:
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class KafkaProducer(typing.Protocol):
|
|
8
|
+
"""
|
|
9
|
+
``legacy_value_serializer`` (optional attribute, default False) is set by
|
|
10
|
+
``kafka_producer_factory`` when an explicit ``value_serializer`` is passed.
|
|
11
|
+
``KafkaMessageBroker`` uses it to decide whether to forward ``message.payload``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
8
14
|
async def produce(
|
|
9
15
|
self,
|
|
10
16
|
topic: typing.Text,
|
|
11
17
|
message: typing.Any,
|
|
18
|
+
headers: dict[str, str] | None = None,
|
|
12
19
|
) -> None: ...
|
|
13
20
|
|
|
14
21
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from cqrs.deserializers.exceptions import DeserializeJsonError, DeserializeProtobufError
|
|
2
|
+
from cqrs.deserializers.json import Deserializable, JsonDeserializer
|
|
3
|
+
from cqrs.deserializers.protobuf import ProtobufDeserializer
|
|
4
|
+
|
|
5
|
+
__all__ = (
|
|
6
|
+
"Deserializable",
|
|
7
|
+
"JsonDeserializer",
|
|
8
|
+
"DeserializeJsonError",
|
|
9
|
+
"ProtobufDeserializer",
|
|
10
|
+
"DeserializeProtobufError",
|
|
11
|
+
)
|
|
@@ -2,6 +2,22 @@ import dataclasses
|
|
|
2
2
|
import typing
|
|
3
3
|
|
|
4
4
|
|
|
5
|
+
@dataclasses.dataclass(frozen=True)
|
|
6
|
+
class DeserializeProtobufError:
|
|
7
|
+
"""
|
|
8
|
+
Error that occurred during protobuf deserialization.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
error_message: Human-readable error message
|
|
12
|
+
error_type: Type of the exception that occurred
|
|
13
|
+
message_data: The original message data that failed to deserialize
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
error_message: str
|
|
17
|
+
error_type: typing.Type[Exception]
|
|
18
|
+
message_data: str | bytes | None
|
|
19
|
+
|
|
20
|
+
|
|
5
21
|
@dataclasses.dataclass(frozen=True)
|
|
6
22
|
class DeserializeJsonError:
|
|
7
23
|
"""
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import typing
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from cqrs.deserializers.exceptions import DeserializeProtobufError
|
|
6
|
+
|
|
7
|
+
if sys.version_info >= (3, 11):
|
|
8
|
+
from typing import Self # novm
|
|
9
|
+
else:
|
|
10
|
+
from typing_extensions import Self
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("cqrs")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ProtoDeserializable(typing.Protocol):
|
|
16
|
+
"""Objects that can be constructed from a protobuf message via ``from_proto``."""
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def from_proto(cls, proto: typing.Any) -> Self: ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_T = typing.TypeVar("_T", bound=ProtoDeserializable)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ProtobufDeserializer(typing.Generic[_T]):
|
|
26
|
+
"""
|
|
27
|
+
Deserializer for protobuf messages.
|
|
28
|
+
|
|
29
|
+
Converts protobuf wire bytes into Python objects using the ``from_proto``
|
|
30
|
+
classmethod of the target model. Duck-typed: this module does not import
|
|
31
|
+
``google.protobuf``.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, model: type[_T], proto_type: typing.Any) -> None:
|
|
35
|
+
self._model = model
|
|
36
|
+
self._proto_type = proto_type
|
|
37
|
+
|
|
38
|
+
def __call__(self, data: bytes | None) -> _T | None | DeserializeProtobufError:
|
|
39
|
+
if data is None:
|
|
40
|
+
return None
|
|
41
|
+
try:
|
|
42
|
+
return self._model.from_proto(self._proto_type.FromString(data))
|
|
43
|
+
except Exception as e:
|
|
44
|
+
logger.error(
|
|
45
|
+
f"Error while deserializing protobuf message: {e}",
|
|
46
|
+
)
|
|
47
|
+
return DeserializeProtobufError(
|
|
48
|
+
error_message=str(e),
|
|
49
|
+
error_type=type(e),
|
|
50
|
+
message_data=data,
|
|
51
|
+
)
|
|
@@ -15,6 +15,9 @@ from cqrs.container.scope import (
|
|
|
15
15
|
from cqrs.events.event import IDomainEvent, IEvent, INotificationEvent
|
|
16
16
|
from cqrs.events import event_handler, map
|
|
17
17
|
from cqrs.events.fallback import EventHandlerFallback
|
|
18
|
+
from cqrs.serializers.default import headers_for_content_type
|
|
19
|
+
from cqrs.serializers.json import JsonEventSerializer
|
|
20
|
+
from cqrs.serializers.protocol import EventSerializer
|
|
18
21
|
|
|
19
22
|
logger = logging.getLogger("cqrs")
|
|
20
23
|
|
|
@@ -38,6 +41,8 @@ class EventEmitter:
|
|
|
38
41
|
container: di_container.Container,
|
|
39
42
|
message_broker: message_brokers.MessageBroker | None = None,
|
|
40
43
|
scope_strategy: ScopeStrategy = ScopeStrategy.NONE,
|
|
44
|
+
*,
|
|
45
|
+
serializer: EventSerializer | None = None,
|
|
41
46
|
) -> None:
|
|
42
47
|
"""
|
|
43
48
|
Initialize the event emitter.
|
|
@@ -48,6 +53,7 @@ class EventEmitter:
|
|
|
48
53
|
message_broker: Optional broker for notification events; required
|
|
49
54
|
when emitting :class:`~cqrs.events.event.INotificationEvent`.
|
|
50
55
|
scope_strategy: DI scope boundary; HANDLER opens a scope per handler.
|
|
56
|
+
serializer: Codec used to fill ``Message.payload_bytes``. Defaults to JSON.
|
|
51
57
|
|
|
52
58
|
Example::
|
|
53
59
|
|
|
@@ -64,6 +70,7 @@ class EventEmitter:
|
|
|
64
70
|
self._container = wrap_container(container)
|
|
65
71
|
self._message_broker = message_broker
|
|
66
72
|
self._scope_strategy = scope_strategy
|
|
73
|
+
self._serializer = serializer or JsonEventSerializer()
|
|
67
74
|
|
|
68
75
|
@functools.singledispatchmethod
|
|
69
76
|
async def emit(self, event: IEvent) -> typing.Sequence[IEvent]:
|
|
@@ -108,11 +115,20 @@ class EventEmitter:
|
|
|
108
115
|
f"To send event {event}, message broker must be specified.",
|
|
109
116
|
)
|
|
110
117
|
|
|
118
|
+
# payload (dict) and payload_bytes are both intentional: stub brokers /
|
|
119
|
+
# Message.to_dict() need the structured dict; transport uses wire bytes.
|
|
120
|
+
content_type, headers = headers_for_content_type(
|
|
121
|
+
self._serializer.content_type_for(event),
|
|
122
|
+
event,
|
|
123
|
+
)
|
|
111
124
|
message = message_brokers.Message(
|
|
112
125
|
message_name=type(event).__name__,
|
|
113
126
|
message_id=event.event_id,
|
|
114
127
|
topic=event.topic,
|
|
115
128
|
payload=event.to_dict(),
|
|
129
|
+
payload_bytes=self._serializer.serialize(event),
|
|
130
|
+
content_type=content_type,
|
|
131
|
+
headers=headers,
|
|
116
132
|
)
|
|
117
133
|
|
|
118
134
|
logger.debug(
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
3
|
import aio_pika
|
|
4
|
-
import orjson
|
|
5
4
|
|
|
6
5
|
from cqrs.adapters import protocol as adapters_protocol
|
|
7
6
|
from cqrs.message_brokers import protocol
|
|
7
|
+
from cqrs.serializers.default import message_wire_bytes
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class AMQPMessageBroker(protocol.MessageBroker):
|
|
@@ -20,8 +20,16 @@ class AMQPMessageBroker(protocol.MessageBroker):
|
|
|
20
20
|
logging.getLogger("aio_pika").setLevel(pika_log_level)
|
|
21
21
|
|
|
22
22
|
async def send_message(self, message: protocol.Message) -> None:
|
|
23
|
+
kwargs: dict = {}
|
|
24
|
+
if message.content_type is not None:
|
|
25
|
+
kwargs["content_type"] = message.content_type
|
|
26
|
+
if message.headers is not None:
|
|
27
|
+
kwargs["headers"] = message.headers
|
|
23
28
|
await self.publisher.publish(
|
|
24
|
-
message=aio_pika.Message(
|
|
29
|
+
message=aio_pika.Message(
|
|
30
|
+
body=message_wire_bytes(message),
|
|
31
|
+
**kwargs,
|
|
32
|
+
),
|
|
25
33
|
queue_name=message.topic,
|
|
26
34
|
exchange_name=self.exchange_name,
|
|
27
35
|
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
from cqrs.adapters import protocol as adapters_protocol
|
|
5
|
+
from cqrs.message_brokers import protocol
|
|
6
|
+
from cqrs.serializers.default import message_wire_bytes
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class KafkaMessageBroker(protocol.MessageBroker):
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
producer: adapters_protocol.KafkaProducer,
|
|
13
|
+
aiokafka_log_level: typing.Text = "ERROR",
|
|
14
|
+
):
|
|
15
|
+
self._producer = producer
|
|
16
|
+
logging.getLogger("aiokafka").setLevel(aiokafka_log_level)
|
|
17
|
+
|
|
18
|
+
def _produce_value(self, message: protocol.Message) -> typing.Any:
|
|
19
|
+
"""
|
|
20
|
+
Choose the value passed to ``KafkaProducer.produce``.
|
|
21
|
+
|
|
22
|
+
When a legacy ``value_serializer`` was configured on the producer and the
|
|
23
|
+
message has no codec wire bytes, forward ``message.payload`` so that
|
|
24
|
+
serializer can run. Otherwise prefer ``payload_bytes`` / encoded payload
|
|
25
|
+
so Outbox and EventSerializer bytes are not discarded.
|
|
26
|
+
"""
|
|
27
|
+
legacy_value_serializer = getattr(
|
|
28
|
+
self._producer,
|
|
29
|
+
"legacy_value_serializer",
|
|
30
|
+
False,
|
|
31
|
+
)
|
|
32
|
+
if legacy_value_serializer and message.payload_bytes is None and message.content_type is None:
|
|
33
|
+
return message.payload
|
|
34
|
+
return message_wire_bytes(message)
|
|
35
|
+
|
|
36
|
+
async def send_message(self, message: protocol.Message) -> None:
|
|
37
|
+
payload = self._produce_value(message)
|
|
38
|
+
if message.headers is None:
|
|
39
|
+
await self._producer.produce(message.topic, payload)
|
|
40
|
+
else:
|
|
41
|
+
await self._producer.produce(
|
|
42
|
+
message.topic,
|
|
43
|
+
payload,
|
|
44
|
+
headers=message.headers,
|
|
45
|
+
)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import base64
|
|
3
|
+
import dataclasses
|
|
4
|
+
import typing
|
|
5
|
+
import uuid
|
|
6
|
+
from dataclass_wizard import asdict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclasses.dataclass
|
|
10
|
+
class Message:
|
|
11
|
+
"""
|
|
12
|
+
Internal message structure for message broker communication.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
message_name: Name of the message type
|
|
16
|
+
message_id: Unique identifier for the message (auto-generated if not provided)
|
|
17
|
+
topic: Message broker topic where the message should be sent
|
|
18
|
+
payload: Structured message payload (typically ``event.to_dict()``)
|
|
19
|
+
payload_bytes: Optional codec wire-bytes for the transport
|
|
20
|
+
content_type: Optional MIME type (set only by typed codecs such as Protobuf)
|
|
21
|
+
headers: Optional broker headers (set only when ``content_type`` is set)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
message_name: typing.Text
|
|
25
|
+
topic: typing.Text
|
|
26
|
+
payload: typing.Any
|
|
27
|
+
message_id: uuid.UUID = dataclasses.field(default_factory=uuid.uuid4)
|
|
28
|
+
payload_bytes: bytes | None = None
|
|
29
|
+
content_type: str | None = None
|
|
30
|
+
headers: dict[str, str] | None = None
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> dict[str, typing.Any]:
|
|
33
|
+
"""
|
|
34
|
+
Convert the message instance to a dictionary representation.
|
|
35
|
+
|
|
36
|
+
Core fields use ``dataclass_wizard.asdict`` camelCase (``messageName``,
|
|
37
|
+
``messageId``, …) for JSON-path ``GET /published`` stability. Codec
|
|
38
|
+
extras (``content_type`` / ``payload_bytes`` / ``headers``) are added
|
|
39
|
+
only when ``content_type`` is set, in snake_case.
|
|
40
|
+
"""
|
|
41
|
+
# Rebuild with only the original fields so asdict matches the historical
|
|
42
|
+
# JSON-path shape (camelCase messageName/messageId). Extra codec fields
|
|
43
|
+
# still appear as None camelCase keys from defaults — drop them.
|
|
44
|
+
data = asdict(
|
|
45
|
+
Message(
|
|
46
|
+
message_name=self.message_name,
|
|
47
|
+
topic=self.topic,
|
|
48
|
+
payload=self.payload,
|
|
49
|
+
message_id=self.message_id,
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
data.pop("payloadBytes", None)
|
|
53
|
+
data.pop("contentType", None)
|
|
54
|
+
data.pop("headers", None)
|
|
55
|
+
if self.content_type is not None:
|
|
56
|
+
data["content_type"] = self.content_type
|
|
57
|
+
if self.payload_bytes is not None:
|
|
58
|
+
data["payload_bytes"] = base64.b64encode(self.payload_bytes).decode("ascii")
|
|
59
|
+
data["payload_bytes_encoding"] = "base64"
|
|
60
|
+
if self.headers is not None:
|
|
61
|
+
data["headers"] = self.headers
|
|
62
|
+
return data
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class MessageBroker(abc.ABC):
|
|
66
|
+
"""
|
|
67
|
+
The interface over a message broker.
|
|
68
|
+
|
|
69
|
+
Used for sending messages to message brokers (currently only redis supported).
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
@abc.abstractmethod
|
|
73
|
+
async def send_message(self, message: Message) -> None:
|
|
74
|
+
raise NotImplementedError
|