python-cqrs 0.0.2__tar.gz → 0.0.5__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 (52) hide show
  1. {python_cqrs-0.0.2/src/python_cqrs.egg-info → python_cqrs-0.0.5}/PKG-INFO +15 -16
  2. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/pyproject.toml +13 -15
  3. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/compressors/protocol.py +2 -0
  4. python_cqrs-0.0.5/src/cqrs/container/di.py +29 -0
  5. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/container/protocol.py +3 -3
  6. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/dispatcher/dispatcher.py +22 -8
  7. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/events/bootstrap.py +12 -15
  8. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/events/event.py +9 -5
  9. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/events/event_emitter.py +11 -6
  10. python_cqrs-0.0.5/src/cqrs/events/map.py +28 -0
  11. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/mediator.py +17 -11
  12. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/middlewares/base.py +1 -2
  13. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/middlewares/logging.py +9 -10
  14. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/outbox/producer.py +32 -7
  15. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/outbox/protocol.py +13 -1
  16. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/outbox/sqlalchemy.py +87 -29
  17. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/requests/bootstrap.py +18 -20
  18. python_cqrs-0.0.5/src/cqrs/requests/map.py +27 -0
  19. {python_cqrs-0.0.2 → python_cqrs-0.0.5/src/python_cqrs.egg-info}/PKG-INFO +15 -16
  20. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/python_cqrs.egg-info/SOURCES.txt +0 -1
  21. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/python_cqrs.egg-info/requires.txt +6 -11
  22. python_cqrs-0.0.2/src/cqrs/container/di.py +0 -19
  23. python_cqrs-0.0.2/src/cqrs/events/map.py +0 -27
  24. python_cqrs-0.0.2/src/cqrs/registry.py +0 -29
  25. python_cqrs-0.0.2/src/cqrs/requests/map.py +0 -30
  26. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/LICENSE +0 -0
  27. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/README.md +0 -0
  28. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/setup.cfg +0 -0
  29. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/__init__.py +0 -0
  30. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/adapters/__init__.py +0 -0
  31. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/adapters/amqp.py +0 -0
  32. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/adapters/kafka.py +0 -0
  33. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/compressors/__init__.py +0 -0
  34. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/compressors/zlib.py +0 -0
  35. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/container/__init__.py +0 -0
  36. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/dispatcher/__init__.py +0 -0
  37. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/events/__init__.py +0 -0
  38. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/events/event_handler.py +0 -0
  39. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/message_brokers/__init__.py +0 -0
  40. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/message_brokers/amqp.py +0 -0
  41. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/message_brokers/devnull.py +0 -0
  42. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/message_brokers/kafka.py +0 -0
  43. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/message_brokers/protocol.py +0 -0
  44. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/middlewares/__init__.py +0 -0
  45. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/outbox/__init__.py +0 -0
  46. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/outbox/repository.py +0 -0
  47. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/requests/__init__.py +0 -0
  48. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/requests/request.py +0 -0
  49. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/requests/request_handler.py +0 -0
  50. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/cqrs/response.py +0 -0
  51. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/python_cqrs.egg-info/dependency_links.txt +0 -0
  52. {python_cqrs-0.0.2 → python_cqrs-0.0.5}/src/python_cqrs.egg-info/top_level.txt +0 -0
@@ -1,7 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-cqrs
3
- Version: 0.0.2
4
- Author: Vadim Kozyrevskiy
3
+ Version: 0.0.5
4
+ Summary: Python CQRS pattern implementation
5
+ Author: Nikita Kunov
6
+ Author-email: Vadim Kozyrevskiy <vadikko2@mail.ru>, Dmitriy Kutlubaev <kutlubaev00@mail.ru>
5
7
  Description-Content-Type: text/markdown
6
8
  License-File: LICENSE
7
9
  Requires-Dist: pydantic==2.*
@@ -10,22 +12,19 @@ Requires-Dist: aio-pika==9.3.0
10
12
  Requires-Dist: di[anyio]==0.79.2
11
13
  Requires-Dist: sqlalchemy[asyncio]==2.0.*
12
14
  Requires-Dist: retry-async==0.1.4
13
- Provides-Extra: git
14
- Requires-Dist: pre-commit==3.8.0; extra == "git"
15
+ Provides-Extra: dev
16
+ Requires-Dist: pre-commit==3.8.0; extra == "dev"
17
+ Requires-Dist: pyright==1.1.377; extra == "dev"
18
+ Requires-Dist: ruff==0.6.2; extra == "dev"
19
+ Requires-Dist: aiokafka==0.10.0; extra == "dev"
20
+ Requires-Dist: pytest~=7.4.2; extra == "dev"
21
+ Requires-Dist: pytest-asyncio~=0.21.1; extra == "dev"
22
+ Requires-Dist: pytest-env==0.6.2; extra == "dev"
23
+ Requires-Dist: python-dotenv==1.0.1; extra == "dev"
24
+ Requires-Dist: cryptography==42.0.2; extra == "dev"
25
+ Requires-Dist: asyncmy==0.2.9; extra == "dev"
15
26
  Provides-Extra: kafka
16
27
  Requires-Dist: aiokafka==0.10.0; extra == "kafka"
17
- Provides-Extra: lint
18
- Requires-Dist: flake8==7.0.0; extra == "lint"
19
- Requires-Dist: flake8-pytest; extra == "lint"
20
- Requires-Dist: black; extra == "lint"
21
- Provides-Extra: tests
22
- Requires-Dist: aiokafka==0.10.0; extra == "tests"
23
- Requires-Dist: pytest~=7.4.2; extra == "tests"
24
- Requires-Dist: pytest-asyncio~=0.21.1; extra == "tests"
25
- Requires-Dist: pytest-env==0.6.2; extra == "tests"
26
- Requires-Dist: python-dotenv==1.0.1; extra == "tests"
27
- Requires-Dist: cryptography==42.0.2; extra == "tests"
28
- Requires-Dist: asyncmy==0.2.9; extra == "tests"
29
28
 
30
29
  # CQRS
31
30
 
@@ -4,7 +4,9 @@ requires = ["setuptools>=42", "wheel"]
4
4
 
5
5
  [project]
6
6
  authors = [
7
- {name = "Vadim Kozyrevskiy"}
7
+ {name = "Vadim Kozyrevskiy", email = "vadikko2@mail.ru"},
8
+ {name = "Dmitriy Kutlubaev", email = "kutlubaev00@mail.ru"},
9
+ {name = "Nikita Kunov"}
8
10
  ]
9
11
  dependencies = [
10
12
  "pydantic==2.*",
@@ -14,24 +16,17 @@ dependencies = [
14
16
  "sqlalchemy[asyncio]==2.0.*",
15
17
  "retry-async==0.1.4"
16
18
  ]
17
- description = ""
19
+ description = "Python CQRS pattern implementation"
18
20
  name = "python-cqrs"
19
21
  readme = "README.md"
20
- version = "0.0.2"
22
+ version = "0.0.5"
21
23
 
22
24
  [project.optional-dependencies]
23
- git = [
24
- "pre-commit==3.8.0 "
25
- ]
26
- kafka = [
27
- "aiokafka==0.10.0"
28
- ]
29
- lint = [
30
- "flake8==7.0.0",
31
- "flake8-pytest",
32
- "black"
33
- ]
34
- tests = [
25
+ dev = [
26
+ "pre-commit==3.8.0",
27
+ "pyright==1.1.377",
28
+ "ruff==0.6.2",
29
+ # Tests
35
30
  "aiokafka==0.10.0",
36
31
  "pytest~=7.4.2",
37
32
  "pytest-asyncio~=0.21.1",
@@ -40,6 +35,9 @@ tests = [
40
35
  "cryptography==42.0.2",
41
36
  "asyncmy==0.2.9"
42
37
  ]
38
+ kafka = [
39
+ "aiokafka==0.10.0"
40
+ ]
43
41
 
44
42
  [tool.pytest.ini_options]
45
43
  addopts = "--junit-xml=report.xml"
@@ -4,6 +4,8 @@ import typing
4
4
  class Compressor(typing.Protocol):
5
5
  def compress(self, value: bytes) -> bytes:
6
6
  """Compress value"""
7
+ raise NotImplementedError
7
8
 
8
9
  def decompress(self, value: bytes) -> bytes:
9
10
  """Decompress compressed value"""
11
+ raise NotImplementedError
@@ -0,0 +1,29 @@
1
+ import typing
2
+
3
+ import di
4
+ from di import dependent, executors
5
+
6
+ from cqrs import container as cqrs_container
7
+
8
+ T = typing.TypeVar("T")
9
+
10
+
11
+ class DIContainer(cqrs_container.Container[di.Container]):
12
+ def __init__(self, external_container: di.Container) -> None:
13
+ self._external_container = external_container
14
+
15
+ @property
16
+ def external_container(self) -> di.Container:
17
+ return self._external_container
18
+
19
+ def attach_external_container(self, container: di.Container) -> None:
20
+ self._external_container = container
21
+
22
+ async def resolve(self, type_: typing.Type[T]) -> T:
23
+ executor = executors.AsyncExecutor()
24
+ solved = self._external_container.solve(
25
+ dependent.Dependent(type_, scope="request"),
26
+ scopes=["request"],
27
+ )
28
+ with self._external_container.enter_scope("request") as state:
29
+ return await solved.execute_async(executor=executor, state=state)
@@ -11,10 +11,10 @@ class Container(typing.Protocol[C]):
11
11
 
12
12
  @property
13
13
  def external_container(self) -> C:
14
- ...
14
+ raise NotImplementedError
15
15
 
16
16
  def attach_external_container(self, container: C) -> None:
17
- ...
17
+ raise NotImplementedError
18
18
 
19
19
  async def resolve(self, type_: typing.Type[T]) -> T:
20
- ...
20
+ raise NotImplementedError
@@ -3,14 +3,20 @@ import typing
3
3
 
4
4
  import pydantic
5
5
 
6
- from cqrs import container as di_container
7
- from cqrs import events as cqrs_events
8
- from cqrs import middlewares, requests
9
- from cqrs import response as res
6
+ from cqrs import (
7
+ container as di_container,
8
+ events as cqrs_events,
9
+ middlewares,
10
+ requests,
11
+ response as res,
12
+ )
10
13
 
11
14
  logger = logging.getLogger("cqrs")
12
15
 
13
16
 
17
+ class RequestHandlerDoesNotExist(Exception): ...
18
+
19
+
14
20
  class RequestDispatchResult(pydantic.BaseModel):
15
21
  response: res.Response | None = pydantic.Field(default=None)
16
22
  events: typing.List[cqrs_events.Event] = pydantic.Field(default_factory=list)
@@ -28,7 +34,11 @@ class RequestDispatcher:
28
34
  self._middleware_chain = middleware_chain or middlewares.MiddlewareChain()
29
35
 
30
36
  async def dispatch(self, request: requests.Request) -> RequestDispatchResult:
31
- handler_type = self._request_map.get(type(request))
37
+ handler_type = self._request_map.get(type(request), None)
38
+ if handler_type is None:
39
+ raise RequestHandlerDoesNotExist(
40
+ f"RequestHandler not found matching Request type {type(request)}",
41
+ )
32
42
  handler = await self._container.resolve(handler_type)
33
43
  wrapped_handle = self._middleware_chain.wrap(handler.handle)
34
44
  response = await wrapped_handle(request)
@@ -49,12 +59,16 @@ class EventDispatcher:
49
59
  self._container = container
50
60
  self._middleware_chain = middleware_chain or middlewares.MiddlewareChain()
51
61
 
52
- async def _handle_event(self, event: E, handle_type: typing.Type[cqrs_events.EventHandler[E]]):
62
+ async def _handle_event(
63
+ self,
64
+ event: cqrs_events.Event,
65
+ handle_type: typing.Type[cqrs_events.EventHandler[cqrs_events.Event]],
66
+ ):
53
67
  handler = await self._container.resolve(handle_type)
54
68
  await handler.handle(event)
55
69
 
56
- async def dispatch(self, event: E) -> None:
57
- handler_types = self._event_map.get(type(event))
70
+ async def dispatch(self, event: cqrs_events.Event) -> None:
71
+ handler_types = self._event_map.get(type(event), [])
58
72
  if not handler_types:
59
73
  logger.warning(
60
74
  "Handlers for event %s not found",
@@ -5,23 +5,19 @@ import di
5
5
  import cqrs
6
6
  from cqrs import events
7
7
  from cqrs.container import di as ed_di_container
8
- from cqrs.middlewares import base as mediator_middlewares
9
- from cqrs.middlewares import logging as logging_middleware
8
+ from cqrs.middlewares import base as mediator_middlewares, logging as logging_middleware
10
9
 
11
10
 
12
11
  def setup_mediator(
13
- container: ed_di_container.DIContainer | None,
14
- middlewares: typing.Iterable[mediator_middlewares.Middleware] | None = None,
15
- events_mapper: typing.Callable[[events.EventMap], None] = None,
12
+ container: ed_di_container.DIContainer,
13
+ middlewares: typing.Iterable[mediator_middlewares.Middleware],
14
+ events_mapper: typing.Callable[[events.EventMap], None] | None = None,
16
15
  ) -> cqrs.EventMediator:
17
-
18
16
  _events_mapper = events.EventMap()
19
- if events_mapper:
17
+ if events_mapper is not None:
20
18
  events_mapper(_events_mapper)
21
19
 
22
20
  middleware_chain = mediator_middlewares.MiddlewareChain()
23
- if middlewares is None:
24
- middlewares = []
25
21
 
26
22
  for middleware in middlewares:
27
23
  middleware_chain.add(middleware)
@@ -34,9 +30,9 @@ def setup_mediator(
34
30
 
35
31
 
36
32
  def bootstrap(
37
- di_container: di.Container | None = None,
38
- middlewares: typing.Iterable[mediator_middlewares.Middleware] | None = None,
39
- events_mapper: typing.Callable[[events.EventMap], None] = None,
33
+ di_container: di.Container,
34
+ middlewares: typing.Sequence[mediator_middlewares.Middleware] | None = None,
35
+ events_mapper: typing.Callable[[events.EventMap], None] | None = None,
40
36
  on_startup: typing.List[typing.Callable[[], None]] | None = None,
41
37
  ) -> cqrs.EventMediator:
42
38
  if on_startup is None:
@@ -45,11 +41,12 @@ def bootstrap(
45
41
  for fun in on_startup:
46
42
  fun()
47
43
 
48
- if middlewares is None:
49
- middlewares = []
50
44
  container = ed_di_container.DIContainer(di_container)
45
+ middlewares_list: typing.List[mediator_middlewares.Middleware] = list(
46
+ middlewares or [],
47
+ )
51
48
  return setup_mediator(
52
49
  container,
53
50
  events_mapper=events_mapper,
54
- middlewares=middlewares + [logging_middleware.LoggingMiddleware()],
51
+ middlewares=middlewares_list + [logging_middleware.LoggingMiddleware()],
55
52
  )
@@ -9,7 +9,7 @@ class Event(pydantic.BaseModel, frozen=True):
9
9
  """The base class for events"""
10
10
 
11
11
 
12
- class DomainEvent(Event):
12
+ class DomainEvent(Event, frozen=True):
13
13
  """
14
14
  The base class for domain events.
15
15
  """
@@ -18,7 +18,7 @@ class DomainEvent(Event):
18
18
  _P = typing.TypeVar("_P")
19
19
 
20
20
 
21
- class NotificationEvent(Event):
21
+ class NotificationEvent(Event, frozen=True):
22
22
  """
23
23
  The base class for notification events.
24
24
 
@@ -38,7 +38,9 @@ class NotificationEvent(Event):
38
38
  """
39
39
 
40
40
  event_id: uuid.UUID = pydantic.Field(default_factory=uuid.uuid4)
41
- event_timestamp: datetime.datetime = pydantic.Field(default_factory=datetime.datetime.now)
41
+ event_timestamp: datetime.datetime = pydantic.Field(
42
+ default_factory=datetime.datetime.now,
43
+ )
42
44
  event_name: typing.Text
43
45
  event_type: typing.ClassVar[typing.Text] = "notification_event"
44
46
 
@@ -50,7 +52,7 @@ class NotificationEvent(Event):
50
52
  return hash(self.event_id)
51
53
 
52
54
 
53
- class ECSTEvent(Event, typing.Generic[_P]):
55
+ class ECSTEvent(Event, typing.Generic[_P], frozen=True):
54
56
  """
55
57
  Base class for ECST events.
56
58
 
@@ -74,7 +76,9 @@ class ECSTEvent(Event, typing.Generic[_P]):
74
76
  """
75
77
 
76
78
  event_id: uuid.UUID = pydantic.Field(default_factory=uuid.uuid4)
77
- event_timestamp: datetime.datetime = pydantic.Field(default_factory=datetime.datetime.now)
79
+ event_timestamp: datetime.datetime = pydantic.Field(
80
+ default_factory=datetime.datetime.now,
81
+ )
78
82
  event_name: typing.Text
79
83
  event_type: typing.ClassVar = "ecst_event"
80
84
 
@@ -24,12 +24,11 @@ class EventEmitter:
24
24
  self._message_broker = message_broker
25
25
 
26
26
  @functools.singledispatchmethod
27
- async def emit(self, event: event.Event) -> None:
28
- ...
27
+ async def emit(self, event: event.Event) -> None: ...
29
28
 
30
29
  @emit.register
31
30
  async def _(self, event: event.DomainEvent) -> None:
32
- handlers_types = self._event_map.get(type(event))
31
+ handlers_types = self._event_map.get(type(event), [])
33
32
  if not handlers_types:
34
33
  logger.warning(
35
34
  "Handlers for domain event %s not found",
@@ -47,7 +46,9 @@ class EventEmitter:
47
46
  @emit.register
48
47
  async def _(self, event: event.NotificationEvent) -> None:
49
48
  if not self._message_broker:
50
- raise RuntimeError("To use NotificationEvent, message_broker argument must be specified.")
49
+ raise RuntimeError(
50
+ "To use NotificationEvent, message_broker argument must be specified.",
51
+ )
51
52
 
52
53
  message = _build_message(event)
53
54
 
@@ -62,7 +63,9 @@ class EventEmitter:
62
63
  @emit.register
63
64
  async def _(self, event: event.ECSTEvent) -> None:
64
65
  if not self._message_broker:
65
- raise RuntimeError("To use ECSTEvent, message_broker argument must be specified.")
66
+ raise RuntimeError(
67
+ "To use ECSTEvent, message_broker argument must be specified.",
68
+ )
66
69
 
67
70
  message = _build_message(event)
68
71
 
@@ -75,7 +78,9 @@ class EventEmitter:
75
78
  await self._message_broker.send_message(message)
76
79
 
77
80
 
78
- def _build_message(event: event.NotificationEvent | event.ECSTEvent) -> message_brokers.Message:
81
+ def _build_message(
82
+ event: event.NotificationEvent | event.ECSTEvent,
83
+ ) -> message_brokers.Message:
79
84
  payload = event.model_dump(mode="json")
80
85
 
81
86
  return message_brokers.Message(
@@ -0,0 +1,28 @@
1
+ import typing
2
+
3
+ from cqrs.events import event, event_handler
4
+
5
+ _KT = typing.TypeVar("_KT", bound=typing.Type[event.Event])
6
+ _VT = typing.List[typing.Type[event_handler.EventHandler[event.Event]]]
7
+
8
+
9
+ class EventMap(typing.Dict[_KT, _VT]):
10
+ def bind(
11
+ self,
12
+ event_type: _KT,
13
+ handler_type: typing.Type[event_handler.EventHandler[event.Event]],
14
+ ) -> None:
15
+ if event_type not in self:
16
+ self[event_type] = [handler_type]
17
+ else:
18
+ if handler_type in self[event_type]:
19
+ raise KeyError(f"{handler_type} already bind to {event_type}")
20
+ self[event_type].append(handler_type)
21
+
22
+ def __setitem__(self, __key: _KT, __value: _VT) -> None:
23
+ if __key in self:
24
+ raise KeyError(f"{__key} already exists in registry")
25
+ super().__setitem__(__key, __value)
26
+
27
+ def __delitem__(self, __key_: _KT):
28
+ raise TypeError(f"{self.__class__.__name__} has no delete method")
@@ -1,11 +1,13 @@
1
1
  import typing
2
2
 
3
- from cqrs import container as di_container
4
- from cqrs import dispatcher, events, middlewares, requests, response
5
-
6
- Req = typing.TypeVar("Req", bound=requests.Request, contravariant=True)
7
- Resp = typing.TypeVar("Resp", bound=response.Response, covariant=True)
8
- E = typing.TypeVar("E", bound=events.Event, contravariant=True)
3
+ from cqrs import (
4
+ container as di_container,
5
+ dispatcher,
6
+ events,
7
+ middlewares,
8
+ requests,
9
+ response,
10
+ )
9
11
 
10
12
 
11
13
  class RequestMediator:
@@ -43,7 +45,9 @@ class RequestMediator:
43
45
  event_emitter: events.EventEmitter | None = None,
44
46
  middleware_chain: middlewares.MiddlewareChain | None = None,
45
47
  *,
46
- dispatcher_type: typing.Type[dispatcher.RequestDispatcher] = dispatcher.RequestDispatcher,
48
+ dispatcher_type: typing.Type[
49
+ dispatcher.RequestDispatcher
50
+ ] = dispatcher.RequestDispatcher,
47
51
  ) -> None:
48
52
  self._event_emitter = event_emitter
49
53
  self._dispatcher = dispatcher_type(
@@ -52,7 +56,7 @@ class RequestMediator:
52
56
  middleware_chain=middleware_chain, # type: ignore
53
57
  )
54
58
 
55
- async def send(self, request: Req) -> Resp | None:
59
+ async def send(self, request: requests.Request) -> response.Response | None:
56
60
  dispatch_result = await self._dispatcher.dispatch(request)
57
61
 
58
62
  if dispatch_result.events:
@@ -60,7 +64,7 @@ class RequestMediator:
60
64
 
61
65
  return dispatch_result.response
62
66
 
63
- async def _send_events(self, events: typing.List[E]) -> None:
67
+ async def _send_events(self, events: typing.List[events.Event]) -> None:
64
68
  if not self._event_emitter:
65
69
  return
66
70
 
@@ -91,7 +95,9 @@ class EventMediator:
91
95
  container: di_container.Container,
92
96
  middleware_chain: middlewares.MiddlewareChain | None = None,
93
97
  *,
94
- dispatcher_type: typing.Type[dispatcher.EventDispatcher] = dispatcher.EventDispatcher,
98
+ dispatcher_type: typing.Type[
99
+ dispatcher.EventDispatcher
100
+ ] = dispatcher.EventDispatcher,
95
101
  ):
96
102
  self._dispatcher = dispatcher_type(
97
103
  event_map=event_map, # type: ignore
@@ -99,5 +105,5 @@ class EventMediator:
99
105
  middleware_chain=middleware_chain, # type: ignore
100
106
  )
101
107
 
102
- async def send(self, event: E) -> None:
108
+ async def send(self, event: events.Event) -> None:
103
109
  await self._dispatcher.dispatch(event)
@@ -9,8 +9,7 @@ HandleType = typing.Callable[[Req], typing.Awaitable[Res]]
9
9
 
10
10
 
11
11
  class Middleware(typing.Protocol):
12
- async def __call__(self, request: requests.Request, handle: HandleType) -> Res:
13
- ...
12
+ async def __call__(self, request: requests.Request, handle: HandleType) -> Res: ...
14
13
 
15
14
 
16
15
  class MiddlewareChain:
@@ -2,21 +2,18 @@ import logging
2
2
  import typing
3
3
 
4
4
  from cqrs import requests, response
5
+ from cqrs.middlewares import base
5
6
 
6
7
  Req = typing.TypeVar("Req", bound=requests.Request, contravariant=True)
7
8
  Res = typing.TypeVar("Res", response.Response, None, covariant=True)
8
9
  HandleType = typing.Callable[[Req], typing.Awaitable[Res]]
9
10
 
11
+ logger = logging.getLogger("cqrs")
10
12
 
11
- class LoggingMiddleware:
12
- def __init__(
13
- self,
14
- logger: logging.Logger | None = None,
15
- ) -> None:
16
- self._logger = logger or logging.getLogger("cqrs")
17
13
 
18
- async def __call__(self, request: Req, handle: HandleType) -> Res:
19
- self._logger.debug(
14
+ class LoggingMiddleware(base.Middleware):
15
+ async def __call__(self, request: requests.Request, handle: HandleType) -> Res:
16
+ logger.debug(
20
17
  "Handle %s request",
21
18
  type(request).__name__,
22
19
  extra={
@@ -25,11 +22,13 @@ class LoggingMiddleware:
25
22
  },
26
23
  )
27
24
  resp = await handle(request)
28
- self._logger.debug(
25
+ logger.debug(
29
26
  "Request %s handled",
30
27
  type(request).__name__,
31
28
  extra={
32
- "request_json_fields": {"response": resp.model_dump(mode="json") if resp else {}},
29
+ "request_json_fields": {
30
+ "response": resp.model_dump(mode="json") if resp else {},
31
+ },
33
32
  "to_mask": True,
34
33
  },
35
34
  )
@@ -13,7 +13,10 @@ logger = logging.getLogger("cqrs")
13
13
  logger.setLevel(logging.DEBUG)
14
14
 
15
15
  SessionFactory: typing.TypeAlias = typing.Callable[[], sql_session.AsyncSession]
16
- Serializer: typing.TypeAlias = typing.Callable[[repository_protocol.Event], typing.Awaitable[typing.Dict]]
16
+ Serializer: typing.TypeAlias = typing.Callable[
17
+ [repository_protocol.Event],
18
+ typing.Awaitable[typing.Dict],
19
+ ]
17
20
 
18
21
 
19
22
  class EventProducer:
@@ -27,15 +30,27 @@ class EventProducer:
27
30
  self.repository = repository
28
31
  self.serializer = serializer
29
32
 
30
- async def periodically_task(self, batch_size: int = 100, wait_ms: int = 500) -> None:
33
+ async def periodically_task(
34
+ self,
35
+ batch_size: int = 100,
36
+ wait_ms: int = 500,
37
+ ) -> None:
31
38
  """Calls produce periodically with specified delay"""
32
39
  while True:
33
40
  await asyncio.sleep(float(wait_ms) / 1000.0)
34
41
  await self.produce_batch(batch_size)
35
42
 
36
- async def send_message(self, session: repository_protocol.Session, event: cqrs.ECSTEvent | cqrs.NotificationEvent):
43
+ async def send_message(
44
+ self,
45
+ session: object,
46
+ event: cqrs.ECSTEvent | cqrs.NotificationEvent,
47
+ ):
37
48
  try:
38
- serialized = (await self.serializer(event)) if self.serializer else event.model_dump(mode="json")
49
+ serialized = (
50
+ (await self.serializer(event))
51
+ if self.serializer
52
+ else event.model_dump(mode="json")
53
+ )
39
54
  await self.message_broker.send_message(
40
55
  broker_protocol.Message(
41
56
  message_type=event.event_type,
@@ -45,10 +60,20 @@ class EventProducer:
45
60
  ),
46
61
  )
47
62
  except Exception as e:
48
- logger.error(f"Error while producing event {event.event_id} to kafka broker: {e}")
49
- await self.repository.update_status(session, event.event_id, repository_protocol.EventStatus.NOT_PRODUCED)
63
+ logger.error(
64
+ f"Error while producing event {event.event_id} to kafka broker: {e}",
65
+ )
66
+ await self.repository.update_status(
67
+ session,
68
+ event.event_id,
69
+ repository_protocol.EventStatus.NOT_PRODUCED,
70
+ )
50
71
  else:
51
- await self.repository.update_status(session, event.event_id, repository_protocol.EventStatus.PRODUCED)
72
+ await self.repository.update_status(
73
+ session,
74
+ event.event_id,
75
+ repository_protocol.EventStatus.PRODUCED,
76
+ )
52
77
 
53
78
  async def produce_one(self, event_id: uuid.UUID) -> None:
54
79
  async with self.repository as session:
@@ -11,33 +11,45 @@ Event: typing.TypeAlias = ev.NotificationEvent | ev.ECSTEvent
11
11
  class Outbox(typing.Protocol):
12
12
  def add(self, event: Event):
13
13
  """Adds event to outbox"""
14
+ raise NotImplementedError
14
15
 
15
16
  async def save(self):
16
17
  """Commits events to the storage"""
18
+ raise NotImplementedError
17
19
 
18
20
  async def get_events(self, batch_size: int = 100) -> typing.List[Event]:
19
21
  """Returns not produced events"""
22
+ raise NotImplementedError
20
23
 
21
24
  async def get_event(self, event_id: uuid.UUID) -> Event | None:
22
25
  """Returns event by id"""
26
+ raise NotImplementedError
23
27
 
24
28
  async def mark_as_produced(self, event_id: uuid.UUID) -> None:
25
29
  """Marks event as produced"""
30
+ raise NotImplementedError
26
31
 
27
32
  async def mark_as_failure(self, event_id: uuid.UUID) -> None:
28
33
  """Marks event as not produced with failure"""
34
+ raise NotImplementedError
29
35
 
30
36
 
31
37
  class EventProducer(abc.ABC):
32
38
  @abc.abstractmethod
33
39
  async def produce_one(self, event_id: uuid.UUID) -> None:
34
40
  """Produces event to broker"""
41
+ raise NotImplementedError
35
42
 
36
43
  @abc.abstractmethod
37
44
  async def produce_batch(self, batch_size: int = 100) -> None:
38
45
  """Produces events to broker"""
46
+ raise NotImplementedError
39
47
 
40
- async def periodically_task(self, batch_size: int = 100, wait_ms: int = 500) -> None:
48
+ async def periodically_task(
49
+ self,
50
+ batch_size: int = 100,
51
+ wait_ms: int = 500,
52
+ ) -> None:
41
53
  """Calls produce periodically with specified delay"""
42
54
  while True:
43
55
  await asyncio.sleep(float(wait_ms) / 1000.0)
@@ -46,13 +46,21 @@ class OutboxModel(Base):
46
46
  autoincrement=True,
47
47
  comment="Identity",
48
48
  )
49
- event_id = sqlalchemy.Column(sqlalchemy.Uuid, nullable=False, comment="Event idempotency id")
49
+ event_id = sqlalchemy.Column(
50
+ sqlalchemy.Uuid,
51
+ nullable=False,
52
+ comment="Event idempotency id",
53
+ )
50
54
  event_id_bin = sqlalchemy.Column(
51
55
  sqlalchemy.BINARY(16),
52
56
  nullable=False,
53
57
  comment="Event idempotency id in 16 bit presentation",
54
58
  )
55
- event_type = sqlalchemy.Column(sqlalchemy.Enum(EventType), nullable=False, comment="Event type")
59
+ event_type = sqlalchemy.Column(
60
+ sqlalchemy.Enum(EventType),
61
+ nullable=False,
62
+ comment="Event type",
63
+ )
56
64
  event_status = sqlalchemy.Column(
57
65
  sqlalchemy.Enum(repository.EventStatus),
58
66
  nullable=False,
@@ -65,7 +73,11 @@ class OutboxModel(Base):
65
73
  default=0,
66
74
  comment="Event producing flush counter",
67
75
  )
68
- event_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, comment="Event name")
76
+ event_name = sqlalchemy.Column(
77
+ sqlalchemy.String(255),
78
+ nullable=False,
79
+ comment="Event name",
80
+ )
69
81
 
70
82
  created_at = sqlalchemy.Column(
71
83
  sqlalchemy.DateTime,
@@ -73,10 +85,17 @@ class OutboxModel(Base):
73
85
  server_default=func.now(),
74
86
  comment="Event creation timestamp",
75
87
  )
76
- payload = sqlalchemy.Column(mysql.BLOB, nullable=False, default={}, comment="Event payload")
88
+ payload = sqlalchemy.Column(
89
+ mysql.BLOB,
90
+ nullable=False,
91
+ default={},
92
+ comment="Event payload",
93
+ )
77
94
 
78
95
  def row_to_dict(self) -> typing.Dict[typing.Text, typing.Any]:
79
- return {column.name: getattr(self, column.name) for column in self.__table__.columns}
96
+ return {
97
+ column.name: getattr(self, column.name) for column in self.__table__.columns
98
+ }
80
99
 
81
100
  @classmethod
82
101
  def get_batch_query(cls, size: int) -> sqlalchemy.Select:
@@ -85,7 +104,12 @@ class OutboxModel(Base):
85
104
  .select_from(cls)
86
105
  .where(
87
106
  sqlalchemy.and_(
88
- cls.event_status.in_([repository.EventStatus.NEW, repository.EventStatus.NOT_PRODUCED]),
107
+ cls.event_status.in_(
108
+ [
109
+ repository.EventStatus.NEW,
110
+ repository.EventStatus.NOT_PRODUCED,
111
+ ],
112
+ ),
89
113
  cls.flush_counter < MAX_FLUSH_COUNTER_VALUE,
90
114
  ),
91
115
  )
@@ -103,7 +127,12 @@ class OutboxModel(Base):
103
127
  .where(
104
128
  sqlalchemy.and_(
105
129
  cls.event_id_bin == func.UUID_TO_BIN(event_id),
106
- cls.event_status.in_([repository.EventStatus.NEW, repository.EventStatus.NOT_PRODUCED]),
130
+ cls.event_status.in_(
131
+ [
132
+ repository.EventStatus.NEW,
133
+ repository.EventStatus.NOT_PRODUCED,
134
+ ],
135
+ ),
107
136
  cls.flush_counter < MAX_FLUSH_COUNTER_VALUE,
108
137
  ),
109
138
  )
@@ -116,19 +145,23 @@ class OutboxModel(Base):
116
145
  def update_status_query(
117
146
  cls,
118
147
  event_id: uuid.UUID,
119
- status: typing.Literal[
120
- repository.EventStatus.PRODUCED,
121
- repository.EventStatus.NOT_PRODUCED,
122
- ],
148
+ status: repository.EventStatus,
123
149
  ) -> sqlalchemy.Update:
124
- values = {"event_status": status}
150
+ values = {
151
+ "event_status": status,
152
+ "flush_counter": cls.flush_counter,
153
+ }
125
154
  if status == repository.EventStatus.NOT_PRODUCED:
126
- values["flush_counter"] = cls.flush_counter + 1
155
+ values["flush_counter"] += 1
127
156
 
128
- return sqlalchemy.update(cls).where(cls.event_id_bin == func.UUID_TO_BIN(event_id)).values(**values)
157
+ return (
158
+ sqlalchemy.update(cls)
159
+ .where(cls.event_id_bin == func.UUID_TO_BIN(event_id))
160
+ .values(**values)
161
+ )
129
162
 
130
163
  @classmethod
131
- def status_sorting_case(cls) -> sqlalchemy.case:
164
+ def status_sorting_case(cls) -> sqlalchemy.Case:
132
165
  return sqlalchemy.case(
133
166
  {
134
167
  repository.EventStatus.NEW: 1,
@@ -140,8 +173,12 @@ class OutboxModel(Base):
140
173
  )
141
174
 
142
175
 
143
- class SqlAlchemyOutboxedEventRepository(repository.OutboxedEventRepository[sql_session.AsyncSession]):
144
- EVENT_CLASS_MAPPING: typing.ClassVar[typing.Dict[EventType, typing.Type[repository.Event]]] = {
176
+ class SqlAlchemyOutboxedEventRepository(
177
+ repository.OutboxedEventRepository[sql_session.AsyncSession],
178
+ ):
179
+ EVENT_CLASS_MAPPING: typing.ClassVar[
180
+ typing.Dict[EventType, typing.Type[repository.Event]]
181
+ ] = {
145
182
  EventType.NOTIFICATION_EVENT: ev.NotificationEvent,
146
183
  EventType.ECST_EVENT: ev.ECSTEvent,
147
184
  }
@@ -164,26 +201,42 @@ class SqlAlchemyOutboxedEventRepository(repository.OutboxedEventRepository[sql_s
164
201
  def _process_events(self, model: OutboxModel) -> repository.Event:
165
202
  event_dict = model.row_to_dict()
166
203
  event_dict["payload"] = orjson.loads(
167
- self._compressor.decompress(event_dict["payload"]) if self._compressor else event_dict["payload"],
204
+ self._compressor.decompress(event_dict["payload"])
205
+ if self._compressor
206
+ else event_dict["payload"],
207
+ )
208
+ return self.EVENT_CLASS_MAPPING[event_dict["event_type"]].model_validate(
209
+ event_dict,
168
210
  )
169
- return self.EVENT_CLASS_MAPPING[event_dict["event_type"]].model_validate(event_dict)
170
211
 
171
- async def get_many(self, session: sql_session.AsyncSession, batch_size: int = 100) -> typing.List[repository.Event]:
212
+ async def get_many(
213
+ self,
214
+ session: sql_session.AsyncSession,
215
+ batch_size: int = 100,
216
+ ) -> typing.List[repository.Event]:
172
217
  events: typing.Sequence[OutboxModel] = (
173
- (await session.execute(OutboxModel.get_batch_query(batch_size))).scalars().all()
218
+ (await session.execute(OutboxModel.get_batch_query(batch_size)))
219
+ .scalars()
220
+ .all()
174
221
  )
175
222
 
176
223
  tasks = []
177
224
  for event in events:
178
- if not self.EVENT_CLASS_MAPPING.get(event.event_type):
225
+ if not self.EVENT_CLASS_MAPPING.get(EventType(event.event_type)):
179
226
  logger.warning(f"Unknown event type for {event}")
180
227
  continue
181
228
  tasks.append(asyncio.to_thread(self._process_events, event))
182
229
 
183
230
  return await asyncio.gather(*tasks) # noqa
184
231
 
185
- async def get_one(self, session: sql_session.AsyncSession, event_id: uuid.UUID) -> repository.Event | None:
186
- event: OutboxModel | None = (await session.execute(OutboxModel.get_event_query(event_id))).scalar()
232
+ async def get_one(
233
+ self,
234
+ session: sql_session.AsyncSession,
235
+ event_id: uuid.UUID,
236
+ ) -> repository.Event | None:
237
+ event: OutboxModel | None = (
238
+ await session.execute(OutboxModel.get_event_query(event_id))
239
+ ).scalar()
187
240
 
188
241
  if event is None:
189
242
  return
@@ -194,7 +247,9 @@ class SqlAlchemyOutboxedEventRepository(repository.OutboxedEventRepository[sql_s
194
247
 
195
248
  event_dict = event.row_to_dict()
196
249
  event_dict["payload"] = orjson.loads(
197
- self._compressor.decompress(event_dict["payload"]) if self._compressor else event_dict["payload"],
250
+ self._compressor.decompress(event_dict["payload"])
251
+ if self._compressor
252
+ else event_dict["payload"],
198
253
  )
199
254
 
200
255
  return self.EVENT_CLASS_MAPPING[event.event_type].model_validate(event_dict)
@@ -205,7 +260,9 @@ class SqlAlchemyOutboxedEventRepository(repository.OutboxedEventRepository[sql_s
205
260
  event_id: uuid.UUID,
206
261
  new_status: repository.EventStatus,
207
262
  ) -> None:
208
- await session.execute(OutboxModel.update_status_query(event_id, new_status))
263
+ await session.execute(
264
+ statement=OutboxModel.update_status_query(event_id, new_status),
265
+ )
209
266
 
210
267
  async def commit(self, session: sql_session.AsyncSession):
211
268
  await session.commit()
@@ -218,6 +275,7 @@ class SqlAlchemyOutboxedEventRepository(repository.OutboxedEventRepository[sql_s
218
275
  return self.session
219
276
 
220
277
  async def __aexit__(self, exc_type, exc_val, exc_tb):
221
- await self.session.rollback()
222
- await self.session.close()
223
- self.session = None
278
+ if self.session:
279
+ await self.session.rollback()
280
+ await self.session.close()
281
+ self.session = None
@@ -6,16 +6,15 @@ import cqrs
6
6
  from cqrs import events, requests
7
7
  from cqrs.container import di as ed_di_container
8
8
  from cqrs.message_brokers import devnull, protocol
9
- from cqrs.middlewares import base as mediator_middlewares
10
- from cqrs.middlewares import logging as logging_middleware
9
+ from cqrs.middlewares import base as mediator_middlewares, logging as logging_middleware
11
10
 
12
11
  DEFAULT_MESSAGE_BROKER = devnull.DevnullMessageBroker()
13
12
 
14
13
 
15
14
  def setup_event_emitter(
16
- container: ed_di_container.DIContainer | None,
17
- domain_events_mapper: typing.Callable[[events.EventMap], None] = None,
18
- message_broker: protocol.MessageBroker = None,
15
+ container: ed_di_container.DIContainer,
16
+ domain_events_mapper: typing.Callable[[events.EventMap], None] | None = None,
17
+ message_broker: protocol.MessageBroker | None = None,
19
18
  ):
20
19
  if message_broker is None:
21
20
  message_broker = DEFAULT_MESSAGE_BROKER
@@ -33,12 +32,11 @@ def setup_event_emitter(
33
32
 
34
33
  def setup_mediator(
35
34
  event_emitter: events.EventEmitter,
36
- container: ed_di_container.DIContainer | None,
37
- middlewares: typing.Iterable[mediator_middlewares.Middleware] | None = None,
38
- commands_mapper: typing.Callable[[requests.RequestMap], None] = None,
39
- queries_mapper: typing.Callable[[requests.RequestMap], None] = None,
35
+ container: ed_di_container.DIContainer,
36
+ middlewares: typing.Iterable[mediator_middlewares.Middleware],
37
+ commands_mapper: typing.Callable[[requests.RequestMap], None] | None = None,
38
+ queries_mapper: typing.Callable[[requests.RequestMap], None] | None = None,
40
39
  ) -> cqrs.RequestMediator:
41
-
42
40
  requests_mapper = requests.RequestMap()
43
41
  if commands_mapper:
44
42
  commands_mapper(requests_mapper)
@@ -46,8 +44,6 @@ def setup_mediator(
46
44
  queries_mapper(requests_mapper)
47
45
 
48
46
  middleware_chain = mediator_middlewares.MiddlewareChain()
49
- if middlewares is None:
50
- middlewares = []
51
47
 
52
48
  for middleware in middlewares:
53
49
  middleware_chain.add(middleware)
@@ -61,12 +57,12 @@ def setup_mediator(
61
57
 
62
58
 
63
59
  def bootstrap(
60
+ di_container: di.Container,
64
61
  message_broker: protocol.MessageBroker | None = None,
65
- di_container: di.Container | None = None,
66
- middlewares: typing.Iterable[mediator_middlewares.Middleware] | None = None,
67
- commands_mapper: typing.Callable[[requests.RequestMap], None] = None,
68
- domain_events_mapper: typing.Callable[[events.EventMap], None] = None,
69
- queries_mapper: typing.Callable[[requests.RequestMap], None] = None,
62
+ middlewares: typing.Sequence[mediator_middlewares.Middleware] | None = None,
63
+ commands_mapper: typing.Callable[[requests.RequestMap], None] | None = None,
64
+ domain_events_mapper: typing.Callable[[events.EventMap], None] | None = None,
65
+ queries_mapper: typing.Callable[[requests.RequestMap], None] | None = None,
70
66
  on_startup: typing.List[typing.Callable[[], None]] | None = None,
71
67
  ) -> cqrs.RequestMediator:
72
68
  if message_broker is None:
@@ -77,18 +73,20 @@ def bootstrap(
77
73
  for fun in on_startup:
78
74
  fun()
79
75
 
80
- if middlewares is None:
81
- middlewares = []
82
76
  container = ed_di_container.DIContainer(di_container)
77
+
83
78
  event_emitter = setup_event_emitter(
84
79
  container,
85
80
  domain_events_mapper,
86
81
  message_broker,
87
82
  )
83
+ middlewares_list: typing.List[mediator_middlewares.Middleware] = list(
84
+ middlewares or [],
85
+ )
88
86
  return setup_mediator(
89
87
  event_emitter,
90
88
  container,
91
- middlewares=middlewares + [logging_middleware.LoggingMiddleware()],
89
+ middlewares=middlewares_list + [logging_middleware.LoggingMiddleware()],
92
90
  commands_mapper=commands_mapper,
93
91
  queries_mapper=queries_mapper,
94
92
  )
@@ -0,0 +1,27 @@
1
+ import typing
2
+
3
+ from cqrs import response
4
+ from cqrs.requests import request, request_handler
5
+
6
+ _KT = typing.TypeVar("_KT", bound=typing.Type[request.Request])
7
+ _VT = typing.TypeVar(
8
+ "_VT",
9
+ bound=typing.Type[
10
+ request_handler.RequestHandler[request.Request, response.Response]
11
+ ],
12
+ )
13
+
14
+
15
+ class RequestMap(typing.Dict[_KT, _VT]):
16
+ _registry: typing.Dict[_KT, _VT]
17
+
18
+ def bind(self, request_type: _KT, handler_type: _VT) -> None:
19
+ self[request_type] = handler_type
20
+
21
+ def __setitem__(self, __key: _KT, __value: _VT) -> None:
22
+ if __key in self:
23
+ raise KeyError(f"{__key} already exists in registry")
24
+ super().__setitem__(__key, __value)
25
+
26
+ def __delitem__(self, __key):
27
+ raise TypeError(f"{self.__class__.__name__} has no delete method")
@@ -1,7 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-cqrs
3
- Version: 0.0.2
4
- Author: Vadim Kozyrevskiy
3
+ Version: 0.0.5
4
+ Summary: Python CQRS pattern implementation
5
+ Author: Nikita Kunov
6
+ Author-email: Vadim Kozyrevskiy <vadikko2@mail.ru>, Dmitriy Kutlubaev <kutlubaev00@mail.ru>
5
7
  Description-Content-Type: text/markdown
6
8
  License-File: LICENSE
7
9
  Requires-Dist: pydantic==2.*
@@ -10,22 +12,19 @@ Requires-Dist: aio-pika==9.3.0
10
12
  Requires-Dist: di[anyio]==0.79.2
11
13
  Requires-Dist: sqlalchemy[asyncio]==2.0.*
12
14
  Requires-Dist: retry-async==0.1.4
13
- Provides-Extra: git
14
- Requires-Dist: pre-commit==3.8.0; extra == "git"
15
+ Provides-Extra: dev
16
+ Requires-Dist: pre-commit==3.8.0; extra == "dev"
17
+ Requires-Dist: pyright==1.1.377; extra == "dev"
18
+ Requires-Dist: ruff==0.6.2; extra == "dev"
19
+ Requires-Dist: aiokafka==0.10.0; extra == "dev"
20
+ Requires-Dist: pytest~=7.4.2; extra == "dev"
21
+ Requires-Dist: pytest-asyncio~=0.21.1; extra == "dev"
22
+ Requires-Dist: pytest-env==0.6.2; extra == "dev"
23
+ Requires-Dist: python-dotenv==1.0.1; extra == "dev"
24
+ Requires-Dist: cryptography==42.0.2; extra == "dev"
25
+ Requires-Dist: asyncmy==0.2.9; extra == "dev"
15
26
  Provides-Extra: kafka
16
27
  Requires-Dist: aiokafka==0.10.0; extra == "kafka"
17
- Provides-Extra: lint
18
- Requires-Dist: flake8==7.0.0; extra == "lint"
19
- Requires-Dist: flake8-pytest; extra == "lint"
20
- Requires-Dist: black; extra == "lint"
21
- Provides-Extra: tests
22
- Requires-Dist: aiokafka==0.10.0; extra == "tests"
23
- Requires-Dist: pytest~=7.4.2; extra == "tests"
24
- Requires-Dist: pytest-asyncio~=0.21.1; extra == "tests"
25
- Requires-Dist: pytest-env==0.6.2; extra == "tests"
26
- Requires-Dist: python-dotenv==1.0.1; extra == "tests"
27
- Requires-Dist: cryptography==42.0.2; extra == "tests"
28
- Requires-Dist: asyncmy==0.2.9; extra == "tests"
29
28
 
30
29
  # CQRS
31
30
 
@@ -3,7 +3,6 @@ README.md
3
3
  pyproject.toml
4
4
  src/cqrs/__init__.py
5
5
  src/cqrs/mediator.py
6
- src/cqrs/registry.py
7
6
  src/cqrs/response.py
8
7
  src/cqrs/adapters/__init__.py
9
8
  src/cqrs/adapters/amqp.py
@@ -5,18 +5,10 @@ di[anyio]==0.79.2
5
5
  sqlalchemy[asyncio]==2.0.*
6
6
  retry-async==0.1.4
7
7
 
8
- [git]
8
+ [dev]
9
9
  pre-commit==3.8.0
10
-
11
- [kafka]
12
- aiokafka==0.10.0
13
-
14
- [lint]
15
- flake8==7.0.0
16
- flake8-pytest
17
- black
18
-
19
- [tests]
10
+ pyright==1.1.377
11
+ ruff==0.6.2
20
12
  aiokafka==0.10.0
21
13
  pytest~=7.4.2
22
14
  pytest-asyncio~=0.21.1
@@ -24,3 +16,6 @@ pytest-env==0.6.2
24
16
  python-dotenv==1.0.1
25
17
  cryptography==42.0.2
26
18
  asyncmy==0.2.9
19
+
20
+ [kafka]
21
+ aiokafka==0.10.0
@@ -1,19 +0,0 @@
1
- import typing
2
-
3
- import di
4
- from di import dependent, executors
5
-
6
- from cqrs import container
7
-
8
- T = typing.TypeVar("T")
9
-
10
-
11
- class DIContainer(container.Container[di.Container]):
12
- def __init__(self, external_container: di.Container | None = None) -> None:
13
- self._external_container = external_container
14
-
15
- async def resolve(self, type_: typing.Type[T]) -> T:
16
- executor = executors.AsyncExecutor()
17
- solved = self._external_container.solve(dependent.Dependent(type_, scope="request"), scopes=["request"])
18
- with self._external_container.enter_scope("request") as state:
19
- return await solved.execute_async(executor=executor, state=state)
@@ -1,27 +0,0 @@
1
- import collections
2
- import typing
3
-
4
- from cqrs import registry
5
- from cqrs.events import event, event_handler
6
-
7
- E = typing.TypeVar("E", bound=event.Event, contravariant=True)
8
-
9
-
10
- class EventMap(registry.InMemoryRegistry[typing.Type[E]], typing.List[typing.Type[event_handler.EventHandler]]):
11
- _registry: collections.defaultdict
12
-
13
- def __init__(self) -> None:
14
- super().__init__()
15
- self._registry = collections.defaultdict(list)
16
-
17
- def bind(self, event_type: typing.Type[E], handler_type: typing.Type[event_handler.EventHandler[E]]) -> None:
18
- self[event_type].append(handler_type)
19
-
20
- def get(self, event_type: typing.Type[E]) -> typing.List[typing.Type[event_handler.EventHandler[E]]]:
21
- return self._registry[event_type]
22
-
23
- def get_events(self) -> list[typing.Type[E]]:
24
- return list(self.keys())
25
-
26
- def __str__(self) -> str:
27
- return str(self._registry)
@@ -1,29 +0,0 @@
1
- import typing
2
- from collections import abc
3
-
4
- _VT = typing.TypeVar("_VT")
5
- _KT = typing.TypeVar("_KT")
6
-
7
-
8
- class InMemoryRegistry(abc.MutableMapping[_KT, _VT]):
9
- _registry: typing.Dict[_KT, _VT]
10
-
11
- def __init__(self):
12
- self._registry = dict()
13
-
14
- def __setitem__(self, __key: _KT, __value: _VT) -> None:
15
- if __key in self._registry:
16
- raise KeyError(f"{__key} already exists in registry")
17
- self._registry[__key] = __value
18
-
19
- def __delitem__(self, __key):
20
- raise TypeError(f"{self.__class__.__name__} has no delete method")
21
-
22
- def __getitem__(self, __key: _KT) -> _VT:
23
- return self._registry[__key]
24
-
25
- def __len__(self):
26
- return len(self._registry.keys())
27
-
28
- def __iter__(self):
29
- return iter(self._registry.keys())
@@ -1,30 +0,0 @@
1
- import typing
2
-
3
- from cqrs import registry
4
- from cqrs.requests import request, request_handler
5
-
6
- TReq = typing.TypeVar("TReq", bound=typing.Type[request.Request], contravariant=True)
7
- TH = typing.TypeVar("TH", bound=typing.Type[request_handler.RequestHandler], contravariant=True)
8
-
9
-
10
- class RequestMap(registry.InMemoryRegistry[TReq, TH]):
11
- def bind(
12
- self,
13
- request_type: TReq,
14
- handler_type: TH,
15
- ) -> None:
16
- self._registry[request_type] = handler_type
17
-
18
- def get(self, request_type: TReq) -> TH:
19
- handler_type = self._registry.get(request_type)
20
- if not handler_type:
21
- raise RequestHandlerDoesNotExist("RequestHandler not found matching Request type.")
22
-
23
- return handler_type
24
-
25
- def __str__(self) -> str:
26
- return str(self._registry)
27
-
28
-
29
- class RequestHandlerDoesNotExist(Exception):
30
- ...
File without changes
File without changes
File without changes