python-cq 0.4.0__tar.gz → 0.5.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-cq
3
- Version: 0.4.0
3
+ Version: 0.5.1
4
4
  Summary: Lightweight CQRS library.
5
5
  Project-URL: Repository, https://github.com/100nm/python-cq
6
6
  Author: remimd
@@ -12,9 +12,9 @@ from ._core.message import (
12
12
  QueryBus,
13
13
  command_handler,
14
14
  event_handler,
15
- get_command_bus,
16
- get_event_bus,
17
- get_query_bus,
15
+ new_command_bus,
16
+ new_event_bus,
17
+ new_query_bus,
18
18
  query_handler,
19
19
  )
20
20
  from ._core.middleware import Middleware, MiddlewareResult
@@ -39,8 +39,8 @@ __all__ = (
39
39
  "RelatedEvents",
40
40
  "command_handler",
41
41
  "event_handler",
42
- "get_command_bus",
43
- "get_event_bus",
44
- "get_query_bus",
42
+ "new_command_bus",
43
+ "new_event_bus",
44
+ "new_query_bus",
45
45
  "query_handler",
46
46
  )
@@ -0,0 +1,93 @@
1
+ from abc import ABC, abstractmethod
2
+ from collections.abc import Awaitable, Callable, Iterator
3
+ from typing import Any, Protocol, Self, runtime_checkable
4
+
5
+ import anyio
6
+ from anyio.abc import TaskGroup
7
+
8
+ from cq._core.dispatcher.base import BaseDispatcher, Dispatcher
9
+ from cq._core.handler import (
10
+ HandlerFactory,
11
+ HandlerManager,
12
+ MultipleHandlerManager,
13
+ SingleHandlerManager,
14
+ )
15
+
16
+ type Listener[T] = Callable[[T], Awaitable[Any]]
17
+
18
+
19
+ @runtime_checkable
20
+ class Bus[I, O](Dispatcher[I, O], Protocol):
21
+ __slots__ = ()
22
+
23
+ @abstractmethod
24
+ def subscribe(self, input_type: type[I], factory: HandlerFactory[[I], O]) -> Self:
25
+ raise NotImplementedError
26
+
27
+ @abstractmethod
28
+ def add_listeners(self, *listeners: Listener[I]) -> Self:
29
+ raise NotImplementedError
30
+
31
+
32
+ class BaseBus[I, O](BaseDispatcher[I, O], Bus[I, O], ABC):
33
+ __slots__ = ("__listeners", "__manager")
34
+
35
+ __listeners: list[Listener[I]]
36
+ __manager: HandlerManager[I, O]
37
+
38
+ def __init__(self, manager: HandlerManager[I, O]) -> None:
39
+ super().__init__()
40
+ self.__listeners = []
41
+ self.__manager = manager
42
+
43
+ def add_listeners(self, *listeners: Listener[I]) -> Self:
44
+ self.__listeners.extend(listeners)
45
+ return self
46
+
47
+ def subscribe(self, input_type: type[I], factory: HandlerFactory[[I], O]) -> Self:
48
+ self.__manager.subscribe(input_type, factory)
49
+ return self
50
+
51
+ def _handlers_from(
52
+ self,
53
+ input_type: type[I],
54
+ ) -> Iterator[Callable[[I], Awaitable[O]]]:
55
+ return self.__manager.handlers_from(input_type)
56
+
57
+ def _trigger_listeners(self, input_value: I, /, task_group: TaskGroup) -> None:
58
+ for listener in self.__listeners:
59
+ task_group.start_soon(listener, input_value)
60
+
61
+
62
+ class SimpleBus[I, O](BaseBus[I, O]):
63
+ __slots__ = ()
64
+
65
+ def __init__(self, manager: HandlerManager[I, O] | None = None) -> None:
66
+ super().__init__(manager or SingleHandlerManager())
67
+
68
+ async def dispatch(self, input_value: I, /) -> O:
69
+ async with anyio.create_task_group() as task_group:
70
+ self._trigger_listeners(input_value, task_group)
71
+
72
+ for handler in self._handlers_from(type(input_value)):
73
+ return await self._invoke_with_middlewares(handler, input_value)
74
+
75
+ return NotImplemented
76
+
77
+
78
+ class TaskBus[I](BaseBus[I, None]):
79
+ __slots__ = ()
80
+
81
+ def __init__(self, manager: HandlerManager[I, None] | None = None) -> None:
82
+ super().__init__(manager or MultipleHandlerManager())
83
+
84
+ async def dispatch(self, input_value: I, /) -> None:
85
+ async with anyio.create_task_group() as task_group:
86
+ self._trigger_listeners(input_value, task_group)
87
+
88
+ for handler in self._handlers_from(type(input_value)):
89
+ task_group.start_soon(
90
+ self._invoke_with_middlewares,
91
+ handler,
92
+ input_value,
93
+ )
@@ -0,0 +1,111 @@
1
+ from abc import abstractmethod
2
+ from collections import defaultdict
3
+ from collections.abc import Awaitable, Callable, Iterator
4
+ from dataclasses import dataclass, field
5
+ from functools import partial
6
+ from inspect import getmro, isclass
7
+ from typing import Any, Protocol, Self, runtime_checkable
8
+
9
+ import injection
10
+
11
+ type HandlerType[**P, T] = type[Handler[P, T]]
12
+ type HandlerFactory[**P, T] = Callable[..., Awaitable[Handler[P, T]]]
13
+
14
+
15
+ @runtime_checkable
16
+ class Handler[**P, T](Protocol):
17
+ __slots__ = ()
18
+
19
+ @abstractmethod
20
+ async def handle(self, *args: P.args, **kwargs: P.kwargs) -> T:
21
+ raise NotImplementedError
22
+
23
+
24
+ @runtime_checkable
25
+ class HandlerManager[I, O](Protocol):
26
+ __slots__ = ()
27
+
28
+ @abstractmethod
29
+ def handlers_from(
30
+ self,
31
+ input_type: type[I],
32
+ ) -> Iterator[Callable[[I], Awaitable[O]]]:
33
+ raise NotImplementedError
34
+
35
+ @abstractmethod
36
+ def subscribe(self, input_type: type[I], factory: HandlerFactory[[I], O]) -> Self:
37
+ raise NotImplementedError
38
+
39
+
40
+ @dataclass(repr=False, eq=False, frozen=True, slots=True)
41
+ class MultipleHandlerManager[I, O](HandlerManager[I, O]):
42
+ __factories: dict[type[I], list[HandlerFactory[[I], O]]] = field(
43
+ default_factory=partial(defaultdict, list),
44
+ init=False,
45
+ )
46
+
47
+ def handlers_from(
48
+ self,
49
+ input_type: type[I],
50
+ ) -> Iterator[Callable[[I], Awaitable[O]]]:
51
+ for it in getmro(input_type):
52
+ for factory in self.__factories.get(it, ()):
53
+ yield _make_handle_function(factory)
54
+
55
+ def subscribe(self, input_type: type[I], factory: HandlerFactory[[I], O]) -> Self:
56
+ self.__factories[input_type].append(factory)
57
+ return self
58
+
59
+
60
+ @dataclass(repr=False, eq=False, frozen=True, slots=True)
61
+ class SingleHandlerManager[I, O](HandlerManager[I, O]):
62
+ __factories: dict[type[I], HandlerFactory[[I], O]] = field(
63
+ default_factory=dict,
64
+ init=False,
65
+ )
66
+
67
+ def handlers_from(
68
+ self,
69
+ input_type: type[I],
70
+ ) -> Iterator[Callable[[I], Awaitable[O]]]:
71
+ for it in getmro(input_type):
72
+ factory = self.__factories.get(it, None)
73
+ if factory is not None:
74
+ yield _make_handle_function(factory)
75
+
76
+ def subscribe(self, input_type: type[I], factory: HandlerFactory[[I], O]) -> Self:
77
+ if input_type in self.__factories:
78
+ raise RuntimeError(
79
+ f"A handler is already registered for the input type: `{input_type}`."
80
+ )
81
+
82
+ self.__factories[input_type] = factory
83
+ return self
84
+
85
+
86
+ @dataclass(repr=False, eq=False, frozen=True, slots=True)
87
+ class HandlerDecorator[I, O]:
88
+ manager: HandlerManager[I, O]
89
+ injection_module: injection.Module = field(default_factory=injection.mod)
90
+
91
+ def __call__(self, input_type: type[I], /) -> Any:
92
+ def decorator(wrapped: type[Handler[[I], O]]) -> type[Handler[[I], O]]:
93
+ if not isclass(wrapped) or not issubclass(wrapped, Handler):
94
+ raise TypeError(f"`{wrapped}` isn't a valid handler.")
95
+
96
+ factory = self.injection_module.make_async_factory(wrapped)
97
+ self.manager.subscribe(input_type, factory)
98
+ return wrapped
99
+
100
+ return decorator
101
+
102
+
103
+ def _make_handle_function[I, O](
104
+ factory: HandlerFactory[[I], O],
105
+ ) -> Callable[[I], Awaitable[O]]:
106
+ return partial(__handle, factory=factory)
107
+
108
+
109
+ async def __handle[I, O](input_value: I, factory: HandlerFactory[[I], O]) -> O:
110
+ handler = await factory()
111
+ return await handler.handle(input_value)
@@ -0,0 +1,78 @@
1
+ from abc import ABC
2
+ from typing import Any
3
+
4
+ import injection
5
+
6
+ from cq._core.dispatcher.bus import Bus, SimpleBus, TaskBus
7
+ from cq._core.dto import DTO
8
+ from cq._core.handler import (
9
+ HandlerDecorator,
10
+ MultipleHandlerManager,
11
+ SingleHandlerManager,
12
+ )
13
+ from cq._core.scope import CQScope
14
+ from cq.middlewares.scope import InjectionScopeMiddleware
15
+
16
+
17
+ class Message(DTO, ABC):
18
+ __slots__ = ()
19
+
20
+
21
+ class Command(Message, ABC):
22
+ __slots__ = ()
23
+
24
+
25
+ class Event(Message, ABC):
26
+ __slots__ = ()
27
+
28
+
29
+ class Query(Message, ABC):
30
+ __slots__ = ()
31
+
32
+
33
+ type CommandBus[T] = Bus[Command, T]
34
+ type EventBus = Bus[Event, None]
35
+ type QueryBus[T] = Bus[Query, T]
36
+
37
+ AnyCommandBus = CommandBus[Any]
38
+
39
+
40
+ command_handler: HandlerDecorator[Command, Any] = HandlerDecorator(
41
+ SingleHandlerManager(),
42
+ )
43
+ event_handler: HandlerDecorator[Event, None] = HandlerDecorator(
44
+ MultipleHandlerManager(),
45
+ )
46
+ query_handler: HandlerDecorator[Query, Any] = HandlerDecorator(
47
+ SingleHandlerManager(),
48
+ )
49
+
50
+
51
+ @injection.singleton(
52
+ on=CommandBus,
53
+ ignore_type_hint=True, # type: ignore[call-arg]
54
+ inject=False,
55
+ mode="fallback",
56
+ )
57
+ def new_command_bus[T]() -> CommandBus[T]:
58
+ bus = SimpleBus(command_handler.manager)
59
+ bus.add_middlewares(InjectionScopeMiddleware(CQScope.ON_COMMAND))
60
+ return bus
61
+
62
+
63
+ @injection.singleton(
64
+ inject=False,
65
+ mode="fallback",
66
+ )
67
+ def new_event_bus() -> EventBus:
68
+ return TaskBus(event_handler.manager)
69
+
70
+
71
+ @injection.singleton(
72
+ on=QueryBus,
73
+ ignore_type_hint=True, # type: ignore[call-arg]
74
+ inject=False,
75
+ mode="fallback",
76
+ )
77
+ def new_query_bus[T]() -> QueryBus[T]:
78
+ return SimpleBus(query_handler.manager)
@@ -19,7 +19,7 @@ class RelatedEvents(Protocol):
19
19
  raise NotImplementedError
20
20
 
21
21
 
22
- @dataclass(frozen=True, slots=True)
22
+ @dataclass(repr=False, eq=False, frozen=True, slots=True)
23
23
  class _RelatedEvents(RelatedEvents):
24
24
  items: list[Event] = field(default_factory=list)
25
25
 
@@ -27,8 +27,8 @@ class _RelatedEvents(RelatedEvents):
27
27
  self.items.extend(events)
28
28
 
29
29
 
30
- @injection.scoped(CQScope.ON_COMMAND)
31
- async def _related_events_recipe(event_bus: EventBus) -> AsyncIterator[RelatedEvents]:
30
+ @injection.scoped(CQScope.ON_COMMAND, mode="fallback")
31
+ async def related_events_recipe(event_bus: EventBus) -> AsyncIterator[RelatedEvents]:
32
32
  yield (instance := _RelatedEvents())
33
33
  events = instance.items
34
34
 
@@ -38,6 +38,3 @@ async def _related_events_recipe(event_bus: EventBus) -> AsyncIterator[RelatedEv
38
38
  async with anyio.create_task_group() as task_group:
39
39
  for event in events:
40
40
  task_group.start_soon(event_bus.dispatch, event)
41
-
42
-
43
- del _related_events_recipe
@@ -11,6 +11,10 @@ __all__ = ("RetryMiddleware",)
11
11
  class RetryMiddleware:
12
12
  __slots__ = ("__delay", "__exceptions", "__retry")
13
13
 
14
+ __delay: float
15
+ __exceptions: tuple[type[BaseException], ...]
16
+ __retry: int
17
+
14
18
  def __init__(
15
19
  self,
16
20
  retry: int,
@@ -13,6 +13,8 @@ __all__ = ("InjectionScopeMiddleware",)
13
13
  class InjectionScopeMiddleware:
14
14
  __slots__ = ("__scope_name",)
15
15
 
16
+ __scope_name: str
17
+
16
18
  def __init__(self, scope_name: str) -> None:
17
19
  self.__scope_name = scope_name
18
20
 
@@ -19,7 +19,7 @@ test = [
19
19
 
20
20
  [project]
21
21
  name = "python-cq"
22
- version = "0.4.0"
22
+ version = "0.5.1"
23
23
  description = "Lightweight CQRS library."
24
24
  license = { text = "MIT" }
25
25
  readme = "README.md"
@@ -102,5 +102,5 @@ extend-select = ["F", "I", "N"]
102
102
  fixable = ["ALL"]
103
103
 
104
104
  [tool.uv]
105
- default-groups = ["dev", "test"]
105
+ default-groups = ["dev", "example", "test"]
106
106
  package = true
@@ -1,165 +0,0 @@
1
- from abc import ABC, abstractmethod
2
- from collections import defaultdict
3
- from collections.abc import Awaitable, Callable
4
- from dataclasses import dataclass, field
5
- from inspect import getmro, isclass
6
- from types import GenericAlias
7
- from typing import Any, Protocol, Self, TypeAliasType, runtime_checkable
8
-
9
- import anyio
10
- import injection
11
-
12
- from cq._core.dispatcher.base import BaseDispatcher, Dispatcher
13
-
14
- type HandlerType[**P, T] = type[Handler[P, T]]
15
- type HandlerFactory[**P, T] = Callable[..., Awaitable[Handler[P, T]]]
16
-
17
- type Listener[T] = Callable[[T], Awaitable[Any]]
18
-
19
- type BusType[I, O] = type[Bus[I, O]]
20
-
21
-
22
- @runtime_checkable
23
- class Handler[**P, T](Protocol):
24
- __slots__ = ()
25
-
26
- @abstractmethod
27
- async def handle(self, *args: P.args, **kwargs: P.kwargs) -> T:
28
- raise NotImplementedError
29
-
30
-
31
- @runtime_checkable
32
- class Bus[I, O](Dispatcher[I, O], Protocol):
33
- __slots__ = ()
34
-
35
- @abstractmethod
36
- def subscribe(self, input_type: type[I], factory: HandlerFactory[[I], O]) -> Self:
37
- raise NotImplementedError
38
-
39
- @abstractmethod
40
- def add_listeners(self, *listeners: Listener[I]) -> Self:
41
- raise NotImplementedError
42
-
43
-
44
- @dataclass(eq=False, frozen=True, slots=True)
45
- class SubscriberDecorator[I, O]:
46
- bus_type: BusType[I, O] | TypeAliasType | GenericAlias
47
- injection_module: injection.Module = field(default_factory=injection.mod)
48
-
49
- def __call__(self, first_input_type: type[I], /, *input_types: type[I]) -> Any:
50
- def decorator(wrapped: type[Handler[[I], O]]) -> type[Handler[[I], O]]:
51
- if not isclass(wrapped) or not issubclass(wrapped, Handler):
52
- raise TypeError(f"`{wrapped}` isn't a valid handler.")
53
-
54
- bus = self.injection_module.find_instance(self.bus_type)
55
- factory = self.injection_module.make_async_factory(wrapped)
56
-
57
- for input_type in (first_input_type, *input_types):
58
- bus.subscribe(input_type, factory)
59
-
60
- return wrapped
61
-
62
- return decorator
63
-
64
-
65
- class BaseBus[I, O](BaseDispatcher[I, O], Bus[I, O], ABC):
66
- __slots__ = ("__listeners",)
67
-
68
- __listeners: list[Listener[I]]
69
-
70
- def __init__(self) -> None:
71
- super().__init__()
72
- self.__listeners = []
73
-
74
- def add_listeners(self, *listeners: Listener[I]) -> Self:
75
- self.__listeners.extend(listeners)
76
- return self
77
-
78
- async def _trigger_listeners(self, input_value: I, /) -> None:
79
- listeners = self.__listeners
80
-
81
- if not listeners:
82
- return
83
-
84
- async with anyio.create_task_group() as task_group:
85
- for listener in listeners:
86
- task_group.start_soon(listener, input_value)
87
-
88
- @staticmethod
89
- def _make_handle_function(
90
- handler_factory: HandlerFactory[[I], O],
91
- ) -> Callable[[I], Awaitable[O]]:
92
- async def handle(input_value: I) -> O:
93
- handler = await handler_factory()
94
- return await handler.handle(input_value)
95
-
96
- return handle
97
-
98
-
99
- class SimpleBus[I, O](BaseBus[I, O]):
100
- __slots__ = ("__handlers",)
101
-
102
- __handlers: dict[type[I], HandlerFactory[[I], O]]
103
-
104
- def __init__(self) -> None:
105
- super().__init__()
106
- self.__handlers = {}
107
-
108
- async def dispatch(self, input_value: I, /) -> O:
109
- await self._trigger_listeners(input_value)
110
-
111
- for input_type in getmro(type(input_value)):
112
- if handler_factory := self.__handlers.get(input_type):
113
- break
114
-
115
- else:
116
- return NotImplemented
117
-
118
- handler = self._make_handle_function(handler_factory)
119
- return await self._invoke_with_middlewares(handler, input_value)
120
-
121
- def subscribe(self, input_type: type[I], factory: HandlerFactory[[I], O]) -> Self:
122
- if input_type in self.__handlers:
123
- raise RuntimeError(
124
- f"A handler is already registered for the input type: `{input_type}`."
125
- )
126
-
127
- self.__handlers[input_type] = factory
128
- return self
129
-
130
-
131
- class TaskBus[I](BaseBus[I, None]):
132
- __slots__ = ("__handlers",)
133
-
134
- __handlers: dict[type[I], list[HandlerFactory[[I], None]]]
135
-
136
- def __init__(self) -> None:
137
- super().__init__()
138
- self.__handlers = defaultdict(list)
139
-
140
- async def dispatch(self, input_value: I, /) -> None:
141
- await self._trigger_listeners(input_value)
142
-
143
- for input_type in getmro(type(input_value)):
144
- if handler_factories := self.__handlers.get(input_type):
145
- break
146
-
147
- else:
148
- return
149
-
150
- async with anyio.create_task_group() as task_group:
151
- for handler_factory in handler_factories:
152
- handler = self._make_handle_function(handler_factory)
153
- task_group.start_soon(
154
- self._invoke_with_middlewares,
155
- handler,
156
- input_value,
157
- )
158
-
159
- def subscribe(
160
- self,
161
- input_type: type[I],
162
- factory: HandlerFactory[[I], None],
163
- ) -> Self:
164
- self.__handlers[input_type].append(factory)
165
- return self
@@ -1,73 +0,0 @@
1
- from abc import ABC
2
- from typing import Any
3
-
4
- import injection
5
-
6
- from cq._core.dispatcher.bus import Bus, SimpleBus, SubscriberDecorator, TaskBus
7
- from cq._core.dto import DTO
8
- from cq._core.scope import CQScope
9
- from cq.middlewares.scope import InjectionScopeMiddleware
10
-
11
-
12
- class Message(DTO, ABC):
13
- __slots__ = ()
14
-
15
-
16
- class Command(Message, ABC):
17
- __slots__ = ()
18
-
19
-
20
- class Event(Message, ABC):
21
- __slots__ = ()
22
-
23
-
24
- class Query(Message, ABC):
25
- __slots__ = ()
26
-
27
-
28
- type CommandBus[T] = Bus[Command, T]
29
- type EventBus = Bus[Event, None]
30
- type QueryBus[T] = Bus[Query, T]
31
-
32
- AnyCommandBus = CommandBus[Any]
33
-
34
-
35
- command_handler: SubscriberDecorator[Command, Any] = SubscriberDecorator(CommandBus)
36
- event_handler: SubscriberDecorator[Event, None] = SubscriberDecorator(EventBus)
37
- query_handler: SubscriberDecorator[Query, Any] = SubscriberDecorator(QueryBus)
38
-
39
-
40
- @injection.inject
41
- def get_command_bus[T](bus: CommandBus[T] = NotImplemented, /) -> CommandBus[T]:
42
- return bus
43
-
44
-
45
- def new_command_bus[T]() -> CommandBus[T]:
46
- bus: CommandBus[T] = SimpleBus()
47
- bus.add_middlewares(
48
- InjectionScopeMiddleware(CQScope.ON_COMMAND),
49
- )
50
- return bus
51
-
52
-
53
- @injection.inject
54
- def get_event_bus(bus: EventBus = NotImplemented, /) -> EventBus:
55
- return bus
56
-
57
-
58
- def new_event_bus() -> EventBus:
59
- return TaskBus()
60
-
61
-
62
- @injection.inject
63
- def get_query_bus[T](bus: QueryBus[T] = NotImplemented, /) -> QueryBus[T]:
64
- return bus
65
-
66
-
67
- def new_query_bus[T]() -> QueryBus[T]:
68
- return SimpleBus()
69
-
70
-
71
- injection.set_constant(new_command_bus(), CommandBus, alias=True)
72
- injection.set_constant(new_event_bus(), EventBus, alias=True)
73
- injection.set_constant(new_query_bus(), QueryBus, alias=True)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes