Skip to content

Commit b82bb63

Browse files
author
remimd
committed
docs
1 parent 7b1aee7 commit b82bb63

8 files changed

Lines changed: 94 additions & 4 deletions

File tree

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,18 @@
77

88
Documentation: https://python-cq.remimd.dev
99

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.
10+
**python-cq** is a Python package designed to organize your code following CQRS principles. It provides a `DIAdapter` protocol for dependency injection, with [python-injection](https://github.com/100nm/python-injection) as the default implementation available via the `[injection]` extra.
1111

1212
## Installation
1313

1414
⚠️ _Requires Python 3.12 or higher_
15+
16+
Without dependency injection:
1517
```bash
1618
pip install python-cq
1719
```
20+
21+
With [python-injection](https://github.com/100nm/python-injection) as the DI backend (recommended):
22+
```bash
23+
pip install "python-cq[injection]"
24+
```

docs/di.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Custom DI
2+
3+
**python-cq** abstracts dependency injection behind the `DIAdapter` protocol, allowing you to integrate any DI framework.
4+
5+
## The `CQ` class
6+
7+
`CQ` is the central object that binds together the handler registries and the DI adapter. The module-level decorators (`command_handler`, `event_handler`, `query_handler`) and bus factories (`new_command_bus`, `new_event_bus`, `new_query_bus`) all derive from a default `CQ` instance created at import time.
8+
9+
You can create additional isolated instances when you need separate handler registries, for example in tests or in a multi-tenant setup:
10+
11+
```python
12+
from cq import CQ, ContextCommandPipeline
13+
14+
cq = CQ(my_di_adapter).register_defaults()
15+
16+
command_handler = cq.command_handler
17+
event_handler = cq.event_handler
18+
query_handler = cq.query_handler
19+
20+
new_command_bus = cq.new_command_bus
21+
new_event_bus = cq.new_event_bus
22+
new_query_bus = cq.new_query_bus
23+
```
24+
25+
When using `ContextCommandPipeline`, pass `cq.di` explicitly so it uses the same DI adapter:
26+
27+
```python
28+
ContextCommandPipeline(cq.di)
29+
```
30+
31+
## Implementing a `DIAdapter`
32+
33+
`DIAdapter` is a `Protocol`. Implement it to integrate any DI framework:
34+
35+
```python
36+
from collections.abc import Awaitable, Callable
37+
from cq import CQ, DIAdapter, CommandBus, EventBus, QueryBus
38+
from typing import Any, AsyncContextManager
39+
40+
class MyDIAdapter(DIAdapter):
41+
def command_scope(self) -> AsyncContextManager[None]:
42+
# Return an async context manager that:
43+
# - opens a DI scope for the duration of a command dispatch
44+
# - manages the lifecycle of a RelatedEvents instance within that scope
45+
# - silently ignores nested activations (re-entrant calls)
46+
...
47+
48+
def lazy[T](self, tp: type[T]) -> Callable[[], Awaitable[T]]:
49+
# Ask the DI framework for a resolver for `tp`.
50+
# The returned callable, when called and awaited, performs the resolution.
51+
...
52+
53+
def register_defaults(
54+
self,
55+
command_bus: Callable[..., CommandBus[Any]],
56+
event_bus: Callable[..., EventBus],
57+
query_bus: Callable[..., QueryBus[Any]],
58+
) -> None:
59+
# Register the bus factories as default providers so that handlers
60+
# can declare CommandBus, EventBus, or QueryBus as dependencies.
61+
# This method is optional; the default implementation is a no-op.
62+
...
63+
64+
def wire[T](self, tp: type[T]) -> Callable[..., Awaitable[T]]:
65+
# Return an async factory that instantiates `tp` with injected dependencies.
66+
# Used internally to build handler instances.
67+
...
68+
69+
70+
cq = CQ(MyDIAdapter()).register_defaults()
71+
```

docs/guides/configuring.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Configuring a Bus
22

3+
!!! note
4+
This guide assumes the `[injection]` extra is installed.
5+
36
Each bus can be customized with listeners and middlewares. To do so, create a factory function decorated with `@injectable` that returns the configured bus.
47
```python
58
from cq import CommandBus, MiddlewareResult, new_command_bus

docs/guides/dispatching.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Each bus can take a generic parameter to specify the return type of the `dispatc
66

77
## Retrieving a bus
88

9-
Bus instances are available through [python-injection](https://github.com/100nm/python-injection)'s dependency injection:
9+
Bus instances are resolved through the configured DI adapter. When using the `[injection]` extra:
1010

1111
```python
1212
from cq import CommandBus

docs/guides/messages.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class CreateUserHandler:
3030
...
3131
```
3232

33-
All constructor dependencies are resolved at runtime by [python-injection](https://github.com/100nm/python-injection).
33+
All constructor dependencies are resolved at runtime by the configured DI adapter.
3434

3535
### Using NamedTuple for handlers
3636

docs/guides/pipeline.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class PaymentContext:
1515
transaction_id: int
1616

1717
pipeline: ContextCommandPipeline[ValidateCartCommand] = ContextCommandPipeline()
18+
# Optionally pass a custom DIAdapter: ContextCommandPipeline(my_di)
1819

1920
@pipeline.step
2021
def _(self, result: CartValidatedResult) -> CreateTransactionCommand:

docs/index.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![PyPI - Version](https://img.shields.io/pypi/v/python-cq.svg?color=4051b5&style=for-the-badge)](https://pypi.org/project/python-cq)
44
[![PyPI - Downloads](https://img.shields.io/pypi/dm/python-cq.svg?color=4051b5&style=for-the-badge)](https://pypistats.org/packages/python-cq)
55

6-
**python-cq** is a 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.
6+
**python-cq** is a Python package designed to organize your code following CQRS principles. It provides a `DIAdapter` protocol for dependency injection, with [python-injection](https://github.com/100nm/python-injection) as the default implementation available via the `[injection]` extra.
77

88
## What is CQRS?
99

@@ -35,6 +35,13 @@ This knowledge will help you design coherent handlers and organize your code eff
3535
## Installation
3636

3737
Requires Python 3.12 or higher.
38+
39+
Without dependency injection:
3840
```bash
3941
pip install python-cq
4042
```
43+
44+
With [python-injection](https://github.com/100nm/python-injection) as the DI backend (recommended):
45+
```bash
46+
pip install "python-cq[injection]"
47+
```

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ nav:
2121
- Dispatching messages: guides/dispatching.md
2222
- Configuring a bus: guides/configuring.md
2323
- Executing multiple commands: guides/pipeline.md
24+
- Custom DI: di.md
2425

2526
plugins:
2627
- search

0 commit comments

Comments
 (0)