python-cq 0.14.1__tar.gz → 0.15.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {python_cq-0.14.1 → python_cq-0.15.0}/PKG-INFO +1 -1
  2. {python_cq-0.14.1 → python_cq-0.15.0}/cq/_core/dispatcher/base.py +9 -1
  3. {python_cq-0.14.1 → python_cq-0.15.0}/cq/_core/dispatcher/bus.py +8 -5
  4. {python_cq-0.14.1 → python_cq-0.15.0}/cq/_core/handler.py +60 -39
  5. {python_cq-0.14.1 → python_cq-0.15.0}/cq/middlewares/retry.py +1 -1
  6. {python_cq-0.14.1 → python_cq-0.15.0}/cq/middlewares/scope.py +1 -1
  7. {python_cq-0.14.1 → python_cq-0.15.0}/pyproject.toml +1 -1
  8. {python_cq-0.14.1 → python_cq-0.15.0}/.gitignore +0 -0
  9. {python_cq-0.14.1 → python_cq-0.15.0}/LICENSE +0 -0
  10. {python_cq-0.14.1 → python_cq-0.15.0}/cq/__init__.py +0 -0
  11. {python_cq-0.14.1 → python_cq-0.15.0}/cq/_core/__init__.py +0 -0
  12. {python_cq-0.14.1 → python_cq-0.15.0}/cq/_core/common/__init__.py +0 -0
  13. {python_cq-0.14.1 → python_cq-0.15.0}/cq/_core/common/typing.py +0 -0
  14. {python_cq-0.14.1 → python_cq-0.15.0}/cq/_core/dispatcher/__init__.py +0 -0
  15. {python_cq-0.14.1 → python_cq-0.15.0}/cq/_core/dispatcher/lazy.py +0 -0
  16. {python_cq-0.14.1 → python_cq-0.15.0}/cq/_core/dispatcher/pipe.py +0 -0
  17. {python_cq-0.14.1 → python_cq-0.15.0}/cq/_core/message.py +0 -0
  18. {python_cq-0.14.1 → python_cq-0.15.0}/cq/_core/middleware.py +0 -0
  19. {python_cq-0.14.1 → python_cq-0.15.0}/cq/_core/pipetools.py +0 -0
  20. {python_cq-0.14.1 → python_cq-0.15.0}/cq/_core/related_events.py +0 -0
  21. {python_cq-0.14.1 → python_cq-0.15.0}/cq/_core/scope.py +0 -0
  22. {python_cq-0.14.1 → python_cq-0.15.0}/cq/exceptions.py +0 -0
  23. {python_cq-0.14.1 → python_cq-0.15.0}/cq/ext/__init__.py +0 -0
  24. {python_cq-0.14.1 → python_cq-0.15.0}/cq/ext/fastapi.py +0 -0
  25. {python_cq-0.14.1 → python_cq-0.15.0}/cq/middlewares/__init__.py +0 -0
  26. {python_cq-0.14.1 → python_cq-0.15.0}/cq/py.typed +0 -0
  27. {python_cq-0.14.1 → python_cq-0.15.0}/docs/index.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-cq
3
- Version: 0.14.1
3
+ Version: 0.15.0
4
4
  Summary: CQRS library for async Python projects.
5
5
  Project-URL: Documentation, https://python-cq.remimd.dev
6
6
  Project-URL: Repository, https://github.com/100nm/python-cq
@@ -1,5 +1,6 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from collections.abc import Awaitable, Callable
3
+ from contextlib import AsyncExitStack, suppress
3
4
  from typing import Protocol, Self, runtime_checkable
4
5
 
5
6
  from cq._core.middleware import Middleware, MiddlewareGroup
@@ -40,5 +41,12 @@ class BaseDispatcher[I, O](Dispatcher[I, O], ABC):
40
41
  handler: Callable[[I], Awaitable[O]],
41
42
  input_value: I,
42
43
  /,
44
+ fail_silently: bool = False,
43
45
  ) -> O:
44
- return await self.__middleware_group.invoke(handler, input_value)
46
+ async with AsyncExitStack() as stack:
47
+ if fail_silently:
48
+ stack.enter_context(suppress(Exception))
49
+
50
+ return await self.__middleware_group.invoke(handler, input_value)
51
+
52
+ return NotImplemented
@@ -7,6 +7,7 @@ from anyio.abc import TaskGroup
7
7
 
8
8
  from cq._core.dispatcher.base import BaseDispatcher, Dispatcher
9
9
  from cq._core.handler import (
10
+ HandleFunction,
10
11
  HandlerFactory,
11
12
  HandlerRegistry,
12
13
  MultipleHandlerRegistry,
@@ -53,10 +54,7 @@ class BaseBus[I, O](BaseDispatcher[I, O], Bus[I, O], ABC):
53
54
  self.__registry.subscribe(input_type, factory)
54
55
  return self
55
56
 
56
- def _handlers_from(
57
- self,
58
- input_type: type[I],
59
- ) -> Iterator[Callable[[I], Awaitable[O]]]:
57
+ def _handlers_from(self, input_type: type[I]) -> Iterator[HandleFunction[[I], O]]:
60
58
  return self.__registry.handlers_from(input_type)
61
59
 
62
60
  def _trigger_listeners(self, input_value: I, /, task_group: TaskGroup) -> None:
@@ -75,7 +73,11 @@ class SimpleBus[I, O](BaseBus[I, O]):
75
73
  self._trigger_listeners(input_value, task_group)
76
74
 
77
75
  for handler in self._handlers_from(type(input_value)):
78
- return await self._invoke_with_middlewares(handler, input_value)
76
+ return await self._invoke_with_middlewares(
77
+ handler,
78
+ input_value,
79
+ handler.fail_silently,
80
+ )
79
81
 
80
82
  return NotImplemented
81
83
 
@@ -95,4 +97,5 @@ class TaskBus[I](BaseBus[I, None]):
95
97
  self._invoke_with_middlewares,
96
98
  handler,
97
99
  input_value,
100
+ handler.fail_silently,
98
101
  )
@@ -21,74 +21,96 @@ class Handler[**P, T](Protocol):
21
21
  __slots__ = ()
22
22
 
23
23
  @abstractmethod
24
- async def handle(self, *args: P.args, **kwargs: P.kwargs) -> T:
24
+ async def handle(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
25
25
  raise NotImplementedError
26
26
 
27
27
 
28
+ @dataclass(repr=False, eq=False, frozen=True, slots=True)
29
+ class HandleFunction[**P, T]:
30
+ handler_factory: HandlerFactory[P, T]
31
+ handler_type: HandlerType[P, T] | None = field(default=None)
32
+ fail_silently: bool = field(default=False)
33
+
34
+ async def __call__(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
35
+ handler = await self.handler_factory()
36
+ return await handler.handle(*args, **kwargs)
37
+
38
+
28
39
  @runtime_checkable
29
40
  class HandlerRegistry[I, O](Protocol):
30
41
  __slots__ = ()
31
42
 
32
43
  @abstractmethod
33
- def handlers_from(
34
- self,
35
- input_type: type[I],
36
- ) -> Iterator[Callable[[I], Awaitable[O]]]:
44
+ def handlers_from(self, input_type: type[I]) -> Iterator[HandleFunction[[I], O]]:
37
45
  raise NotImplementedError
38
46
 
39
47
  @abstractmethod
40
- def subscribe(self, input_type: type[I], factory: HandlerFactory[[I], O]) -> Self:
48
+ def subscribe(
49
+ self,
50
+ input_type: type[I],
51
+ handler_factory: HandlerFactory[[I], O],
52
+ handler_type: HandlerType[[I], O] | None = ...,
53
+ fail_silently: bool = ...,
54
+ ) -> Self:
41
55
  raise NotImplementedError
42
56
 
43
57
 
44
58
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
45
59
  class MultipleHandlerRegistry[I, O](HandlerRegistry[I, O]):
46
- __factories: dict[type[I], list[HandlerFactory[[I], O]]] = field(
60
+ __values: dict[type[I], list[HandleFunction[[I], O]]] = field(
47
61
  default_factory=partial(defaultdict, list),
48
62
  init=False,
49
63
  )
50
64
 
51
- def handlers_from(
65
+ def handlers_from(self, input_type: type[I]) -> Iterator[HandleFunction[[I], O]]:
66
+ for key_type in _iter_key_types(input_type):
67
+ yield from self.__values.get(key_type, ())
68
+
69
+ def subscribe(
52
70
  self,
53
71
  input_type: type[I],
54
- ) -> Iterator[Callable[[I], Awaitable[O]]]:
55
- for key_type in _iter_key_types(input_type):
56
- for factory in self.__factories.get(key_type, ()):
57
- yield _make_handle_function(factory)
72
+ handler_factory: HandlerFactory[[I], O],
73
+ handler_type: HandlerType[[I], O] | None = None,
74
+ fail_silently: bool = False,
75
+ ) -> Self:
76
+ function = HandleFunction(handler_factory, handler_type, fail_silently)
58
77
 
59
- def subscribe(self, input_type: type[I], factory: HandlerFactory[[I], O]) -> Self:
60
78
  for key_type in _build_key_types(input_type):
61
- self.__factories[key_type].append(factory)
79
+ self.__values[key_type].append(function)
62
80
 
63
81
  return self
64
82
 
65
83
 
66
84
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
67
85
  class SingleHandlerRegistry[I, O](HandlerRegistry[I, O]):
68
- __factories: dict[type[I], HandlerFactory[[I], O]] = field(
86
+ __values: dict[type[I], HandleFunction[[I], O]] = field(
69
87
  default_factory=dict,
70
88
  init=False,
71
89
  )
72
90
 
73
- def handlers_from(
74
- self,
75
- input_type: type[I],
76
- ) -> Iterator[Callable[[I], Awaitable[O]]]:
91
+ def handlers_from(self, input_type: type[I]) -> Iterator[HandleFunction[[I], O]]:
77
92
  for key_type in _iter_key_types(input_type):
78
- factory = self.__factories.get(key_type, None)
79
- if factory is not None:
80
- yield _make_handle_function(factory)
93
+ function = self.__values.get(key_type, None)
94
+ if function is not None:
95
+ yield function
81
96
 
82
- def subscribe(self, input_type: type[I], factory: HandlerFactory[[I], O]) -> Self:
83
- entries = {key_type: factory for key_type in _build_key_types(input_type)}
97
+ def subscribe(
98
+ self,
99
+ input_type: type[I],
100
+ handler_factory: HandlerFactory[[I], O],
101
+ handler_type: HandlerType[[I], O] | None = None,
102
+ fail_silently: bool = False,
103
+ ) -> Self:
104
+ function = HandleFunction(handler_factory, handler_type, fail_silently)
105
+ entries = {key_type: function for key_type in _build_key_types(input_type)}
84
106
 
85
107
  for key_type in entries:
86
- if key_type in self.__factories:
108
+ if key_type in self.__values:
87
109
  raise RuntimeError(
88
110
  f"A handler is already registered for the input type: `{key_type}`."
89
111
  )
90
112
 
91
- self.__factories.update(entries)
113
+ self.__values.update(entries)
92
114
  return self
93
115
 
94
116
 
@@ -105,6 +127,7 @@ class HandlerDecorator[I, O]:
105
127
  input_or_handler_type: type[I],
106
128
  /,
107
129
  *,
130
+ fail_silently: bool = ...,
108
131
  threadsafe: bool | None = ...,
109
132
  ) -> Decorator: ...
110
133
 
@@ -114,6 +137,7 @@ class HandlerDecorator[I, O]:
114
137
  input_or_handler_type: T,
115
138
  /,
116
139
  *,
140
+ fail_silently: bool = ...,
117
141
  threadsafe: bool | None = ...,
118
142
  ) -> T: ...
119
143
 
@@ -123,6 +147,7 @@ class HandlerDecorator[I, O]:
123
147
  input_or_handler_type: None = ...,
124
148
  /,
125
149
  *,
150
+ fail_silently: bool = ...,
126
151
  threadsafe: bool | None = ...,
127
152
  ) -> Decorator: ...
128
153
 
@@ -131,6 +156,7 @@ class HandlerDecorator[I, O]:
131
156
  input_or_handler_type: type[I] | T | None = None,
132
157
  /,
133
158
  *,
159
+ fail_silently: bool = False,
134
160
  threadsafe: bool | None = None,
135
161
  ) -> Any:
136
162
  if (
@@ -138,11 +164,16 @@ class HandlerDecorator[I, O]:
138
164
  and isclass(input_or_handler_type)
139
165
  and issubclass(input_or_handler_type, Handler)
140
166
  ):
141
- return self.__decorator(input_or_handler_type, threadsafe=threadsafe)
167
+ return self.__decorator(
168
+ input_or_handler_type,
169
+ fail_silently=fail_silently,
170
+ threadsafe=threadsafe,
171
+ )
142
172
 
143
173
  return partial(
144
174
  self.__decorator,
145
175
  input_type=input_or_handler_type, # type: ignore[arg-type]
176
+ fail_silently=fail_silently,
146
177
  threadsafe=threadsafe,
147
178
  )
148
179
 
@@ -152,11 +183,12 @@ class HandlerDecorator[I, O]:
152
183
  /,
153
184
  *,
154
185
  input_type: type[I] | None = None,
186
+ fail_silently: bool = False,
155
187
  threadsafe: bool | None = None,
156
188
  ) -> HandlerType[[I], O]:
157
189
  factory = self.injection_module.make_async_factory(wrapped, threadsafe)
158
190
  input_type = input_type or _resolve_input_type(wrapped)
159
- self.registry.subscribe(input_type, factory)
191
+ self.registry.subscribe(input_type, factory, wrapped, fail_silently)
160
192
  return wrapped
161
193
 
162
194
 
@@ -190,14 +222,3 @@ def _resolve_input_type[I, O](handler_type: HandlerType[[I], O]) -> type[I]:
190
222
  f"Unable to resolve input type for handler `{handler_type}`, "
191
223
  "`handle` method must have a type annotation for its first parameter."
192
224
  )
193
-
194
-
195
- def _make_handle_function[I, O](
196
- factory: HandlerFactory[[I], O],
197
- ) -> Callable[[I], Awaitable[O]]:
198
- return partial(__handle, factory=factory)
199
-
200
-
201
- async def __handle[I, O](input_value: I, *, factory: HandlerFactory[[I], O]) -> O:
202
- handler = await factory()
203
- return await handler.handle(input_value)
@@ -25,7 +25,7 @@ class RetryMiddleware:
25
25
  self.__exceptions = tuple(exceptions)
26
26
  self.__retry = retry
27
27
 
28
- async def __call__(self, *args: Any, **kwargs: Any) -> MiddlewareResult[Any]:
28
+ async def __call__(self, /, *args: Any, **kwargs: Any) -> MiddlewareResult[Any]:
29
29
  retry = self.__retry
30
30
 
31
31
  for attempt in range(1, retry + 1):
@@ -19,7 +19,7 @@ class InjectionScopeMiddleware:
19
19
  exist_ok: bool = field(default=False, kw_only=True)
20
20
  threadsafe: bool | None = field(default=None, kw_only=True)
21
21
 
22
- async def __call__(self, *args: Any, **kwargs: Any) -> MiddlewareResult[Any]:
22
+ async def __call__(self, /, *args: Any, **kwargs: Any) -> MiddlewareResult[Any]:
23
23
  async with AsyncExitStack() as stack:
24
24
  try:
25
25
  await stack.enter_async_context(
@@ -22,7 +22,7 @@ test = [
22
22
 
23
23
  [project]
24
24
  name = "python-cq"
25
- version = "0.14.1"
25
+ version = "0.15.0"
26
26
  description = "CQRS library for async Python projects."
27
27
  license = "MIT"
28
28
  license-files = ["LICENSE"]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes