python-cq 0.1.3__tar.gz → 0.2.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.1
2
2
  Name: python-cq
3
- Version: 0.1.3
3
+ Version: 0.2.1
4
4
  Summary: Lightweight CQRS library.
5
5
  Home-page: https://github.com/100nm/python-cq
6
6
  License: MIT
@@ -15,6 +15,7 @@ Classifier: Operating System :: OS Independent
15
15
  Classifier: Programming Language :: Python
16
16
  Classifier: Programming Language :: Python :: 3
17
17
  Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
18
19
  Classifier: Programming Language :: Python :: 3 :: Only
19
20
  Classifier: Topic :: Software Development :: Libraries
20
21
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
@@ -48,5 +49,6 @@ pip install python-cq
48
49
  ## Resources
49
50
 
50
51
  * [**Writing Application Layer**](https://github.com/100nm/python-cq/tree/prod/documentation/writing-application-layer.md)
52
+ * [**Pipeline**](https://github.com/100nm/python-cq/tree/prod/documentation/pipeline.md)
51
53
  * [**FastAPI Example**](https://github.com/100nm/python-cq/tree/prod/documentation/fastapi-example.md)
52
54
 
@@ -21,4 +21,5 @@ pip install python-cq
21
21
  ## Resources
22
22
 
23
23
  * [**Writing Application Layer**](https://github.com/100nm/python-cq/tree/prod/documentation/writing-application-layer.md)
24
+ * [**Pipeline**](https://github.com/100nm/python-cq/tree/prod/documentation/pipeline.md)
24
25
  * [**FastAPI Example**](https://github.com/100nm/python-cq/tree/prod/documentation/fastapi-example.md)
@@ -1,11 +1,19 @@
1
- from ._core.bus import Bus
2
- from ._core.command import Command, CommandBus, command_handler, find_command_bus
1
+ from ._core.command import (
2
+ AnyCommandBus,
3
+ Command,
4
+ CommandBus,
5
+ command_handler,
6
+ find_command_bus,
7
+ )
8
+ from ._core.dispatcher.bus import Bus
9
+ from ._core.dispatcher.pipe import Pipe
3
10
  from ._core.dto import DTO
4
11
  from ._core.event import Event, EventBus, event_handler, find_event_bus
5
12
  from ._core.middleware import Middleware, MiddlewareResult
6
13
  from ._core.query import Query, QueryBus, find_query_bus, query_handler
7
14
 
8
15
  __all__ = (
16
+ "AnyCommandBus",
9
17
  "Bus",
10
18
  "Command",
11
19
  "CommandBus",
@@ -14,6 +22,7 @@ __all__ = (
14
22
  "EventBus",
15
23
  "Middleware",
16
24
  "MiddlewareResult",
25
+ "Pipe",
17
26
  "Query",
18
27
  "QueryBus",
19
28
  "command_handler",
@@ -3,7 +3,7 @@ from typing import Any
3
3
 
4
4
  import injection
5
5
 
6
- from cq._core.bus import Bus, SimpleBus, SubscriberDecorator
6
+ from cq._core.dispatcher.bus import Bus, SimpleBus, SubscriberDecorator
7
7
  from cq._core.dto import DTO
8
8
 
9
9
 
@@ -12,6 +12,7 @@ class Command(DTO, ABC):
12
12
 
13
13
 
14
14
  type CommandBus[T] = Bus[Command, T]
15
+ AnyCommandBus = CommandBus[Any]
15
16
  command_handler: SubscriberDecorator[Command, Any] = SubscriberDecorator(CommandBus)
16
17
 
17
18
  injection.set_constant(SimpleBus(), CommandBus, alias=True)
@@ -0,0 +1,50 @@
1
+ import asyncio
2
+ from abc import ABC, abstractmethod
3
+ from collections.abc import Awaitable
4
+ from typing import Callable, Protocol, Self, runtime_checkable
5
+
6
+ from cq._core.middleware import Middleware, MiddlewareGroup
7
+
8
+
9
+ @runtime_checkable
10
+ class Dispatcher[I, O](Protocol):
11
+ __slots__ = ()
12
+
13
+ @abstractmethod
14
+ async def dispatch(self, input_value: I, /) -> O:
15
+ raise NotImplementedError
16
+
17
+ @abstractmethod
18
+ def dispatch_no_wait(self, *input_values: I) -> None:
19
+ raise NotImplementedError
20
+
21
+ @abstractmethod
22
+ def add_middlewares(self, *middlewares: Middleware[[I], O]) -> Self:
23
+ raise NotImplementedError
24
+
25
+
26
+ class BaseDispatcher[I, O](Dispatcher[I, O], ABC):
27
+ __slots__ = ("__middleware_group",)
28
+
29
+ __middleware_group: MiddlewareGroup[[I], O]
30
+
31
+ def __init__(self) -> None:
32
+ self.__middleware_group = MiddlewareGroup()
33
+
34
+ def dispatch_no_wait(self, *input_values: I) -> None:
35
+ asyncio.gather(
36
+ *(self.dispatch(input_value) for input_value in input_values),
37
+ return_exceptions=True,
38
+ )
39
+
40
+ def add_middlewares(self, *middlewares: Middleware[[I], O]) -> Self:
41
+ self.__middleware_group.add(*middlewares)
42
+ return self
43
+
44
+ async def _invoke_with_middlewares(
45
+ self,
46
+ handler: Callable[[I], Awaitable[O]],
47
+ input_value: I,
48
+ /,
49
+ ) -> O:
50
+ return await self.__middleware_group.invoke(handler, input_value)
@@ -1,5 +1,5 @@
1
1
  import asyncio
2
- from abc import ABC, abstractmethod
2
+ from abc import abstractmethod
3
3
  from collections import defaultdict
4
4
  from collections.abc import Callable
5
5
  from dataclasses import dataclass, field
@@ -9,7 +9,7 @@ from typing import Protocol, Self, TypeAliasType, runtime_checkable
9
9
 
10
10
  import injection
11
11
 
12
- from cq._core.middleware import Middleware, MiddlewareGroup
12
+ from cq._core.dispatcher.base import BaseDispatcher, Dispatcher
13
13
 
14
14
  type HandlerType[**P, T] = type[Handler[P, T]]
15
15
  type HandlerFactory[**P, T] = Callable[..., Handler[P, T]]
@@ -27,43 +27,21 @@ class Handler[**P, T](Protocol):
27
27
 
28
28
 
29
29
  @runtime_checkable
30
- class Bus[I, O](Protocol):
30
+ class Bus[I, O](Dispatcher[I, O], Protocol):
31
31
  __slots__ = ()
32
32
 
33
- @abstractmethod
34
- async def dispatch(self, input_value: I, /) -> O:
35
- raise NotImplementedError
36
-
37
- def dispatch_no_wait(self, first_input_value: I, /, *input_values: I) -> None:
38
- asyncio.gather(
39
- *(
40
- self.dispatch(input_value)
41
- for input_value in (first_input_value, *input_values)
42
- ),
43
- return_exceptions=True,
44
- )
45
-
46
33
  @abstractmethod
47
34
  def subscribe(self, input_type: type[I], factory: HandlerFactory[[I], O]) -> Self:
48
35
  raise NotImplementedError
49
36
 
50
- @abstractmethod
51
- def add_middlewares(self, *middlewares: Middleware[[I], O]) -> Self:
52
- raise NotImplementedError
53
-
54
37
 
55
38
  @dataclass(eq=False, frozen=True, slots=True)
56
39
  class SubscriberDecorator[I, O]:
57
40
  bus_type: BusType[I, O] | TypeAliasType | GenericAlias
58
41
  injection_module: injection.Module = field(default_factory=injection.mod)
59
42
 
60
- def __call__[T](
61
- self,
62
- first_input_type: type[I],
63
- /,
64
- *input_types: type[I],
65
- ) -> Callable[[T], T]:
66
- def decorator(wrapped: T) -> T:
43
+ def __call__(self, first_input_type: type[I], /, *input_types: type[I]): # type: ignore[no-untyped-def]
44
+ def decorator(wrapped): # type: ignore[no-untyped-def]
67
45
  if not isclass(wrapped) or not issubclass(wrapped, Handler):
68
46
  raise TypeError(f"`{wrapped}` isn't a valid handler.")
69
47
 
@@ -73,7 +51,7 @@ class SubscriberDecorator[I, O]:
73
51
  for input_type in (first_input_type, *input_types):
74
52
  bus.subscribe(input_type, factory)
75
53
 
76
- return wrapped # type: ignore[return-value]
54
+ return wrapped
77
55
 
78
56
  return decorator
79
57
 
@@ -81,23 +59,7 @@ class SubscriberDecorator[I, O]:
81
59
  return self.injection_module.find_instance(self.bus_type)
82
60
 
83
61
 
84
- class _BaseBus[I, O](Bus[I, O], ABC):
85
- __slots__ = ("__middleware_group",)
86
-
87
- __middleware_group: MiddlewareGroup[[I], O]
88
-
89
- def __init__(self) -> None:
90
- self.__middleware_group = MiddlewareGroup()
91
-
92
- def add_middlewares(self, *middlewares: Middleware[[I], O]) -> Self:
93
- self.__middleware_group.add(*middlewares)
94
- return self
95
-
96
- async def _invoke(self, handler: Handler[[I], O], input_value: I, /) -> O:
97
- return await self.__middleware_group.invoke(handler.handle, input_value)
98
-
99
-
100
- class SimpleBus[I, O](_BaseBus[I, O]):
62
+ class SimpleBus[I, O](BaseDispatcher[I, O], Bus[I, O]):
101
63
  __slots__ = ("__handlers",)
102
64
 
103
65
  __handlers: dict[type[I], HandlerFactory[[I], O]]
@@ -114,7 +76,10 @@ class SimpleBus[I, O](_BaseBus[I, O]):
114
76
  except KeyError:
115
77
  return NotImplemented
116
78
 
117
- return await self._invoke(handler_factory(), input_value)
79
+ return await self._invoke_with_middlewares(
80
+ handler_factory().handle,
81
+ input_value,
82
+ )
118
83
 
119
84
  def subscribe(self, input_type: type[I], factory: HandlerFactory[[I], O]) -> Self:
120
85
  if input_type in self.__handlers:
@@ -126,7 +91,7 @@ class SimpleBus[I, O](_BaseBus[I, O]):
126
91
  return self
127
92
 
128
93
 
129
- class TaskBus[I](_BaseBus[I, None]):
94
+ class TaskBus[I](BaseDispatcher[I, None], Bus[I, None]):
130
95
  __slots__ = ("__handlers",)
131
96
 
132
97
  __handlers: dict[type[I], list[HandlerFactory[[I], None]]]
@@ -143,7 +108,10 @@ class TaskBus[I](_BaseBus[I, None]):
143
108
 
144
109
  await asyncio.gather(
145
110
  *(
146
- self._invoke(handler_factory(), input_value)
111
+ self._invoke_with_middlewares(
112
+ handler_factory().handle,
113
+ input_value,
114
+ )
147
115
  for handler_factory in handler_factories
148
116
  )
149
117
  )
@@ -0,0 +1,64 @@
1
+ from collections.abc import Callable
2
+ from dataclasses import dataclass, field
3
+ from typing import Any, Awaitable, Self
4
+
5
+ from cq._core.dispatcher.base import BaseDispatcher, Dispatcher
6
+
7
+ type PipeConverter[I, O] = Callable[[O], Awaitable[I]]
8
+
9
+
10
+ @dataclass(repr=False, eq=False, frozen=True, slots=True)
11
+ class PipeStep[I, O]:
12
+ converter: PipeConverter[I, O]
13
+ dispatcher: Dispatcher[I, Any] | None = field(default=None)
14
+
15
+
16
+ class Pipe[I, O](BaseDispatcher[I, O]):
17
+ __slots__ = ("__dispatcher", "__steps")
18
+
19
+ __dispatcher: Dispatcher[Any, Any]
20
+ __steps: list[PipeStep[Any, Any]]
21
+
22
+ def __init__(self, dispatcher: Dispatcher[Any, Any]) -> None:
23
+ super().__init__()
24
+ self.__dispatcher = dispatcher
25
+ self.__steps = []
26
+
27
+ def step[T]( # type: ignore[no-untyped-def]
28
+ self,
29
+ wrapped: PipeConverter[T, Any] | None = None,
30
+ /,
31
+ *,
32
+ dispatcher: Dispatcher[T, Any] | None = None,
33
+ ):
34
+ def decorator(wp): # type: ignore[no-untyped-def]
35
+ step = PipeStep(wp, dispatcher)
36
+ self.__steps.append(step)
37
+ return wp
38
+
39
+ return decorator(wrapped) if wrapped else decorator
40
+
41
+ def add_static_step[T](
42
+ self,
43
+ input_value: T,
44
+ *,
45
+ dispatcher: Dispatcher[T, Any] | None = None,
46
+ ) -> Self:
47
+ @self.step(dispatcher=dispatcher)
48
+ async def converter(_: Any) -> T:
49
+ return input_value
50
+
51
+ return self
52
+
53
+ async def dispatch(self, input_value: I, /) -> O:
54
+ return await self._invoke_with_middlewares(self.__execute, input_value)
55
+
56
+ async def __execute(self, input_value: I) -> O:
57
+ dispatcher = self.__dispatcher
58
+
59
+ for step in self.__steps:
60
+ output_value = await dispatcher.dispatch(input_value)
61
+ input_value = await step.converter(output_value)
62
+ dispatcher = step.dispatcher or self.__dispatcher
63
+
64
+ return await dispatcher.dispatch(input_value)
@@ -2,7 +2,7 @@ from abc import ABC
2
2
 
3
3
  import injection
4
4
 
5
- from cq._core.bus import Bus, SubscriberDecorator, TaskBus
5
+ from cq._core.dispatcher.bus import Bus, SubscriberDecorator, TaskBus
6
6
  from cq._core.dto import DTO
7
7
 
8
8
 
@@ -2,17 +2,15 @@ from collections.abc import AsyncGenerator, Awaitable, Callable, Iterator
2
2
  from dataclasses import dataclass, field
3
3
  from typing import Self
4
4
 
5
+ from cq.exceptions import MiddlewareError
6
+
5
7
  type MiddlewareResult[T] = AsyncGenerator[None, T]
6
8
  type Middleware[**P, T] = Callable[P, MiddlewareResult[T]]
7
9
 
8
10
 
9
- @dataclass(eq=False, frozen=True, slots=True)
11
+ @dataclass(repr=False, eq=False, frozen=True, slots=True)
10
12
  class MiddlewareGroup[**P, T]:
11
- __middlewares: list[Middleware[P, T]] = field(
12
- default_factory=list,
13
- init=False,
14
- repr=False,
15
- )
13
+ __middlewares: list[Middleware[P, T]] = field(default_factory=list, init=False)
16
14
 
17
15
  @property
18
16
  def __stack(self) -> Iterator[Middleware[P, T]]:
@@ -51,6 +49,9 @@ class MiddlewareGroup[**P, T]:
51
49
  await generator.athrow(exc)
52
50
  else:
53
51
  await generator.asend(value)
52
+ raise MiddlewareError(
53
+ f"Too many `yield` keywords in `{middleware}`."
54
+ )
54
55
 
55
56
  except StopAsyncIteration:
56
57
  ...
@@ -3,7 +3,7 @@ from typing import Any
3
3
 
4
4
  import injection
5
5
 
6
- from cq._core.bus import Bus, SimpleBus, SubscriberDecorator
6
+ from cq._core.dispatcher.bus import Bus, SimpleBus, SubscriberDecorator
7
7
  from cq._core.dto import DTO
8
8
 
9
9
 
@@ -0,0 +1,7 @@
1
+ __all__ = ("CQError", "MiddlewareError")
2
+
3
+
4
+ class CQError(Exception): ...
5
+
6
+
7
+ class MiddlewareError(CQError): ...
@@ -3,6 +3,8 @@ from typing import Any
3
3
 
4
4
  from cq import MiddlewareResult
5
5
 
6
+ __all__ = ("RetryMiddleware",)
7
+
6
8
 
7
9
  class RetryMiddleware:
8
10
  __slots__ = ("__delay", "__exceptions", "__retry")
File without changes
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-cq"
3
- version = "0.1.3"
3
+ version = "0.2.1"
4
4
  description = "Lightweight CQRS library."
5
5
  license = "MIT"
6
6
  authors = ["remimd"]
@@ -51,7 +51,6 @@ check_untyped_defs = true
51
51
  disallow_any_generics = true
52
52
  disallow_subclassing_any = true
53
53
  disallow_untyped_defs = true
54
- enable_incomplete_feature = ["NewGenericSyntax"]
55
54
  follow_imports = "silent"
56
55
  no_implicit_reexport = true
57
56
  plugins = ["pydantic.mypy"]
File without changes