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.
Files changed (57) hide show
  1. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/PKG-INFO +21 -4
  2. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/README.md +17 -2
  3. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/pyproject.toml +9 -3
  4. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/__init__.py +7 -7
  5. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/adapters/kafka.py +7 -7
  6. python_cqrs-2.0.2/src/cqrs/compressors/__init__.py +4 -0
  7. python_cqrs-2.0.2/src/cqrs/deserializers/protobuf.py +52 -0
  8. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/events/__init__.py +1 -2
  9. python_cqrs-2.0.2/src/cqrs/events/event.py +59 -0
  10. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/events/event_emitter.py +3 -9
  11. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/message_brokers/protocol.py +1 -2
  12. python_cqrs-2.0.2/src/cqrs/outbox/map.py +24 -0
  13. python_cqrs-2.0.2/src/cqrs/outbox/mock.py +55 -0
  14. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/outbox/repository.py +12 -17
  15. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/outbox/sqlalchemy.py +51 -99
  16. {python_cqrs-1.0.0/src/cqrs/outbox → python_cqrs-2.0.2/src/cqrs}/producer.py +25 -34
  17. python_cqrs-2.0.2/src/cqrs/serializers/__init__.py +0 -0
  18. python_cqrs-2.0.2/src/cqrs/serializers/default.py +8 -0
  19. python_cqrs-2.0.2/src/cqrs/serializers/protobuf.py +39 -0
  20. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/python_cqrs.egg-info/PKG-INFO +21 -4
  21. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/python_cqrs.egg-info/SOURCES.txt +7 -2
  22. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/python_cqrs.egg-info/requires.txt +4 -1
  23. python_cqrs-1.0.0/src/cqrs/events/event.py +0 -103
  24. python_cqrs-1.0.0/src/cqrs/outbox/mock.py +0 -51
  25. python_cqrs-1.0.0/src/cqrs/outbox/protocol.py +0 -56
  26. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/LICENSE +0 -0
  27. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/setup.cfg +0 -0
  28. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/adapters/__init__.py +0 -0
  29. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/adapters/amqp.py +0 -0
  30. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/compressors/protocol.py +0 -0
  31. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/compressors/zlib.py +0 -0
  32. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/container/__init__.py +0 -0
  33. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/container/di.py +0 -0
  34. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/container/protocol.py +0 -0
  35. {python_cqrs-1.0.0/src/cqrs/compressors → python_cqrs-2.0.2/src/cqrs/deserializers}/__init__.py +0 -0
  36. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/dispatcher/__init__.py +0 -0
  37. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/dispatcher/dispatcher.py +0 -0
  38. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/events/bootstrap.py +0 -0
  39. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/events/event_handler.py +0 -0
  40. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/events/map.py +0 -0
  41. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/mediator.py +0 -0
  42. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/message_brokers/__init__.py +0 -0
  43. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/message_brokers/amqp.py +0 -0
  44. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/message_brokers/devnull.py +0 -0
  45. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/message_brokers/kafka.py +0 -0
  46. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/middlewares/__init__.py +0 -0
  47. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/middlewares/base.py +0 -0
  48. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/middlewares/logging.py +0 -0
  49. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/outbox/__init__.py +0 -0
  50. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/requests/__init__.py +0 -0
  51. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/requests/bootstrap.py +0 -0
  52. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/requests/map.py +0 -0
  53. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/requests/request.py +0 -0
  54. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/requests/request_handler.py +0 -0
  55. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/cqrs/response.py +0 -0
  56. {python_cqrs-1.0.0 → python_cqrs-2.0.2}/src/python_cqrs.egg-info/dependency_links.txt +0 -0
  57. {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: 1.0.0
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 the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/kafka_event_consuming.py)
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 the [documentation](https://github.com/vadikko2/python-cqrs/blob/master/examples/kafka_event_consuming.py)
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 = "1.0.0"
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 = ["aiokafka==0.10.0"]
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.protocol import Compressor
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, ECSTEvent, Event, NotificationEvent
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.outbox.producer import EventProducer
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.Dict):
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.Dict):
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=_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,4 @@
1
+ from cqrs.compressors.protocol import Compressor
2
+ from cqrs.compressors.zlib import ZlibCompressor
3
+
4
+ __all__ = ("ZlibCompressor", "Compressor")
@@ -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, ECSTEvent, Event, NotificationEvent
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 | event_model.ECSTEvent,
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 use {event.event_type}, message_broker argument must be specified.",
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 %s Event(%s) to message broker %s",
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.Dict
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
- import uuid
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 OutboxedEventRepository(abc.ABC, typing.Generic[Session]):
20
- def __init__(
21
- self,
22
- session_factory: typing.Callable[[], Session],
23
- compressor: cqrs.Compressor | None = None,
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: 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[Event]:
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
- event_id: uuid.UUID,
56
+ outboxed_event_id: int,
62
57
  new_status: EventStatus,
63
58
  ):
64
59
  """Update the event status"""