python-cq 0.1.2__tar.gz → 0.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-cq
3
- Version: 0.1.2
3
+ Version: 0.2.0
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,10 +1,20 @@
1
- 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
2
10
  from ._core.dto import DTO
3
11
  from ._core.event import Event, EventBus, event_handler, find_event_bus
4
12
  from ._core.middleware import Middleware, MiddlewareResult
5
13
  from ._core.query import Query, QueryBus, find_query_bus, query_handler
6
14
 
7
15
  __all__ = (
16
+ "AnyCommandBus",
17
+ "Bus",
8
18
  "Command",
9
19
  "CommandBus",
10
20
  "DTO",
@@ -12,6 +22,7 @@ __all__ = (
12
22
  "EventBus",
13
23
  "Middleware",
14
24
  "MiddlewareResult",
25
+ "Pipe",
15
26
  "Query",
16
27
  "QueryBus",
17
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,53 @@
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, first_input_value: I, /, *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, first_input_value: I, /, *input_values: I) -> None:
35
+ asyncio.gather(
36
+ *(
37
+ self.dispatch(input_value)
38
+ for input_value in (first_input_value, *input_values)
39
+ ),
40
+ return_exceptions=True,
41
+ )
42
+
43
+ def add_middlewares(self, *middlewares: Middleware[[I], O]) -> Self:
44
+ self.__middleware_group.add(*middlewares)
45
+ return self
46
+
47
+ async def _invoke_with_middlewares(
48
+ self,
49
+ handler: Callable[[I], Awaitable[O]],
50
+ input_value: I,
51
+ /,
52
+ ) -> O:
53
+ 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,29 +27,13 @@ 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
- )
44
-
45
33
  @abstractmethod
46
34
  def subscribe(self, input_type: type[I], factory: HandlerFactory[[I], O]) -> Self:
47
35
  raise NotImplementedError
48
36
 
49
- @abstractmethod
50
- def add_middlewares(self, *middlewares: Middleware[[I], O]) -> Self:
51
- raise NotImplementedError
52
-
53
37
 
54
38
  @dataclass(eq=False, frozen=True, slots=True)
55
39
  class SubscriberDecorator[I, O]:
@@ -80,23 +64,7 @@ class SubscriberDecorator[I, O]:
80
64
  return self.injection_module.find_instance(self.bus_type)
81
65
 
82
66
 
83
- class _BaseBus[I, O](Bus[I, O], ABC):
84
- __slots__ = ("__middleware_group",)
85
-
86
- __middleware_group: MiddlewareGroup[[I], O]
87
-
88
- def __init__(self) -> None:
89
- self.__middleware_group = MiddlewareGroup()
90
-
91
- def add_middlewares(self, *middlewares: Middleware[[I], O]) -> Self:
92
- self.__middleware_group.add(*middlewares)
93
- return self
94
-
95
- async def _invoke(self, handler: Handler[[I], O], input_value: I, /) -> O:
96
- return await self.__middleware_group.invoke(handler.handle, input_value)
97
-
98
-
99
- class SimpleBus[I, O](_BaseBus[I, O]):
67
+ class SimpleBus[I, O](BaseDispatcher[I, O], Bus[I, O]):
100
68
  __slots__ = ("__handlers",)
101
69
 
102
70
  __handlers: dict[type[I], HandlerFactory[[I], O]]
@@ -113,7 +81,10 @@ class SimpleBus[I, O](_BaseBus[I, O]):
113
81
  except KeyError:
114
82
  return NotImplemented
115
83
 
116
- return await self._invoke(handler_factory(), input_value)
84
+ return await self._invoke_with_middlewares(
85
+ handler_factory().handle,
86
+ input_value,
87
+ )
117
88
 
118
89
  def subscribe(self, input_type: type[I], factory: HandlerFactory[[I], O]) -> Self:
119
90
  if input_type in self.__handlers:
@@ -125,7 +96,7 @@ class SimpleBus[I, O](_BaseBus[I, O]):
125
96
  return self
126
97
 
127
98
 
128
- class TaskBus[I](_BaseBus[I, None]):
99
+ class TaskBus[I](BaseDispatcher[I, None], Bus[I, None]):
129
100
  __slots__ = ("__handlers",)
130
101
 
131
102
  __handlers: dict[type[I], list[HandlerFactory[[I], None]]]
@@ -142,7 +113,10 @@ class TaskBus[I](_BaseBus[I, None]):
142
113
 
143
114
  await asyncio.gather(
144
115
  *(
145
- self._invoke(handler_factory(), input_value)
116
+ self._invoke_with_middlewares(
117
+ handler_factory().handle,
118
+ input_value,
119
+ )
146
120
  for handler_factory in handler_factories
147
121
  )
148
122
  )
@@ -0,0 +1,52 @@
1
+ from collections.abc import Callable
2
+ from dataclasses import dataclass, field
3
+ from typing import Any, Awaitable
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
+ async def dispatch(self, input_value: I, /) -> O:
42
+ return await self._invoke_with_middlewares(self.__execute, input_value)
43
+
44
+ async def __execute(self, input_value: I) -> O:
45
+ dispatcher = self.__dispatcher
46
+
47
+ for step in self.__steps:
48
+ output_value = await dispatcher.dispatch(input_value)
49
+ input_value = await step.converter(output_value)
50
+ dispatcher = step.dispatcher or self.__dispatcher
51
+
52
+ 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
 
@@ -6,13 +6,9 @@ type MiddlewareResult[T] = AsyncGenerator[None, T]
6
6
  type Middleware[**P, T] = Callable[P, MiddlewareResult[T]]
7
7
 
8
8
 
9
- @dataclass(eq=False, frozen=True, slots=True)
9
+ @dataclass(repr=False, eq=False, frozen=True, slots=True)
10
10
  class MiddlewareGroup[**P, T]:
11
- __middlewares: list[Middleware[P, T]] = field(
12
- default_factory=list,
13
- init=False,
14
- repr=False,
15
- )
11
+ __middlewares: list[Middleware[P, T]] = field(default_factory=list, init=False)
16
12
 
17
13
  @property
18
14
  def __stack(self) -> Iterator[Middleware[P, T]]:
@@ -51,6 +47,7 @@ class MiddlewareGroup[**P, T]:
51
47
  await generator.athrow(exc)
52
48
  else:
53
49
  await generator.asend(value)
50
+ break
54
51
 
55
52
  except StopAsyncIteration:
56
53
  ...
@@ -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
 
File without changes
@@ -0,0 +1,34 @@
1
+ import asyncio
2
+ from typing import Any
3
+
4
+ from cq import MiddlewareResult
5
+
6
+
7
+ class RetryMiddleware:
8
+ __slots__ = ("__delay", "__exceptions", "__retry")
9
+
10
+ def __init__(
11
+ self,
12
+ retry: int,
13
+ delay: float = 0,
14
+ exceptions: tuple[type[BaseException], ...] = (Exception,),
15
+ ) -> None:
16
+ self.__delay = delay
17
+ self.__exceptions = exceptions
18
+ self.__retry = retry
19
+
20
+ async def __call__(self, *args: Any, **kwargs: Any) -> MiddlewareResult[Any]:
21
+ retry = self.__retry
22
+
23
+ for attempt in range(1, retry + 1):
24
+ try:
25
+ yield
26
+
27
+ except self.__exceptions as exc:
28
+ if attempt == retry:
29
+ raise exc
30
+
31
+ else:
32
+ break
33
+
34
+ await asyncio.sleep(self.__delay)
File without changes
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-cq"
3
- version = "0.1.2"
3
+ version = "0.2.0"
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