Skip to content

Commit 477415a

Browse files
authored
feat: MkDocs
1 parent f11c551 commit 477415a

File tree

19 files changed

+940
-465
lines changed

19 files changed

+940
-465
lines changed

.github/workflows/cd.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313

1414
steps:
1515
- name: Run checkout
16-
uses: actions/checkout@v5
16+
uses: actions/checkout@v6
1717

1818
- name: Set up environment
1919
uses: ./.github/actions/environment

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020

2121
steps:
2222
- name: Run checkout
23-
uses: actions/checkout@v5
23+
uses: actions/checkout@v6
2424

2525
- name: Set up environment
2626
uses: ./.github/actions/environment

.github/workflows/pages.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: GitHub Pages
2+
3+
on:
4+
push:
5+
branches:
6+
- prod
7+
8+
jobs:
9+
delivery:
10+
name: Delivery
11+
runs-on: ubuntu-latest
12+
permissions:
13+
contents: write
14+
15+
steps:
16+
- name: Run checkout
17+
uses: actions/checkout@v6
18+
19+
- name: Configure Git
20+
run: |
21+
git config --global user.name "github-pages[bot]"
22+
git config --global user.email "github-pages[bot]@users.noreply.github.com"
23+
24+
- name: Set up environment
25+
uses: ./.github/actions/environment
26+
27+
- name: Deploy MkDocs
28+
shell: bash
29+
run: uv run mkdocs gh-deploy --force

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,6 @@ mypy:
1616

1717
pytest:
1818
uv run pytest
19+
20+
mkdocs:
21+
uv run mkdocs serve

README.md

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,13 @@
55
[![PyPI - Downloads](https://img.shields.io/pypi/dm/python-cq.svg?color=blue)](https://pypistats.org/packages/python-cq)
66
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
77

8-
Lightweight library for separating Python code according to **Command and Query Responsibility Segregation** principles.
8+
Documentation: https://python-cq.remimd.dev
99

10-
Dependency injection is handled by [python-injection](https://github.com/100nm/python-injection).
11-
12-
Easy to use with [FastAPI](https://github.com/fastapi/fastapi).
10+
Python package designed to organize your code following CQRS principles. It builds on top of [python-injection](https://github.com/100nm/python-injection) for dependency injection.
1311

1412
## Installation
1513

1614
⚠️ _Requires Python 3.12 or higher_
17-
1815
```bash
1916
pip install python-cq
2017
```
21-
22-
## Resources
23-
24-
* [**Writing Application Layer**](https://github.com/100nm/python-cq/tree/prod/documentation/writing-application-layer.md)
25-
* [**Pipeline**](https://github.com/100nm/python-cq/tree/prod/documentation/pipeline.md)
26-
* [**FastAPI Example**](https://github.com/100nm/python-cq/tree/prod/documentation/fastapi-example.md)

cq/_core/handler.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ def subscribe(self, input_type: type[I], factory: HandlerFactory[[I], O]) -> Sel
9090
return self
9191

9292

93+
class _Decorator(Protocol):
94+
def __call__[T](self, wrapped: T, /) -> T: ...
95+
96+
9397
@dataclass(repr=False, eq=False, frozen=True, slots=True)
9498
class HandlerDecorator[I, O]:
9599
registry: HandlerRegistry[I, O]
@@ -104,16 +108,16 @@ def __call__(
104108
/,
105109
*,
106110
threadsafe: bool | None = ...,
107-
) -> Callable[[HandlerType[[I], O]], HandlerType[[I], O]]: ...
111+
) -> _Decorator: ...
108112

109113
@overload
110-
def __call__(
114+
def __call__[T](
111115
self,
112-
input_or_handler_type: HandlerType[[I], O],
116+
input_or_handler_type: T,
113117
/,
114118
*,
115119
threadsafe: bool | None = ...,
116-
) -> HandlerType[[I], O]: ...
120+
) -> T: ...
117121

118122
@overload
119123
def __call__(
@@ -122,11 +126,11 @@ def __call__(
122126
/,
123127
*,
124128
threadsafe: bool | None = ...,
125-
) -> Callable[[HandlerType[[I], O]], HandlerType[[I], O]]: ...
129+
) -> _Decorator: ...
126130

127-
def __call__(
131+
def __call__[T](
128132
self,
129-
input_or_handler_type: type[I] | HandlerType[[I], O] | None = None,
133+
input_or_handler_type: type[I] | T | None = None,
130134
/,
131135
*,
132136
threadsafe: bool | None = None,

cq/middlewares/scope.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ class InjectionScopeMiddleware:
2121

2222
async def __call__(self, *args: Any, **kwargs: Any) -> MiddlewareResult[Any]:
2323
async with AsyncExitStack() as stack:
24-
cm = adefine_scope(self.scope_name, threadsafe=self.threadsafe)
2524
try:
26-
await stack.enter_async_context(cm)
25+
await stack.enter_async_context(
26+
adefine_scope(self.scope_name, threadsafe=self.threadsafe)
27+
)
2728

2829
except ScopeAlreadyDefinedError:
2930
if not self.exist_ok:
3031
raise
3132

32-
del cm
3333
yield

docs/CNAME

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
python-cq.remimd.dev

docs/guides/configuring.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Configuring a Bus
2+
3+
Each bus can be customized with listeners and middlewares. To do so, create a factory function decorated with `@injectable` that returns the configured bus.
4+
```python
5+
from cq import CommandBus, MiddlewareResult, new_command_bus
6+
from injection import injectable
7+
8+
async def listener(message: MessageType):
9+
...
10+
11+
async def middleware(message: MessageType) -> MiddlewareResult[ReturnType]:
12+
# do something before the handler is executed
13+
return_value = yield
14+
# do something after the handler is executed
15+
16+
@injectable
17+
def command_bus_factory() -> CommandBus:
18+
bus = new_command_bus()
19+
bus.add_listeners(listener)
20+
bus.add_middlewares(middleware)
21+
return bus
22+
```
23+
24+
The same pattern applies to `QueryBus` and `EventBus` using `new_query_bus()` and `new_event_bus()`.
25+
26+
## Listeners
27+
28+
Listeners are executed before the handler(s). They receive the message and can perform side effects such as logging or validation.
29+
```python
30+
async def log_listener(message: MessageType):
31+
print(f"Received: {message}")
32+
```
33+
34+
## Middlewares
35+
36+
Middlewares wrap around handler execution, allowing you to run logic before and after a handler processes a message.
37+
```python
38+
async def timing_middleware(message: Any) -> MiddlewareResult[Any]:
39+
start = time.time()
40+
yield
41+
print(f"Execution time: {time.time() - start}s")
42+
```
43+
44+
For commands and queries, middlewares run once around the single handler. For events, middlewares run around each handler individually.
45+
46+
!!! note
47+
The generator was chosen to keep both the input message and the return value read-only.
48+
49+
## Class-based listeners and middlewares
50+
51+
For more flexibility, listeners and middlewares can be defined as classes with a `__call__` method. This allows you to inject dependencies and configure their behavior.
52+
```python
53+
from cq import MiddlewareResult
54+
from dataclasses import dataclass
55+
56+
@dataclass
57+
class LogListener:
58+
logger: Logger
59+
60+
async def __call__(self, message: Any):
61+
self.logger.info(f"Received: {message}")
62+
63+
@dataclass
64+
class TimingMiddleware:
65+
metrics: MetricsService
66+
67+
async def __call__(self, message: Any) -> MiddlewareResult[Any]:
68+
start = time.time()
69+
yield
70+
self.metrics.record(time.time() - start)
71+
```

docs/guides/dispatching.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Dispatching messages
2+
3+
To dispatch messages to their handlers, **python-cq** provides three bus classes: `CommandBus`, `QueryBus`, and `EventBus`.
4+
5+
Each bus can take a generic parameter to specify the return type of the `dispatch` method.
6+
7+
## Retrieving a bus
8+
9+
Bus instances are available through [python-injection](https://github.com/100nm/python-injection)'s dependency injection:
10+
11+
```python
12+
from cq import CommandBus
13+
from injection import inject
14+
15+
@inject
16+
async def create_user(bus: CommandBus[None]):
17+
command = CreateUserCommand(name="John", email="[email protected]")
18+
await bus.dispatch(command)
19+
```
20+
21+
## CommandBus
22+
23+
Use the CommandBus to dispatch commands. It returns the value produced by the handler.
24+
```python
25+
from cq import CommandBus
26+
27+
bus: CommandBus[None]
28+
command = CreateUserCommand(name="John", email="[email protected]")
29+
await bus.dispatch(command)
30+
```
31+
32+
## QueryBus
33+
34+
Use the QueryBus to dispatch queries. It returns the value produced by the handler.
35+
```python
36+
from cq import QueryBus
37+
38+
bus: QueryBus[User]
39+
query = GetUserByIdQuery(user_id)
40+
user = await bus.dispatch(query)
41+
```
42+
43+
## EventBus
44+
45+
Use the EventBus to dispatch events. Since events can be handled by multiple handlers (or none), it does not return a value.
46+
```python
47+
from cq import EventBus
48+
49+
bus: EventBus
50+
event = UserCreatedEvent(user_id)
51+
await bus.dispatch(event)
52+
```

0 commit comments

Comments
 (0)