python-cqrs 1.0.0__tar.gz → 2.0.2__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-1.0.0 → python_cqrs-2.0.2}/PKG-INFO +21 -4
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/README.md +17 -2
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/pyproject.toml +9 -3
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/__init__.py +7 -7
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/adapters/kafka.py +7 -7
- python_cqrs-2.0.2/src/cqrs/compressors/__init__.py +4 -0
- python_cqrs-2.0.2/src/cqrs/deserializers/protobuf.py +52 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/events/__init__.py +1 -2
- python_cqrs-2.0.2/src/cqrs/events/event.py +59 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/events/event_emitter.py +3 -9
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/message_brokers/protocol.py +1 -2
- python_cqrs-2.0.2/src/cqrs/outbox/map.py +24 -0
- python_cqrs-2.0.2/src/cqrs/outbox/mock.py +55 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/outbox/repository.py +12 -17
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/outbox/sqlalchemy.py +51 -99
- {python_cqrs-1.0.0/src/cqrs/outbox → python_cqrs-2.0.2/src/cqrs}/producer.py +25 -34
- python_cqrs-2.0.2/src/cqrs/serializers/__init__.py +0 -0
- python_cqrs-2.0.2/src/cqrs/serializers/default.py +8 -0
- python_cqrs-2.0.2/src/cqrs/serializers/protobuf.py +39 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/python_cqrs.egg-info/PKG-INFO +21 -4
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/python_cqrs.egg-info/SOURCES.txt +7 -2
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/python_cqrs.egg-info/requires.txt +4 -1
- python_cqrs-1.0.0/src/cqrs/events/event.py +0 -103
- python_cqrs-1.0.0/src/cqrs/outbox/mock.py +0 -51
- python_cqrs-1.0.0/src/cqrs/outbox/protocol.py +0 -56
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/LICENSE +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/setup.cfg +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/adapters/__init__.py +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/adapters/amqp.py +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/compressors/protocol.py +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/compressors/zlib.py +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/container/__init__.py +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/container/di.py +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/container/protocol.py +0 -0
- {python_cqrs-1.0.0/src/cqrs/compressors → python_cqrs-2.0.2/src/cqrs/deserializers}/__init__.py +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/dispatcher/__init__.py +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/dispatcher/dispatcher.py +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/events/bootstrap.py +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/events/event_handler.py +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/events/map.py +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/mediator.py +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/message_brokers/__init__.py +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/message_brokers/amqp.py +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/message_brokers/devnull.py +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/message_brokers/kafka.py +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/middlewares/__init__.py +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/middlewares/base.py +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/middlewares/logging.py +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/outbox/__init__.py +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/requests/__init__.py +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/requests/bootstrap.py +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/requests/map.py +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/requests/request.py +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/requests/request_handler.py +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/response.py +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/python_cqrs.egg-info/dependency_links.txt +0 -0
- {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/python_cqrs.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-cqrs
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: Python CQRS pattern implementation
|
|
5
5
|
Author-email: Vadim Kozyrevskiy <vadikko2@mail.ru>
|
|
6
6
|
Maintainer-email: Vadim Kozyrevskiy <vadikko2@mail.ru>
|
|
@@ -33,12 +33,14 @@ Requires-Dist: pytest-env==0.6.2; extra == "dev"
|
|
|
33
33
|
Requires-Dist: cryptography==42.0.2; extra == "dev"
|
|
34
34
|
Requires-Dist: asyncmy==0.2.9; extra == "dev"
|
|
35
35
|
Provides-Extra: examples
|
|
36
|
-
Requires-Dist: aiokafka==0.10.0; extra == "examples"
|
|
37
36
|
Requires-Dist: fastapi==0.109.*; extra == "examples"
|
|
38
37
|
Requires-Dist: uvicorn==0.32.0; extra == "examples"
|
|
39
38
|
Requires-Dist: faststream[kafka]==0.5.28; extra == "examples"
|
|
40
39
|
Provides-Extra: kafka
|
|
41
40
|
Requires-Dist: aiokafka==0.10.0; extra == "kafka"
|
|
41
|
+
Requires-Dist: confluent-kafka==2.6.0; extra == "kafka"
|
|
42
|
+
Provides-Extra: protobuf
|
|
43
|
+
Requires-Dist: protobuf==4.25.5; extra == "protobuf"
|
|
42
44
|
|
|
43
45
|
# Python CQRS pattern implementation with Transaction Outbox supporting
|
|
44
46
|
|
|
@@ -59,7 +61,8 @@ project ([documentation](https://akhundmurad.github.io/diator/)) with several en
|
|
|
59
61
|
6. Added support for [Transaction Outbox](https://microservices.io/patterns/data/transactional-outbox.html), ensuring
|
|
60
62
|
that `Notification` and `ECST` events are sent to the broker;
|
|
61
63
|
7. FastAPI supporting;
|
|
62
|
-
8. FastStream supporting
|
|
64
|
+
8. FastStream supporting;
|
|
65
|
+
9. [Protobuf](https://protobuf.dev/) events supporting.
|
|
63
66
|
|
|
64
67
|
## Request Handlers
|
|
65
68
|
|
|
@@ -295,6 +298,11 @@ the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/save_e
|
|
|
295
298
|
> You can specify the name of the Outbox table using the environment variable `OUTBOX_SQLA_TABLE`.
|
|
296
299
|
> By default, it is set to `outbox`.
|
|
297
300
|
|
|
301
|
+
> [!TIP]
|
|
302
|
+
> If you use the protobuf events you should specify `OutboxedEventRepository`
|
|
303
|
+
> by [protobuf serialize](https://github.com/vadikko2/cqrs/blob/master/src/cqrs/serializers/protobuf.py). A complete example can be found in
|
|
304
|
+
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/save_proto_events_into_outbox.py)
|
|
305
|
+
|
|
298
306
|
## Producing Events from Outbox to Kafka
|
|
299
307
|
|
|
300
308
|
As an implementation of the Transactional Outbox pattern, the SqlAlchemyOutboxedEventRepository is available for use as
|
|
@@ -507,4 +515,13 @@ async def hello_world_event_handler(
|
|
|
507
515
|
await msg.ack()
|
|
508
516
|
```
|
|
509
517
|
|
|
510
|
-
A complete example can be found in
|
|
518
|
+
A complete example can be found in
|
|
519
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/kafka_event_consuming.py)
|
|
520
|
+
|
|
521
|
+
## Protobuf messaging
|
|
522
|
+
|
|
523
|
+
The `python-cqrs` package supports integration with [protobuf](https://developers.google.com/protocol-buffers/).\
|
|
524
|
+
Protocol buffers are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data –
|
|
525
|
+
think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use
|
|
526
|
+
special generated source code to easily write and read your structured data to and from a variety of data streams and
|
|
527
|
+
using a variety of languages.
|
|
@@ -17,7 +17,8 @@ project ([documentation](https://akhundmurad.github.io/diator/)) with several en
|
|
|
17
17
|
6. Added support for [Transaction Outbox](https://microservices.io/patterns/data/transactional-outbox.html), ensuring
|
|
18
18
|
that `Notification` and `ECST` events are sent to the broker;
|
|
19
19
|
7. FastAPI supporting;
|
|
20
|
-
8. FastStream supporting
|
|
20
|
+
8. FastStream supporting;
|
|
21
|
+
9. [Protobuf](https://protobuf.dev/) events supporting.
|
|
21
22
|
|
|
22
23
|
## Request Handlers
|
|
23
24
|
|
|
@@ -253,6 +254,11 @@ the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/save_e
|
|
|
253
254
|
> You can specify the name of the Outbox table using the environment variable `OUTBOX_SQLA_TABLE`.
|
|
254
255
|
> By default, it is set to `outbox`.
|
|
255
256
|
|
|
257
|
+
> [!TIP]
|
|
258
|
+
> If you use the protobuf events you should specify `OutboxedEventRepository`
|
|
259
|
+
> by [protobuf serialize](https://github.com/vadikko2/cqrs/blob/master/src/cqrs/serializers/protobuf.py). A complete example can be found in
|
|
260
|
+
the [documentation](https://github.com/vadikko2/cqrs/blob/master/examples/save_proto_events_into_outbox.py)
|
|
261
|
+
|
|
256
262
|
## Producing Events from Outbox to Kafka
|
|
257
263
|
|
|
258
264
|
As an implementation of the Transactional Outbox pattern, the SqlAlchemyOutboxedEventRepository is available for use as
|
|
@@ -465,4 +471,13 @@ async def hello_world_event_handler(
|
|
|
465
471
|
await msg.ack()
|
|
466
472
|
```
|
|
467
473
|
|
|
468
|
-
A complete example can be found in
|
|
474
|
+
A complete example can be found in
|
|
475
|
+
the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/kafka_event_consuming.py)
|
|
476
|
+
|
|
477
|
+
## Protobuf messaging
|
|
478
|
+
|
|
479
|
+
The `python-cqrs` package supports integration with [protobuf](https://developers.google.com/protocol-buffers/).\
|
|
480
|
+
Protocol buffers are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data –
|
|
481
|
+
think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use
|
|
482
|
+
special generated source code to easily write and read your structured data to and from a variety of data streams and
|
|
483
|
+
using a variety of languages.
|
|
@@ -28,7 +28,7 @@ maintainers = [{name = "Vadim Kozyrevskiy", email = "vadikko2@mail.ru"}]
|
|
|
28
28
|
name = "python-cqrs"
|
|
29
29
|
readme = "README.md"
|
|
30
30
|
requires-python = ">=3.10"
|
|
31
|
-
version = "
|
|
31
|
+
version = "2.0.2"
|
|
32
32
|
|
|
33
33
|
[project.optional-dependencies]
|
|
34
34
|
dev = [
|
|
@@ -45,12 +45,18 @@ dev = [
|
|
|
45
45
|
"asyncmy==0.2.9"
|
|
46
46
|
]
|
|
47
47
|
examples = [
|
|
48
|
-
"aiokafka==0.10.0",
|
|
49
48
|
"fastapi==0.109.*",
|
|
50
49
|
"uvicorn==0.32.0",
|
|
51
50
|
"faststream[kafka]==0.5.28"
|
|
52
51
|
]
|
|
53
|
-
kafka = [
|
|
52
|
+
kafka = [
|
|
53
|
+
"aiokafka==0.10.0",
|
|
54
|
+
# for SchemaRegistry
|
|
55
|
+
"confluent-kafka==2.6.0"
|
|
56
|
+
]
|
|
57
|
+
protobuf = [
|
|
58
|
+
"protobuf==4.25.5"
|
|
59
|
+
]
|
|
54
60
|
|
|
55
61
|
[project.urls]
|
|
56
62
|
Issues = "https://github.com/vadikko2/python-cqrs/issues"
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
from cqrs.compressors
|
|
2
|
-
from cqrs.compressors.zlib import ZlibCompressor
|
|
1
|
+
from cqrs.compressors import Compressor, ZlibCompressor
|
|
3
2
|
from cqrs.container.di import DIContainer
|
|
4
3
|
from cqrs.container.protocol import Container
|
|
5
4
|
from cqrs.events import EventMap
|
|
6
|
-
from cqrs.events.event import DomainEvent,
|
|
5
|
+
from cqrs.events.event import DomainEvent, Event, NotificationEvent
|
|
7
6
|
from cqrs.events.event_emitter import EventEmitter
|
|
8
7
|
from cqrs.events.event_handler import EventHandler, SyncEventHandler
|
|
9
8
|
from cqrs.mediator import EventMediator, RequestMediator
|
|
10
|
-
from cqrs.
|
|
9
|
+
from cqrs.producer import EventProducer
|
|
11
10
|
from cqrs.outbox.repository import OutboxedEventRepository
|
|
12
11
|
from cqrs.outbox.sqlalchemy import SqlAlchemyOutboxedEventRepository
|
|
12
|
+
from cqrs.outbox.map import OutboxedEventMap
|
|
13
13
|
from cqrs.requests import RequestMap
|
|
14
14
|
from cqrs.requests.request import Request
|
|
15
15
|
from cqrs.requests.request_handler import RequestHandler, SyncRequestHandler
|
|
@@ -20,11 +20,11 @@ __all__ = (
|
|
|
20
20
|
"EventMediator",
|
|
21
21
|
"DomainEvent",
|
|
22
22
|
"NotificationEvent",
|
|
23
|
-
"ECSTEvent",
|
|
24
23
|
"Event",
|
|
25
24
|
"EventEmitter",
|
|
26
25
|
"EventHandler",
|
|
27
26
|
"EventMap",
|
|
27
|
+
"OutboxedEventMap",
|
|
28
28
|
"SyncEventHandler",
|
|
29
29
|
"Request",
|
|
30
30
|
"RequestHandler",
|
|
@@ -34,8 +34,8 @@ __all__ = (
|
|
|
34
34
|
"OutboxedEventRepository",
|
|
35
35
|
"SqlAlchemyOutboxedEventRepository",
|
|
36
36
|
"EventProducer",
|
|
37
|
-
"Compressor",
|
|
38
|
-
"ZlibCompressor",
|
|
39
37
|
"Container",
|
|
40
38
|
"DIContainer",
|
|
39
|
+
"Compressor",
|
|
40
|
+
"ZlibCompressor",
|
|
41
41
|
)
|
|
@@ -4,10 +4,11 @@ import logging
|
|
|
4
4
|
import typing
|
|
5
5
|
|
|
6
6
|
import aiokafka
|
|
7
|
-
import orjson
|
|
8
7
|
import retry_async
|
|
9
8
|
from aiokafka import errors
|
|
10
9
|
|
|
10
|
+
from cqrs.serializers import default
|
|
11
|
+
|
|
11
12
|
__all__ = (
|
|
12
13
|
"KafkaProducer",
|
|
13
14
|
"kafka_producer_factory",
|
|
@@ -40,9 +41,7 @@ SaslMechanism: typing.TypeAlias = typing.Literal[
|
|
|
40
41
|
logger = logging.getLogger("cqrs")
|
|
41
42
|
logger.setLevel(logging.DEBUG)
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
def _serializer(message: typing.Dict) -> typing.ByteString:
|
|
45
|
-
return orjson.dumps(message)
|
|
44
|
+
Serializer = typing.Callable[[typing.Any], typing.ByteString | None]
|
|
46
45
|
|
|
47
46
|
|
|
48
47
|
class _Singleton(type):
|
|
@@ -70,12 +69,12 @@ class KafkaProducer(metaclass=_Singleton):
|
|
|
70
69
|
if not await self._producer.client.ready(node_id=node_id):
|
|
71
70
|
await self._producer.start()
|
|
72
71
|
|
|
73
|
-
async def _produce(self, topic: typing.Text, message: typing.
|
|
72
|
+
async def _produce(self, topic: typing.Text, message: typing.Any):
|
|
74
73
|
await self._check_connection()
|
|
75
74
|
logger.debug(f"produce message {message} to topic {topic}")
|
|
76
75
|
await self._producer.send_and_wait(topic, value=message)
|
|
77
76
|
|
|
78
|
-
async def produce(self, topic: typing.Text, message: typing.
|
|
77
|
+
async def produce(self, topic: typing.Text, message: typing.Any):
|
|
79
78
|
"""
|
|
80
79
|
Produces event to kafka broker.
|
|
81
80
|
Tries to reconnect if connect has been lost or has not been opened.
|
|
@@ -94,13 +93,14 @@ def kafka_producer_factory(
|
|
|
94
93
|
retry_delay: int = 1,
|
|
95
94
|
user: typing.Text | None = None,
|
|
96
95
|
password: typing.Text | None = None,
|
|
96
|
+
value_serializer: Serializer | None = None,
|
|
97
97
|
) -> KafkaProducer:
|
|
98
98
|
loop = asyncio.get_event_loop()
|
|
99
99
|
asyncio.set_event_loop(loop)
|
|
100
100
|
|
|
101
101
|
producer = aiokafka.AIOKafkaProducer(
|
|
102
102
|
bootstrap_servers=dsn,
|
|
103
|
-
value_serializer=
|
|
103
|
+
value_serializer=value_serializer or default.default_serializer,
|
|
104
104
|
security_protocol=security_protocol,
|
|
105
105
|
sasl_mechanism=sasl_mechanism,
|
|
106
106
|
sasl_plain_username=user,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
import pydantic
|
|
5
|
+
from confluent_kafka.schema_registry import protobuf
|
|
6
|
+
from google.protobuf.message import Message
|
|
7
|
+
|
|
8
|
+
import cqrs
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("cqrs")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProtobufValueDeserializer:
|
|
14
|
+
"""
|
|
15
|
+
Deserialize protobuf message into CQRS event model.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
model: typing.Type[cqrs.NotificationEvent],
|
|
21
|
+
protobuf_model: typing.Type[Message],
|
|
22
|
+
):
|
|
23
|
+
self._model = model
|
|
24
|
+
self._protobuf_model = protobuf_model
|
|
25
|
+
|
|
26
|
+
def __call__(
|
|
27
|
+
self,
|
|
28
|
+
msg: typing.ByteString,
|
|
29
|
+
) -> cqrs.NotificationEvent | None:
|
|
30
|
+
protobuf_deserializer = protobuf.ProtobufDeserializer(
|
|
31
|
+
self._protobuf_model,
|
|
32
|
+
{"use.deprecated.format": False},
|
|
33
|
+
)
|
|
34
|
+
try:
|
|
35
|
+
proto_event = protobuf_deserializer(msg, None)
|
|
36
|
+
except Exception as error:
|
|
37
|
+
logger.error(
|
|
38
|
+
f"Error while deserializing protobuf message: {error}",
|
|
39
|
+
)
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
if proto_event is None:
|
|
43
|
+
logger.debug("Protobuf message is empty")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
return self._model.model_validate(proto_event)
|
|
48
|
+
except pydantic.ValidationError as error:
|
|
49
|
+
logger.error(
|
|
50
|
+
f"Error while deserializing protobuf message: {error}",
|
|
51
|
+
)
|
|
52
|
+
return
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from cqrs.events.event import DomainEvent,
|
|
1
|
+
from cqrs.events.event import DomainEvent, Event, NotificationEvent
|
|
2
2
|
from cqrs.events.event_emitter import EventEmitter
|
|
3
3
|
from cqrs.events.event_handler import EventHandler
|
|
4
4
|
from cqrs.events.map import EventMap
|
|
@@ -6,7 +6,6 @@ from cqrs.events.map import EventMap
|
|
|
6
6
|
__all__ = (
|
|
7
7
|
"Event",
|
|
8
8
|
"DomainEvent",
|
|
9
|
-
"ECSTEvent",
|
|
10
9
|
"NotificationEvent",
|
|
11
10
|
"EventEmitter",
|
|
12
11
|
"EventHandler",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import typing
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
import dotenv
|
|
8
|
+
import pydantic
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("cqrs")
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from google.protobuf.message import Message # noqa
|
|
14
|
+
except ImportError:
|
|
15
|
+
logger.warning(
|
|
16
|
+
"Please install protobuf dependencies with: `pip install python-cqrs[protobuf]`",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
dotenv.load_dotenv()
|
|
20
|
+
|
|
21
|
+
DEFAULT_OUTPUT_TOPIC = os.getenv("DEFAULT_OUTPUT_TOPIC", "output_topic")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Event(pydantic.BaseModel, frozen=True):
|
|
25
|
+
"""
|
|
26
|
+
The base class for events
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DomainEvent(Event, frozen=True):
|
|
31
|
+
"""
|
|
32
|
+
The base class for domain events
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
_P = typing.TypeVar("_P", typing.Any, None)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class NotificationEvent(Event, typing.Generic[_P], frozen=True):
|
|
40
|
+
"""
|
|
41
|
+
The base class for notification events
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
event_id: uuid.UUID = pydantic.Field(default_factory=uuid.uuid4)
|
|
45
|
+
event_timestamp: datetime.datetime = pydantic.Field(
|
|
46
|
+
default_factory=datetime.datetime.now,
|
|
47
|
+
)
|
|
48
|
+
event_name: typing.Text
|
|
49
|
+
topic: typing.Text = pydantic.Field(default=DEFAULT_OUTPUT_TOPIC)
|
|
50
|
+
|
|
51
|
+
payload: _P = pydantic.Field(default=None)
|
|
52
|
+
|
|
53
|
+
model_config = pydantic.ConfigDict(from_attributes=True)
|
|
54
|
+
|
|
55
|
+
def proto(self) -> "Message":
|
|
56
|
+
raise NotImplementedError("Method not implemented")
|
|
57
|
+
|
|
58
|
+
def __hash__(self):
|
|
59
|
+
return hash(self.event_id)
|
|
@@ -33,18 +33,17 @@ class EventEmitter:
|
|
|
33
33
|
|
|
34
34
|
async def _send_to_broker(
|
|
35
35
|
self,
|
|
36
|
-
event: event_model.NotificationEvent
|
|
36
|
+
event: event_model.NotificationEvent,
|
|
37
37
|
) -> None:
|
|
38
38
|
"""
|
|
39
39
|
Sends event to the message broker.
|
|
40
40
|
"""
|
|
41
41
|
if not self._message_broker:
|
|
42
42
|
raise RuntimeError(
|
|
43
|
-
f"To
|
|
43
|
+
f"To send event {event}, message_broker argument must be specified.",
|
|
44
44
|
)
|
|
45
45
|
|
|
46
46
|
message = message_brokers.Message(
|
|
47
|
-
message_type=event.event_type,
|
|
48
47
|
message_name=type(event).__name__,
|
|
49
48
|
message_id=event.event_id,
|
|
50
49
|
topic=event.topic,
|
|
@@ -52,8 +51,7 @@ class EventEmitter:
|
|
|
52
51
|
)
|
|
53
52
|
|
|
54
53
|
logger.debug(
|
|
55
|
-
"Sending
|
|
56
|
-
event.event_type,
|
|
54
|
+
"Sending Event(%s) to message broker %s",
|
|
57
55
|
event.event_id,
|
|
58
56
|
type(self._message_broker).__name__,
|
|
59
57
|
)
|
|
@@ -85,7 +83,3 @@ class EventEmitter:
|
|
|
85
83
|
@emit.register
|
|
86
84
|
async def _(self, event: event_model.NotificationEvent) -> None:
|
|
87
85
|
await self._send_to_broker(event)
|
|
88
|
-
|
|
89
|
-
@emit.register
|
|
90
|
-
async def _(self, event: event_model.ECSTEvent) -> None:
|
|
91
|
-
await self._send_to_broker(event)
|
|
@@ -6,11 +6,10 @@ import pydantic
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class Message(pydantic.BaseModel):
|
|
9
|
-
message_type: typing.Text = pydantic.Field()
|
|
10
9
|
message_name: typing.Text = pydantic.Field()
|
|
11
10
|
message_id: uuid.UUID = pydantic.Field(default_factory=uuid.uuid4)
|
|
12
11
|
topic: typing.Text
|
|
13
|
-
payload: typing.
|
|
12
|
+
payload: typing.Any
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
class MessageBroker(abc.ABC):
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from cqrs.events import event
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OutboxedEventMap:
|
|
7
|
+
_registry: typing.Dict[typing.Text, typing.Type[event.NotificationEvent]] = {}
|
|
8
|
+
|
|
9
|
+
@classmethod
|
|
10
|
+
def register(
|
|
11
|
+
cls,
|
|
12
|
+
event_name: typing.Text,
|
|
13
|
+
event_type: typing.Type[event.NotificationEvent],
|
|
14
|
+
) -> None:
|
|
15
|
+
if event_name in cls._registry:
|
|
16
|
+
raise KeyError(f"Event with {event_name} already registered")
|
|
17
|
+
cls._registry[event_name] = event_type
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def get(
|
|
21
|
+
cls,
|
|
22
|
+
event_name: typing.Text,
|
|
23
|
+
) -> typing.Type[event.NotificationEvent] | None:
|
|
24
|
+
return cls._registry.get(event_name)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
import cqrs
|
|
4
|
+
from cqrs.outbox import repository
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MockOutboxedEventRepository(repository.OutboxedEventRepository[typing.Dict]):
|
|
8
|
+
COUNTER: typing.ClassVar = 0
|
|
9
|
+
|
|
10
|
+
def __init__(self, session_factory: typing.Callable[[], typing.Dict]):
|
|
11
|
+
self.session = session_factory()
|
|
12
|
+
|
|
13
|
+
async def __aenter__(self) -> typing.Dict:
|
|
14
|
+
return self.session
|
|
15
|
+
|
|
16
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
def add(self, session: typing.Dict, event: cqrs.NotificationEvent) -> None:
|
|
20
|
+
MockOutboxedEventRepository.COUNTER += 1
|
|
21
|
+
session[MockOutboxedEventRepository.COUNTER] = repository.OutboxedEvent(
|
|
22
|
+
id=MockOutboxedEventRepository.COUNTER,
|
|
23
|
+
event=event,
|
|
24
|
+
topic=event.topic,
|
|
25
|
+
status=repository.EventStatus.NEW,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
async def get_many(
|
|
29
|
+
self,
|
|
30
|
+
session: typing.Dict,
|
|
31
|
+
batch_size: int = 100,
|
|
32
|
+
topic: typing.Text | None = None,
|
|
33
|
+
) -> typing.List[repository.OutboxedEvent]:
|
|
34
|
+
return list(
|
|
35
|
+
filter(lambda e: topic == e.topic, session.values())
|
|
36
|
+
if topic
|
|
37
|
+
else list(session.values()),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
async def update_status(
|
|
41
|
+
self,
|
|
42
|
+
session: typing.Dict,
|
|
43
|
+
outboxed_event_id: int,
|
|
44
|
+
new_status: repository.EventStatus,
|
|
45
|
+
):
|
|
46
|
+
if outboxed_event_id not in session:
|
|
47
|
+
return
|
|
48
|
+
if new_status is repository.EventStatus.PRODUCED:
|
|
49
|
+
del session[outboxed_event_id]
|
|
50
|
+
|
|
51
|
+
async def commit(self, session: typing.Dict):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
async def rollback(self, session: typing.Dict):
|
|
55
|
+
pass
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import abc
|
|
2
2
|
import enum
|
|
3
3
|
import typing
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
import pydantic
|
|
5
6
|
|
|
6
7
|
import cqrs
|
|
7
8
|
from cqrs.events import event as ev
|
|
8
9
|
|
|
9
|
-
Event = typing.TypeVar("Event", ev.NotificationEvent, ev.ECSTEvent, contravariant=True)
|
|
10
10
|
Session = typing.TypeVar("Session")
|
|
11
11
|
|
|
12
12
|
|
|
@@ -16,15 +16,14 @@ class EventStatus(enum.StrEnum):
|
|
|
16
16
|
NOT_PRODUCED = "not_produced"
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
class
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
):
|
|
25
|
-
self._session_factory = session_factory
|
|
26
|
-
self._compressor = compressor
|
|
19
|
+
class OutboxedEvent(pydantic.BaseModel, frozen=True):
|
|
20
|
+
id: pydantic.PositiveInt
|
|
21
|
+
event: cqrs.NotificationEvent
|
|
22
|
+
topic: typing.Text
|
|
23
|
+
status: EventStatus
|
|
27
24
|
|
|
25
|
+
|
|
26
|
+
class OutboxedEventRepository(abc.ABC, typing.Generic[Session]):
|
|
28
27
|
@abc.abstractmethod
|
|
29
28
|
async def __aenter__(self) -> Session:
|
|
30
29
|
"""start transaction"""
|
|
@@ -37,28 +36,24 @@ class OutboxedEventRepository(abc.ABC, typing.Generic[Session]):
|
|
|
37
36
|
def add(
|
|
38
37
|
self,
|
|
39
38
|
session: Session,
|
|
40
|
-
event:
|
|
39
|
+
event: ev.NotificationEvent,
|
|
41
40
|
) -> None:
|
|
42
41
|
"""Add an event to the repository."""
|
|
43
42
|
|
|
44
|
-
@abc.abstractmethod
|
|
45
|
-
async def get_one(self, session: Session, event_id: uuid.UUID) -> Event | None:
|
|
46
|
-
"""Get one event from the repository."""
|
|
47
|
-
|
|
48
43
|
@abc.abstractmethod
|
|
49
44
|
async def get_many(
|
|
50
45
|
self,
|
|
51
46
|
session: Session,
|
|
52
47
|
batch_size: int = 100,
|
|
53
48
|
topic: typing.Text | None = None,
|
|
54
|
-
) -> typing.List[
|
|
49
|
+
) -> typing.List[OutboxedEvent]:
|
|
55
50
|
"""Get many events from the repository."""
|
|
56
51
|
|
|
57
52
|
@abc.abstractmethod
|
|
58
53
|
async def update_status(
|
|
59
54
|
self,
|
|
60
55
|
session: Session,
|
|
61
|
-
|
|
56
|
+
outboxed_event_id: int,
|
|
62
57
|
new_status: EventStatus,
|
|
63
58
|
):
|
|
64
59
|
"""Update the event status"""
|