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.
- {python_cq-0.1.2 → python_cq-0.2.0}/PKG-INFO +3 -1
- {python_cq-0.1.2 → python_cq-0.2.0}/README.md +1 -0
- {python_cq-0.1.2 → python_cq-0.2.0}/cq/__init__.py +12 -1
- {python_cq-0.1.2 → python_cq-0.2.0}/cq/_core/command.py +2 -1
- python_cq-0.2.0/cq/_core/dispatcher/base.py +53 -0
- {python_cq-0.1.2/cq/_core → python_cq-0.2.0/cq/_core/dispatcher}/bus.py +13 -39
- python_cq-0.2.0/cq/_core/dispatcher/pipe.py +52 -0
- {python_cq-0.1.2 → python_cq-0.2.0}/cq/_core/event.py +1 -1
- {python_cq-0.1.2 → python_cq-0.2.0}/cq/_core/middleware.py +3 -6
- {python_cq-0.1.2 → python_cq-0.2.0}/cq/_core/query.py +1 -1
- python_cq-0.2.0/cq/middlewares/__init__.py +0 -0
- python_cq-0.2.0/cq/middlewares/retry.py +34 -0
- python_cq-0.2.0/cq/py.typed +0 -0
- {python_cq-0.1.2 → python_cq-0.2.0}/pyproject.toml +1 -2
- {python_cq-0.1.2 → python_cq-0.2.0}/cq/_core/__init__.py +0 -0
- /python_cq-0.1.2/cq/py.typed → /python_cq-0.2.0/cq/_core/dispatcher/__init__.py +0 -0
- {python_cq-0.1.2 → python_cq-0.2.0}/cq/_core/dto.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-cq
|
|
3
|
-
Version: 0.
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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](
|
|
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.
|
|
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)
|
|
@@ -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
|
...
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|