python-cqrs 4.14.0__tar.gz → 4.15.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/PKG-INFO +15 -7
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/README.md +11 -6
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/pyproject.toml +3 -1
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/__init__.py +13 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/adapters/kafka.py +27 -6
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/adapters/protocol.py +7 -0
- python_cqrs-4.15.0/src/cqrs/deserializers/__init__.py +11 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/deserializers/exceptions.py +16 -0
- python_cqrs-4.15.0/src/cqrs/deserializers/protobuf.py +51 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/events/event_emitter.py +16 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/message_brokers/amqp.py +10 -2
- python_cqrs-4.15.0/src/cqrs/message_brokers/kafka.py +45 -0
- python_cqrs-4.15.0/src/cqrs/message_brokers/protocol.py +74 -0
- python_cqrs-4.15.0/src/cqrs/outbox/map.py +103 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/outbox/mock.py +18 -2
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/outbox/repository.py +4 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/outbox/sqlalchemy.py +39 -11
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/producer.py +8 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/requests/bootstrap.py +22 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/saga/bootstrap.py +8 -0
- python_cqrs-4.15.0/src/cqrs/serializers/__init__.py +12 -0
- python_cqrs-4.15.0/src/cqrs/serializers/default.py +86 -0
- python_cqrs-4.15.0/src/cqrs/serializers/json.py +22 -0
- python_cqrs-4.15.0/src/cqrs/serializers/protobuf.py +48 -0
- python_cqrs-4.15.0/src/cqrs/serializers/protocol.py +21 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/python_cqrs.egg-info/PKG-INFO +15 -7
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/python_cqrs.egg-info/SOURCES.txt +4 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/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.0}/LICENSE +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/setup.cfg +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/adapters/__init__.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/adapters/amqp.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/adapters/circuit_breaker.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/circuit_breaker.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/compressors/__init__.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/compressors/protocol.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/compressors/zlib.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/container/__init__.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/container/dependency_injector.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/container/di.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/container/dishka.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/container/protocol.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/container/scope.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/deserializers/json.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/dispatcher/__init__.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/dispatcher/event.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/dispatcher/exceptions.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/dispatcher/models.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/dispatcher/request.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/dispatcher/saga.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/dispatcher/streaming.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/events/__init__.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/events/bootstrap.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/events/event.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/events/event_handler.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/events/event_processor.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/events/fallback.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/events/map.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/generic_utils.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/mediator.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/message_brokers/__init__.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/message_brokers/devnull.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/middlewares/__init__.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/middlewares/base.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/middlewares/logging.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/outbox/__init__.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/requests/__init__.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/requests/cor_request_handler.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/requests/fallback.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/requests/map.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/requests/mermaid.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/requests/request.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/requests/request_handler.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/response.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/saga/__init__.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/saga/circuit_breaker.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/saga/compensation.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/saga/execution.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/saga/fallback.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/saga/mermaid.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/saga/models.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/saga/recovery.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/saga/saga.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/saga/step.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/saga/storage/__init__.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/saga/storage/enums.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/saga/storage/memory.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/saga/storage/models.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/saga/storage/protocol.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/saga/storage/sqlalchemy.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/saga/validation.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/sqlalchemy_types.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/cqrs/types.py +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/src/python_cqrs.egg-info/dependency_links.txt +0 -0
- {python_cqrs-4.14.0 → python_cqrs-4.15.0}/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.0
|
|
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
|
|
@@ -923,12 +926,9 @@ A runnable FastAPI flow (route → command → transactional outbox → publishe
|
|
|
923
926
|
> [Database Support](https://mkdocs.python-cqrs.dev/outbox/databases/) docs for the DDL, the Alembic recipe and
|
|
924
927
|
> how to register a native type for your own database.
|
|
925
928
|
> [!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:
|
|
929
|
+
> For Protobuf events, implement `proto()` / `from_proto()` and register a
|
|
930
|
+
> `ProtobufEventSerializer` on an `OutboxedEventMap`. JSON stays the default —
|
|
931
|
+
> see [Protobuf messaging](#protobuf-messaging) and the full guide:
|
|
932
932
|
> [Protobuf Integration](https://mkdocs.python-cqrs.dev/protobuf/).
|
|
933
933
|
|
|
934
934
|
## Producing Events from Outbox to Kafka
|
|
@@ -1231,6 +1231,14 @@ class UserJoinedNotificationEvent(cqrs.NotificationEvent[UserJoinedPayload]):
|
|
|
1231
1231
|
)
|
|
1232
1232
|
```
|
|
1233
1233
|
|
|
1234
|
+
JSON remains the default. Opt-in Protobuf wiring (`ProtobufEventSerializer` on an
|
|
1235
|
+
`OutboxedEventMap`, `Message.payload_bytes`, consume via `ProtobufDeserializer`) is
|
|
1236
|
+
documented in the full guide:
|
|
1237
|
+
[Protobuf Integration](https://mkdocs.python-cqrs.dev/protobuf/).
|
|
1238
|
+
Runnable example:
|
|
1239
|
+
[protobuf_outbox.py](https://github.com/vadikko2/python-cqrs/blob/master/examples/outbox/protobuf_outbox.py)
|
|
1240
|
+
(`pip install "python-cqrs[protobuf]"`).
|
|
1241
|
+
|
|
1234
1242
|
## Contributing
|
|
1235
1243
|
|
|
1236
1244
|
Contributions are welcome. To develop locally:
|
|
@@ -852,12 +852,9 @@ A runnable FastAPI flow (route → command → transactional outbox → publishe
|
|
|
852
852
|
> [Database Support](https://mkdocs.python-cqrs.dev/outbox/databases/) docs for the DDL, the Alembic recipe and
|
|
853
853
|
> how to register a native type for your own database.
|
|
854
854
|
> [!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:
|
|
855
|
+
> For Protobuf events, implement `proto()` / `from_proto()` and register a
|
|
856
|
+
> `ProtobufEventSerializer` on an `OutboxedEventMap`. JSON stays the default —
|
|
857
|
+
> see [Protobuf messaging](#protobuf-messaging) and the full guide:
|
|
861
858
|
> [Protobuf Integration](https://mkdocs.python-cqrs.dev/protobuf/).
|
|
862
859
|
|
|
863
860
|
## Producing Events from Outbox to Kafka
|
|
@@ -1160,6 +1157,14 @@ class UserJoinedNotificationEvent(cqrs.NotificationEvent[UserJoinedPayload]):
|
|
|
1160
1157
|
)
|
|
1161
1158
|
```
|
|
1162
1159
|
|
|
1160
|
+
JSON remains the default. Opt-in Protobuf wiring (`ProtobufEventSerializer` on an
|
|
1161
|
+
`OutboxedEventMap`, `Message.payload_bytes`, consume via `ProtobufDeserializer`) is
|
|
1162
|
+
documented in the full guide:
|
|
1163
|
+
[Protobuf Integration](https://mkdocs.python-cqrs.dev/protobuf/).
|
|
1164
|
+
Runnable example:
|
|
1165
|
+
[protobuf_outbox.py](https://github.com/vadikko2/python-cqrs/blob/master/examples/outbox/protobuf_outbox.py)
|
|
1166
|
+
(`pip install "python-cqrs[protobuf]"`).
|
|
1167
|
+
|
|
1163
1168
|
## Contributing
|
|
1164
1169
|
|
|
1165
1170
|
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.0"
|
|
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"]
|
|
@@ -44,6 +44,13 @@ from cqrs.outbox.sqlalchemy import (
|
|
|
44
44
|
SqlAlchemyOutboxedEventRepository,
|
|
45
45
|
)
|
|
46
46
|
from cqrs.producer import EventProducer
|
|
47
|
+
from cqrs.deserializers import DeserializeProtobufError, ProtobufDeserializer
|
|
48
|
+
from cqrs.serializers import (
|
|
49
|
+
EventCodec,
|
|
50
|
+
EventSerializer,
|
|
51
|
+
JsonEventSerializer,
|
|
52
|
+
ProtobufEventSerializer,
|
|
53
|
+
)
|
|
47
54
|
from cqrs.requests.fallback import RequestHandlerFallback
|
|
48
55
|
from cqrs.requests.map import RequestMap, SagaMap
|
|
49
56
|
from cqrs.requests.mermaid import CoRMermaid
|
|
@@ -111,6 +118,12 @@ __all__ = (
|
|
|
111
118
|
"OutboxedEventRepository",
|
|
112
119
|
"SqlAlchemyOutboxedEventRepository",
|
|
113
120
|
"EventProducer",
|
|
121
|
+
"EventSerializer",
|
|
122
|
+
"EventCodec",
|
|
123
|
+
"JsonEventSerializer",
|
|
124
|
+
"ProtobufEventSerializer",
|
|
125
|
+
"ProtobufDeserializer",
|
|
126
|
+
"DeserializeProtobufError",
|
|
114
127
|
"Container",
|
|
115
128
|
"SupportsScope",
|
|
116
129
|
"DIContainer",
|
|
@@ -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
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
from cqrs.events.event import INotificationEvent
|
|
5
|
+
from cqrs.serializers.json import JsonEventSerializer
|
|
6
|
+
from cqrs.serializers.protocol import EventCodec, EventSerializer
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class _bind_self_or_singleton:
|
|
10
|
+
"""
|
|
11
|
+
Descriptor for dual class/instance API.
|
|
12
|
+
|
|
13
|
+
Class access binds to the process-global singleton; instance access binds to
|
|
14
|
+
that map instance.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, func: typing.Callable) -> None:
|
|
18
|
+
self.func = func
|
|
19
|
+
|
|
20
|
+
def __get__(
|
|
21
|
+
self,
|
|
22
|
+
obj: typing.Any,
|
|
23
|
+
objtype: type | None = None,
|
|
24
|
+
) -> typing.Callable:
|
|
25
|
+
target = obj if obj is not None else objtype._singleton() # type: ignore[union-attr]
|
|
26
|
+
return functools.partial(self.func, target)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class OutboxedEventMap:
|
|
30
|
+
"""
|
|
31
|
+
Registry of outbox event names to types and optional codecs.
|
|
32
|
+
|
|
33
|
+
Class-level ``register`` / ``get`` use a process-global singleton (existing
|
|
34
|
+
behaviour). ``OutboxedEventMap()`` creates an isolated registry.
|
|
35
|
+
|
|
36
|
+
Warning: class-level methods mutate the process singleton; prefer an
|
|
37
|
+
instance map when tests or apps need isolation.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
_instance: typing.ClassVar["OutboxedEventMap | None"] = None
|
|
41
|
+
|
|
42
|
+
def __init__(self) -> None:
|
|
43
|
+
self._entries: typing.Dict[
|
|
44
|
+
typing.Text,
|
|
45
|
+
typing.Tuple[typing.Type[INotificationEvent], EventCodec | None],
|
|
46
|
+
] = {}
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def _singleton(cls) -> "OutboxedEventMap":
|
|
50
|
+
if cls._instance is None:
|
|
51
|
+
cls._instance = cls()
|
|
52
|
+
return cls._instance
|
|
53
|
+
|
|
54
|
+
@_bind_self_or_singleton
|
|
55
|
+
def register(
|
|
56
|
+
self,
|
|
57
|
+
event_name: typing.Text,
|
|
58
|
+
event_type: typing.Type[INotificationEvent],
|
|
59
|
+
serializer: EventCodec | None = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
if event_name in self._entries:
|
|
62
|
+
raise KeyError(f"Event with {event_name} already registered")
|
|
63
|
+
self._entries[event_name] = (event_type, serializer)
|
|
64
|
+
|
|
65
|
+
@_bind_self_or_singleton
|
|
66
|
+
def get(
|
|
67
|
+
self,
|
|
68
|
+
event_name: typing.Text,
|
|
69
|
+
) -> typing.Type[INotificationEvent] | None:
|
|
70
|
+
entry = self._entries.get(event_name)
|
|
71
|
+
if entry is None:
|
|
72
|
+
return None
|
|
73
|
+
return entry[0]
|
|
74
|
+
|
|
75
|
+
@_bind_self_or_singleton
|
|
76
|
+
def get_serializer(
|
|
77
|
+
self,
|
|
78
|
+
event_name: typing.Text,
|
|
79
|
+
) -> EventCodec | None:
|
|
80
|
+
entry = self._entries.get(event_name)
|
|
81
|
+
if entry is None:
|
|
82
|
+
return None
|
|
83
|
+
return entry[1]
|
|
84
|
+
|
|
85
|
+
@_bind_self_or_singleton
|
|
86
|
+
def as_serializer(
|
|
87
|
+
self,
|
|
88
|
+
default: EventSerializer | None = None,
|
|
89
|
+
) -> EventSerializer:
|
|
90
|
+
fallback = default or JsonEventSerializer()
|
|
91
|
+
event_map = self
|
|
92
|
+
|
|
93
|
+
class _Dispatcher:
|
|
94
|
+
def resolve_codec(self, event: INotificationEvent) -> EventSerializer:
|
|
95
|
+
return event_map.get_serializer(event.event_name) or fallback
|
|
96
|
+
|
|
97
|
+
def serialize(self, event: INotificationEvent) -> bytes:
|
|
98
|
+
return self.resolve_codec(event).serialize(event)
|
|
99
|
+
|
|
100
|
+
def content_type_for(self, event: INotificationEvent) -> str | None:
|
|
101
|
+
return self.resolve_codec(event).content_type_for(event)
|
|
102
|
+
|
|
103
|
+
return _Dispatcher()
|
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
import typing
|
|
2
2
|
|
|
3
3
|
import cqrs
|
|
4
|
-
from cqrs.outbox import repository
|
|
4
|
+
from cqrs.outbox import map, repository
|
|
5
|
+
from cqrs.serializers.json import JsonEventSerializer
|
|
6
|
+
from cqrs.serializers.protocol import EventCodec
|
|
5
7
|
|
|
6
8
|
|
|
7
9
|
class MockOutboxedEventRepository(repository.OutboxedEventRepository):
|
|
8
10
|
COUNTER: typing.ClassVar = 0
|
|
9
11
|
|
|
10
|
-
def __init__(
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
session_factory: typing.Callable[[], typing.Dict],
|
|
15
|
+
*,
|
|
16
|
+
serializer: EventCodec | None = None,
|
|
17
|
+
event_map: map.OutboxedEventMap | None = None,
|
|
18
|
+
):
|
|
11
19
|
self.session = session_factory()
|
|
20
|
+
self._serializer = serializer or JsonEventSerializer()
|
|
21
|
+
self._event_map = event_map if event_map is not None else map.OutboxedEventMap
|
|
12
22
|
|
|
13
23
|
async def __aenter__(self) -> typing.Dict:
|
|
14
24
|
return self.session
|
|
@@ -16,13 +26,19 @@ class MockOutboxedEventRepository(repository.OutboxedEventRepository):
|
|
|
16
26
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
17
27
|
pass
|
|
18
28
|
|
|
29
|
+
def _resolve_codec(self, event_name: str) -> EventCodec:
|
|
30
|
+
return self._event_map.get_serializer(event_name) or self._serializer
|
|
31
|
+
|
|
19
32
|
def add(self, event: cqrs.INotificationEvent) -> None:
|
|
20
33
|
MockOutboxedEventRepository.COUNTER += 1
|
|
34
|
+
codec = self._resolve_codec(event.event_name)
|
|
21
35
|
self.session[MockOutboxedEventRepository.COUNTER] = repository.OutboxedEvent(
|
|
22
36
|
id=MockOutboxedEventRepository.COUNTER,
|
|
23
37
|
event=event,
|
|
24
38
|
topic=event.topic,
|
|
25
39
|
status=repository.EventStatus.NEW,
|
|
40
|
+
payload_bytes=codec.serialize(event),
|
|
41
|
+
content_type=codec.content_type_for(event),
|
|
26
42
|
)
|
|
27
43
|
|
|
28
44
|
async def get_many(
|