python-cq 0.14.1__tar.gz → 0.15.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.
Files changed (27) hide show
  1. {python_cq-0.14.1 → python_cq-0.15.1}/PKG-INFO +1 -1
  2. {python_cq-0.14.1 → python_cq-0.15.1}/cq/__init__.py +2 -1
  3. {python_cq-0.14.1 → python_cq-0.15.1}/cq/_core/dispatcher/base.py +9 -1
  4. {python_cq-0.14.1 → python_cq-0.15.1}/cq/_core/dispatcher/bus.py +21 -8
  5. {python_cq-0.14.1 → python_cq-0.15.1}/cq/_core/handler.py +70 -40
  6. {python_cq-0.14.1 → python_cq-0.15.1}/cq/_core/middleware.py +15 -1
  7. {python_cq-0.14.1 → python_cq-0.15.1}/cq/middlewares/retry.py +1 -1
  8. {python_cq-0.14.1 → python_cq-0.15.1}/cq/middlewares/scope.py +1 -1
  9. {python_cq-0.14.1 → python_cq-0.15.1}/pyproject.toml +1 -1
  10. {python_cq-0.14.1 → python_cq-0.15.1}/.gitignore +0 -0
  11. {python_cq-0.14.1 → python_cq-0.15.1}/LICENSE +0 -0
  12. {python_cq-0.14.1 → python_cq-0.15.1}/cq/_core/__init__.py +0 -0
  13. {python_cq-0.14.1 → python_cq-0.15.1}/cq/_core/common/__init__.py +0 -0
  14. {python_cq-0.14.1 → python_cq-0.15.1}/cq/_core/common/typing.py +0 -0
  15. {python_cq-0.14.1 → python_cq-0.15.1}/cq/_core/dispatcher/__init__.py +0 -0
  16. {python_cq-0.14.1 → python_cq-0.15.1}/cq/_core/dispatcher/lazy.py +0 -0
  17. {python_cq-0.14.1 → python_cq-0.15.1}/cq/_core/dispatcher/pipe.py +0 -0
  18. {python_cq-0.14.1 → python_cq-0.15.1}/cq/_core/message.py +0 -0
  19. {python_cq-0.14.1 → python_cq-0.15.1}/cq/_core/pipetools.py +0 -0
  20. {python_cq-0.14.1 → python_cq-0.15.1}/cq/_core/related_events.py +0 -0
  21. {python_cq-0.14.1 → python_cq-0.15.1}/cq/_core/scope.py +0 -0
  22. {python_cq-0.14.1 → python_cq-0.15.1}/cq/exceptions.py +0 -0
  23. {python_cq-0.14.1 → python_cq-0.15.1}/cq/ext/__init__.py +0 -0
  24. {python_cq-0.14.1 → python_cq-0.15.1}/cq/ext/fastapi.py +0 -0
  25. {python_cq-0.14.1 → python_cq-0.15.1}/cq/middlewares/__init__.py +0 -0
  26. {python_cq-0.14.1 → python_cq-0.15.1}/cq/py.typed +0 -0
  27. {python_cq-0.14.1 → python_cq-0.15.1}/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.1
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
@@ -17,7 +17,7 @@ from ._core.message import (
17
17
  new_query_bus,
18
18
  query_handler,
19
19
  )
20
- from ._core.middleware import Middleware, MiddlewareResult
20
+ from ._core.middleware import Middleware, MiddlewareResult, resolve_handler_source
21
21
  from ._core.pipetools import ContextCommandPipeline
22
22
  from ._core.related_events import RelatedEvents
23
23
  from ._core.scope import CQScope
@@ -47,4 +47,5 @@ __all__ = (
47
47
  "new_event_bus",
48
48
  "new_query_bus",
49
49
  "query_handler",
50
+ "resolve_handler_source",
50
51
  )
@@ -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,
@@ -30,7 +31,12 @@ class Bus[I, O](Dispatcher[I, O], Protocol):
30
31
  raise NotImplementedError
31
32
 
32
33
  @abstractmethod
33
- def subscribe(self, input_type: type[I], factory: HandlerFactory[[I], O]) -> Self:
34
+ def subscribe(
35
+ self,
36
+ input_type: type[I],
37
+ factory: HandlerFactory[[I], O],
38
+ fail_silently: bool = ...,
39
+ ) -> Self:
34
40
  raise NotImplementedError
35
41
 
36
42
 
@@ -49,14 +55,16 @@ class BaseBus[I, O](BaseDispatcher[I, O], Bus[I, O], ABC):
49
55
  self.__listeners.extend(listeners)
50
56
  return self
51
57
 
52
- def subscribe(self, input_type: type[I], factory: HandlerFactory[[I], O]) -> Self:
53
- self.__registry.subscribe(input_type, factory)
54
- return self
55
-
56
- def _handlers_from(
58
+ def subscribe(
57
59
  self,
58
60
  input_type: type[I],
59
- ) -> Iterator[Callable[[I], Awaitable[O]]]:
61
+ factory: HandlerFactory[[I], O],
62
+ fail_silently: bool = False,
63
+ ) -> Self:
64
+ self.__registry.subscribe(input_type, factory, fail_silently=fail_silently)
65
+ return self
66
+
67
+ def _handlers_from(self, input_type: type[I]) -> Iterator[HandleFunction[[I], O]]:
60
68
  return self.__registry.handlers_from(input_type)
61
69
 
62
70
  def _trigger_listeners(self, input_value: I, /, task_group: TaskGroup) -> None:
@@ -75,7 +83,11 @@ class SimpleBus[I, O](BaseBus[I, O]):
75
83
  self._trigger_listeners(input_value, task_group)
76
84
 
77
85
  for handler in self._handlers_from(type(input_value)):
78
- return await self._invoke_with_middlewares(handler, input_value)
86
+ return await self._invoke_with_middlewares(
87
+ handler,
88
+ input_value,
89
+ handler.fail_silently,
90
+ )
79
91
 
80
92
  return NotImplemented
81
93
 
@@ -95,4 +107,5 @@ class TaskBus[I](BaseBus[I, None]):
95
107
  self._invoke_with_middlewares,
96
108
  handler,
97
109
  input_value,
110
+ handler.fail_silently,
98
111
  )
@@ -3,7 +3,7 @@ from collections import defaultdict
3
3
  from collections.abc import Awaitable, Callable, Iterator
4
4
  from dataclasses import dataclass, field
5
5
  from functools import partial
6
- from inspect import Parameter, isclass
6
+ from inspect import Parameter, isclass, unwrap
7
7
  from inspect import signature as inspect_signature
8
8
  from typing import TYPE_CHECKING, Any, Protocol, Self, overload, runtime_checkable
9
9
 
@@ -21,74 +21,105 @@ 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
+ factory: HandlerFactory[P, T]
31
+ source: HandlerType[P, T] | Any
32
+ fail_silently: bool
33
+
34
+ async def __call__(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
35
+ handler = await self.factory()
36
+ return await handler.handle(*args, **kwargs)
37
+
38
+ @classmethod
39
+ def create(
40
+ cls,
41
+ factory: HandlerFactory[P, T],
42
+ source: HandlerType[P, T] | None = None,
43
+ fail_silently: bool = False,
44
+ ) -> Self:
45
+ return cls(factory, source or unwrap(factory), fail_silently)
46
+
47
+
28
48
  @runtime_checkable
29
49
  class HandlerRegistry[I, O](Protocol):
30
50
  __slots__ = ()
31
51
 
32
52
  @abstractmethod
33
- def handlers_from(
34
- self,
35
- input_type: type[I],
36
- ) -> Iterator[Callable[[I], Awaitable[O]]]:
53
+ def handlers_from(self, input_type: type[I]) -> Iterator[HandleFunction[[I], O]]:
37
54
  raise NotImplementedError
38
55
 
39
56
  @abstractmethod
40
- def subscribe(self, input_type: type[I], factory: HandlerFactory[[I], O]) -> Self:
57
+ def subscribe(
58
+ self,
59
+ input_type: type[I],
60
+ handler_factory: HandlerFactory[[I], O],
61
+ handler_type: HandlerType[[I], O] | None = ...,
62
+ fail_silently: bool = ...,
63
+ ) -> Self:
41
64
  raise NotImplementedError
42
65
 
43
66
 
44
67
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
45
68
  class MultipleHandlerRegistry[I, O](HandlerRegistry[I, O]):
46
- __factories: dict[type[I], list[HandlerFactory[[I], O]]] = field(
69
+ __values: dict[type[I], list[HandleFunction[[I], O]]] = field(
47
70
  default_factory=partial(defaultdict, list),
48
71
  init=False,
49
72
  )
50
73
 
51
- def handlers_from(
74
+ def handlers_from(self, input_type: type[I]) -> Iterator[HandleFunction[[I], O]]:
75
+ for key_type in _iter_key_types(input_type):
76
+ yield from self.__values.get(key_type, ())
77
+
78
+ def subscribe(
52
79
  self,
53
80
  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)
81
+ handler_factory: HandlerFactory[[I], O],
82
+ handler_type: HandlerType[[I], O] | None = None,
83
+ fail_silently: bool = False,
84
+ ) -> Self:
85
+ function = HandleFunction.create(handler_factory, handler_type, fail_silently)
58
86
 
59
- def subscribe(self, input_type: type[I], factory: HandlerFactory[[I], O]) -> Self:
60
87
  for key_type in _build_key_types(input_type):
61
- self.__factories[key_type].append(factory)
88
+ self.__values[key_type].append(function)
62
89
 
63
90
  return self
64
91
 
65
92
 
66
93
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
67
94
  class SingleHandlerRegistry[I, O](HandlerRegistry[I, O]):
68
- __factories: dict[type[I], HandlerFactory[[I], O]] = field(
95
+ __values: dict[type[I], HandleFunction[[I], O]] = field(
69
96
  default_factory=dict,
70
97
  init=False,
71
98
  )
72
99
 
73
- def handlers_from(
74
- self,
75
- input_type: type[I],
76
- ) -> Iterator[Callable[[I], Awaitable[O]]]:
100
+ def handlers_from(self, input_type: type[I]) -> Iterator[HandleFunction[[I], O]]:
77
101
  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)
102
+ function = self.__values.get(key_type, None)
103
+ if function is not None:
104
+ yield function
81
105
 
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)}
106
+ def subscribe(
107
+ self,
108
+ input_type: type[I],
109
+ handler_factory: HandlerFactory[[I], O],
110
+ handler_type: HandlerType[[I], O] | None = None,
111
+ fail_silently: bool = False,
112
+ ) -> Self:
113
+ function = HandleFunction.create(handler_factory, handler_type, fail_silently)
114
+ entries = {key_type: function for key_type in _build_key_types(input_type)}
84
115
 
85
116
  for key_type in entries:
86
- if key_type in self.__factories:
117
+ if key_type in self.__values:
87
118
  raise RuntimeError(
88
119
  f"A handler is already registered for the input type: `{key_type}`."
89
120
  )
90
121
 
91
- self.__factories.update(entries)
122
+ self.__values.update(entries)
92
123
  return self
93
124
 
94
125
 
@@ -105,6 +136,7 @@ class HandlerDecorator[I, O]:
105
136
  input_or_handler_type: type[I],
106
137
  /,
107
138
  *,
139
+ fail_silently: bool = ...,
108
140
  threadsafe: bool | None = ...,
109
141
  ) -> Decorator: ...
110
142
 
@@ -114,6 +146,7 @@ class HandlerDecorator[I, O]:
114
146
  input_or_handler_type: T,
115
147
  /,
116
148
  *,
149
+ fail_silently: bool = ...,
117
150
  threadsafe: bool | None = ...,
118
151
  ) -> T: ...
119
152
 
@@ -123,6 +156,7 @@ class HandlerDecorator[I, O]:
123
156
  input_or_handler_type: None = ...,
124
157
  /,
125
158
  *,
159
+ fail_silently: bool = ...,
126
160
  threadsafe: bool | None = ...,
127
161
  ) -> Decorator: ...
128
162
 
@@ -131,6 +165,7 @@ class HandlerDecorator[I, O]:
131
165
  input_or_handler_type: type[I] | T | None = None,
132
166
  /,
133
167
  *,
168
+ fail_silently: bool = False,
134
169
  threadsafe: bool | None = None,
135
170
  ) -> Any:
136
171
  if (
@@ -138,11 +173,16 @@ class HandlerDecorator[I, O]:
138
173
  and isclass(input_or_handler_type)
139
174
  and issubclass(input_or_handler_type, Handler)
140
175
  ):
141
- return self.__decorator(input_or_handler_type, threadsafe=threadsafe)
176
+ return self.__decorator(
177
+ input_or_handler_type,
178
+ fail_silently=fail_silently,
179
+ threadsafe=threadsafe,
180
+ )
142
181
 
143
182
  return partial(
144
183
  self.__decorator,
145
184
  input_type=input_or_handler_type, # type: ignore[arg-type]
185
+ fail_silently=fail_silently,
146
186
  threadsafe=threadsafe,
147
187
  )
148
188
 
@@ -152,11 +192,12 @@ class HandlerDecorator[I, O]:
152
192
  /,
153
193
  *,
154
194
  input_type: type[I] | None = None,
195
+ fail_silently: bool = False,
155
196
  threadsafe: bool | None = None,
156
197
  ) -> HandlerType[[I], O]:
157
198
  factory = self.injection_module.make_async_factory(wrapped, threadsafe)
158
199
  input_type = input_type or _resolve_input_type(wrapped)
159
- self.registry.subscribe(input_type, factory)
200
+ self.registry.subscribe(input_type, factory, wrapped, fail_silently)
160
201
  return wrapped
161
202
 
162
203
 
@@ -190,14 +231,3 @@ def _resolve_input_type[I, O](handler_type: HandlerType[[I], O]) -> type[I]:
190
231
  f"Unable to resolve input type for handler `{handler_type}`, "
191
232
  "`handle` method must have a type annotation for its first parameter."
192
233
  )
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)
@@ -1,8 +1,9 @@
1
1
  from collections.abc import AsyncGenerator, Awaitable, Callable
2
2
  from dataclasses import dataclass, field
3
3
  from inspect import isasyncgenfunction
4
- from typing import Concatenate, Self, TypeGuard
4
+ from typing import Any, Concatenate, Self, TypeGuard
5
5
 
6
+ from cq._core.handler import HandleFunction, HandlerType
6
7
  from cq.exceptions import MiddlewareError
7
8
 
8
9
  type MiddlewareResult[T] = AsyncGenerator[None, T]
@@ -63,6 +64,19 @@ class _BoundMiddleware[**P, T]:
63
64
  return await self.middleware(self.call_next, *args, **kwargs)
64
65
 
65
66
 
67
+ def resolve_handler_source[**P, T](
68
+ call_next: Callable[P, Awaitable[T]]
69
+ | _BoundMiddleware[P, T]
70
+ | HandleFunction[P, T],
71
+ /,
72
+ ) -> HandlerType[P, T] | Any:
73
+ while True:
74
+ try:
75
+ call_next = call_next.call_next # type: ignore[union-attr]
76
+ except AttributeError:
77
+ return call_next.source # type: ignore[union-attr]
78
+
79
+
66
80
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
67
81
  class _GeneratorMiddleware[**P, T]:
68
82
  middleware: GeneratorMiddleware[P, T]
@@ -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.1"
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