hexastack-cqrs 0.0.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.
Potentially problematic release.
This version of hexastack-cqrs might be problematic. Click here for more details.
- hexastack_cqrs-0.0.0/PKG-INFO +177 -0
- hexastack_cqrs-0.0.0/README.md +164 -0
- hexastack_cqrs-0.0.0/pyproject.toml +59 -0
- hexastack_cqrs-0.0.0/pyproject.toml.orig +60 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/__init__.py +8 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/adapters/__init__.py +19 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/adapters/buses/__init__.py +23 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/adapters/buses/command/__init__.py +13 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/adapters/buses/command/asynchronous.py +131 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/adapters/buses/command/synchronous.py +53 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/adapters/buses/event/__init__.py +17 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/adapters/buses/event/asynchronous.py +231 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/adapters/buses/event/recording.py +68 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/adapters/buses/event/synchronous.py +93 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/adapters/buses/query/__init__.py +5 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/adapters/buses/query/synchronous.py +53 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/domain/__init__.py +7 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/domain/handlers.py +51 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/infra/__init__.py +99 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/infra/autodiscovery.py +173 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/infra/bootstrap.py +267 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/infra/config.py +107 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/infra/decorators.py +226 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/infra/middleware/__init__.py +17 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/infra/middleware/correlation.py +59 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/infra/middleware/feature_flag.py +81 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/infra/middleware/generic.py +32 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/infra/middleware/logging.py +104 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/infra/middleware/retry.py +95 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/infra/middleware/timing.py +91 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/infra/middleware/unit_of_work.py +75 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/infra/pipeline.py +220 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/infra/registries/__init__.py +24 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/infra/registries/command.py +20 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/infra/registries/handler.py +25 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/infra/registries/presenter.py +80 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/infra/registries/query.py +22 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/ports/__init__.py +0 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/ports/buses.py +75 -0
- hexastack_cqrs-0.0.0/src/hexastack_cqrs/py.typed +0 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: hexastack-cqrs
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: CQRS commands, queries, events, buses, and execution pipeline for Hexastack
|
|
5
|
+
Author: Richard West
|
|
6
|
+
Author-email: Richard West <dopplereffect.us@gmail.com>
|
|
7
|
+
Requires-Dist: hexastack-core
|
|
8
|
+
Requires-Dist: tenacity>=9.0.0
|
|
9
|
+
Requires-Dist: huey>=3.3.4 ; extra == 'huey'
|
|
10
|
+
Requires-Python: >=3.13
|
|
11
|
+
Provides-Extra: huey
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# hexastack-cqrs
|
|
15
|
+
|
|
16
|
+
> Command Query Responsibility Segregation (CQRS) buses, execution pipelines, and middleware for Hexastack.
|
|
17
|
+
|
|
18
|
+
[](https://www.python.org/downloads/)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 1. Overview & Capabilities
|
|
23
|
+
|
|
24
|
+
`hexastack-cqrs` powers the application logic layer of Hexastack applications, enforcing a strict separation between write operations (Commands) and read projections (Queries):
|
|
25
|
+
|
|
26
|
+
- **Synchronous & Asynchronous Buses**: `SynchronousCommandBus`, `SynchronousQueryBus`, `SynchronousEventBus`, and async/distributed buses.
|
|
27
|
+
- **Composable Middleware Pipeline**: Pluggable middlewares executed sequentially before and after command/query handling:
|
|
28
|
+
- `CorrelationMiddleware`: Propagates correlation IDs across requests and async tasks.
|
|
29
|
+
- `TimingMiddleware`: Measures and records execution latency.
|
|
30
|
+
- `LoggingMiddleware`: Logs execution lifecycles and errors via `LoggerPort` (dynamically gated via `features.cqrs.logging`).
|
|
31
|
+
- `TenacityRetryMiddleware`: Exponential backoff and retry policies powered by `tenacity` (dynamically gated via `features.cqrs.retry`).
|
|
32
|
+
- `UnitOfWorkMiddleware`: Automatic transaction scoping (`commit()` on success, `rollback()` on failure).
|
|
33
|
+
- `ConditionalFeatureFlagMiddleware`: Evaluates dynamic feature flags before dispatching commands/queries.
|
|
34
|
+
- **Declarative Decorators & Scanning**: `@command_handler`, `@query_handler`, `@event_handler`, and `@feature_flag` registered via reflective module scanning.
|
|
35
|
+
- **Presenter & Response Mapping**: Integration with `PresenterPort` for formatting outputs across CLI and REST adapters.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 2. Package Anatomy & Key Components
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
hexastack_cqrs/
|
|
43
|
+
├── domain/ # Command, Query, Event, Handler protocols, CQRS exceptions
|
|
44
|
+
├── ports/ # CommandBusPort, QueryBusPort, EventBusPort, MiddlewarePort
|
|
45
|
+
├── adapters/ # Buses (Synchronous & Asynchronous with Huey)
|
|
46
|
+
└── infra/ # CqrsBootstrapper (order=20), Middleware pipeline, Registries, @feature_flag
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Key Exports
|
|
50
|
+
|
|
51
|
+
| Category | Exports |
|
|
52
|
+
|---|---|
|
|
53
|
+
| **Adapters** | `SynchronousCommandBus`, `SynchronousQueryBus`, `SynchronousEventBus`, `HueyCommandBus`, `HueyEventBus` |
|
|
54
|
+
| **Bootstrap** | `CqrsBootstrapper` (order=20), `CqrsConfig` |
|
|
55
|
+
| **Decorators** | `@command_handler`, `@query_handler`, `@event_handler`, `@feature_flag`, `@presenter` |
|
|
56
|
+
| **Domain** | `Command`, `Query`, `Event`, `CommandHandler`, `QueryHandler`, `EventHandler` |
|
|
57
|
+
| **Middlewares** | `CorrelationMiddleware`, `TimingMiddleware`, `LoggingMiddleware`, `TenacityRetryMiddleware`, `UnitOfWorkMiddleware`, `ConditionalFeatureFlagMiddleware`, `ExecutionPipeline` |
|
|
58
|
+
| **Ports** | `CommandBusPort`, `QueryBusPort`, `EventBusPort`, `MiddlewarePort` |
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 3. Monorepo & Sibling Relationships
|
|
63
|
+
|
|
64
|
+
```mermaid
|
|
65
|
+
graph TD
|
|
66
|
+
subgraph DrivingAdapters ["Inbound Driving Adapters"]
|
|
67
|
+
FASTAPI["hexastack-fastapi"]
|
|
68
|
+
GRAPHQL["hexastack-graphql"]
|
|
69
|
+
MCP["hexastack-mcp"]
|
|
70
|
+
GRPC["hexastack-grpc"]
|
|
71
|
+
CLI["hexastack-cli"]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
subgraph CQRS ["hexastack-cqrs"]
|
|
75
|
+
CBUS["CommandBusPort"]
|
|
76
|
+
QBUS["QueryBusPort"]
|
|
77
|
+
EBUS["EventBusPort"]
|
|
78
|
+
PIPE["Middleware Pipeline"]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
subgraph DrivenDependencies ["Driven Subsystems & Kernel"]
|
|
82
|
+
CORE["hexastack-core (DI & Kernel)"]
|
|
83
|
+
DB["hexastack-db (UnitOfWorkPort)"]
|
|
84
|
+
LOG["hexastack-logging (LoggerPort)"]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
FASTAPI -->|dispatches to| CBUS
|
|
88
|
+
FASTAPI -->|dispatches to| QBUS
|
|
89
|
+
GRAPHQL -->|dispatches to| CBUS
|
|
90
|
+
GRAPHQL -->|dispatches to| QBUS
|
|
91
|
+
MCP -->|dispatches to| CBUS
|
|
92
|
+
MCP -->|dispatches to| QBUS
|
|
93
|
+
GRPC -->|dispatches to| CBUS
|
|
94
|
+
GRPC -->|dispatches to| QBUS
|
|
95
|
+
CLI -->|dispatches to| CBUS
|
|
96
|
+
CLI -->|dispatches to| QBUS
|
|
97
|
+
|
|
98
|
+
CBUS --> PIPE
|
|
99
|
+
QBUS --> PIPE
|
|
100
|
+
EBUS --> PIPE
|
|
101
|
+
|
|
102
|
+
PIPE -->|managed by| CORE
|
|
103
|
+
PIPE -. consumes from DI .-> DB
|
|
104
|
+
PIPE -. consumes from DI .-> LOG
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Explicit Dependencies (Direct)
|
|
108
|
+
- `hexastack-core`: Core kernel, DI container, base exceptions, and ports.
|
|
109
|
+
- `tenacity>=9.0.0`: Resilient retry policies with exponential backoff.
|
|
110
|
+
|
|
111
|
+
### Implied / Behavioral Relationships (DI-Mediated)
|
|
112
|
+
- **UnitOfWork Scoping**: `UnitOfWorkMiddleware` dynamically resolves `UnitOfWorkPort` from the DI container (injected by `hexastack-db`) to manage transactions.
|
|
113
|
+
- **Telemetry Integration**: `LoggingMiddleware` dynamically resolves `LoggerPort` from the DI container (injected by `hexastack-logging`).
|
|
114
|
+
- **Driving Adapters**: Consumed by `hexastack-fastapi` and `hexastack-cli` to route incoming user actions to business handlers.
|
|
115
|
+
|
|
116
|
+
### Optional Integrations (Extras)
|
|
117
|
+
- `[huey]`: Enables asynchronous, distributed task queue execution using `HueyCommandBus` and `HueyEventBus`.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 4. Installation
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# Standalone installation
|
|
125
|
+
pip install hexastack-cqrs
|
|
126
|
+
|
|
127
|
+
# With asynchronous distributed background worker (Huey)
|
|
128
|
+
pip install "hexastack-cqrs[huey]"
|
|
129
|
+
|
|
130
|
+
# Via umbrella package
|
|
131
|
+
pip install hexastack
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 5. Configuration Reference
|
|
137
|
+
|
|
138
|
+
```toml
|
|
139
|
+
[hexastack.cqrs]
|
|
140
|
+
enable_correlation = true
|
|
141
|
+
enable_timing = true
|
|
142
|
+
enable_logging = true
|
|
143
|
+
enable_uow = true
|
|
144
|
+
retry_attempts = 3
|
|
145
|
+
retry_backoff_base = 0.5
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 6. Quickstart Example
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from dataclasses import dataclass
|
|
154
|
+
from hexastack_core.infra.bootstrap import bootstrap
|
|
155
|
+
from hexastack_cqrs.domain.command import Command
|
|
156
|
+
from hexastack_cqrs.infra.decorators import command_handler
|
|
157
|
+
from hexastack_cqrs.ports.buses import CommandBusPort
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass(frozen=True)
|
|
161
|
+
class CreateOrderCommand(Command):
|
|
162
|
+
order_id: str
|
|
163
|
+
amount: float
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@command_handler(CreateOrderCommand)
|
|
167
|
+
class CreateOrderHandler:
|
|
168
|
+
def __call__(self, cmd: CreateOrderCommand) -> str:
|
|
169
|
+
return f"Order {cmd.order_id} created for ${cmd.amount:.2f}"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
runtime = bootstrap(packages_to_scan=[__name__])
|
|
173
|
+
bus = runtime.container.get(CommandBusPort)
|
|
174
|
+
|
|
175
|
+
result = bus.dispatch(CreateOrderCommand(order_id="ord-99", amount=49.99))
|
|
176
|
+
print(result) # "Order ord-99 created for $49.99"
|
|
177
|
+
```
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# hexastack-cqrs
|
|
2
|
+
|
|
3
|
+
> Command Query Responsibility Segregation (CQRS) buses, execution pipelines, and middleware for Hexastack.
|
|
4
|
+
|
|
5
|
+
[](https://www.python.org/downloads/)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Overview & Capabilities
|
|
10
|
+
|
|
11
|
+
`hexastack-cqrs` powers the application logic layer of Hexastack applications, enforcing a strict separation between write operations (Commands) and read projections (Queries):
|
|
12
|
+
|
|
13
|
+
- **Synchronous & Asynchronous Buses**: `SynchronousCommandBus`, `SynchronousQueryBus`, `SynchronousEventBus`, and async/distributed buses.
|
|
14
|
+
- **Composable Middleware Pipeline**: Pluggable middlewares executed sequentially before and after command/query handling:
|
|
15
|
+
- `CorrelationMiddleware`: Propagates correlation IDs across requests and async tasks.
|
|
16
|
+
- `TimingMiddleware`: Measures and records execution latency.
|
|
17
|
+
- `LoggingMiddleware`: Logs execution lifecycles and errors via `LoggerPort` (dynamically gated via `features.cqrs.logging`).
|
|
18
|
+
- `TenacityRetryMiddleware`: Exponential backoff and retry policies powered by `tenacity` (dynamically gated via `features.cqrs.retry`).
|
|
19
|
+
- `UnitOfWorkMiddleware`: Automatic transaction scoping (`commit()` on success, `rollback()` on failure).
|
|
20
|
+
- `ConditionalFeatureFlagMiddleware`: Evaluates dynamic feature flags before dispatching commands/queries.
|
|
21
|
+
- **Declarative Decorators & Scanning**: `@command_handler`, `@query_handler`, `@event_handler`, and `@feature_flag` registered via reflective module scanning.
|
|
22
|
+
- **Presenter & Response Mapping**: Integration with `PresenterPort` for formatting outputs across CLI and REST adapters.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 2. Package Anatomy & Key Components
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
hexastack_cqrs/
|
|
30
|
+
├── domain/ # Command, Query, Event, Handler protocols, CQRS exceptions
|
|
31
|
+
├── ports/ # CommandBusPort, QueryBusPort, EventBusPort, MiddlewarePort
|
|
32
|
+
├── adapters/ # Buses (Synchronous & Asynchronous with Huey)
|
|
33
|
+
└── infra/ # CqrsBootstrapper (order=20), Middleware pipeline, Registries, @feature_flag
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Key Exports
|
|
37
|
+
|
|
38
|
+
| Category | Exports |
|
|
39
|
+
|---|---|
|
|
40
|
+
| **Adapters** | `SynchronousCommandBus`, `SynchronousQueryBus`, `SynchronousEventBus`, `HueyCommandBus`, `HueyEventBus` |
|
|
41
|
+
| **Bootstrap** | `CqrsBootstrapper` (order=20), `CqrsConfig` |
|
|
42
|
+
| **Decorators** | `@command_handler`, `@query_handler`, `@event_handler`, `@feature_flag`, `@presenter` |
|
|
43
|
+
| **Domain** | `Command`, `Query`, `Event`, `CommandHandler`, `QueryHandler`, `EventHandler` |
|
|
44
|
+
| **Middlewares** | `CorrelationMiddleware`, `TimingMiddleware`, `LoggingMiddleware`, `TenacityRetryMiddleware`, `UnitOfWorkMiddleware`, `ConditionalFeatureFlagMiddleware`, `ExecutionPipeline` |
|
|
45
|
+
| **Ports** | `CommandBusPort`, `QueryBusPort`, `EventBusPort`, `MiddlewarePort` |
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 3. Monorepo & Sibling Relationships
|
|
50
|
+
|
|
51
|
+
```mermaid
|
|
52
|
+
graph TD
|
|
53
|
+
subgraph DrivingAdapters ["Inbound Driving Adapters"]
|
|
54
|
+
FASTAPI["hexastack-fastapi"]
|
|
55
|
+
GRAPHQL["hexastack-graphql"]
|
|
56
|
+
MCP["hexastack-mcp"]
|
|
57
|
+
GRPC["hexastack-grpc"]
|
|
58
|
+
CLI["hexastack-cli"]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
subgraph CQRS ["hexastack-cqrs"]
|
|
62
|
+
CBUS["CommandBusPort"]
|
|
63
|
+
QBUS["QueryBusPort"]
|
|
64
|
+
EBUS["EventBusPort"]
|
|
65
|
+
PIPE["Middleware Pipeline"]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
subgraph DrivenDependencies ["Driven Subsystems & Kernel"]
|
|
69
|
+
CORE["hexastack-core (DI & Kernel)"]
|
|
70
|
+
DB["hexastack-db (UnitOfWorkPort)"]
|
|
71
|
+
LOG["hexastack-logging (LoggerPort)"]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
FASTAPI -->|dispatches to| CBUS
|
|
75
|
+
FASTAPI -->|dispatches to| QBUS
|
|
76
|
+
GRAPHQL -->|dispatches to| CBUS
|
|
77
|
+
GRAPHQL -->|dispatches to| QBUS
|
|
78
|
+
MCP -->|dispatches to| CBUS
|
|
79
|
+
MCP -->|dispatches to| QBUS
|
|
80
|
+
GRPC -->|dispatches to| CBUS
|
|
81
|
+
GRPC -->|dispatches to| QBUS
|
|
82
|
+
CLI -->|dispatches to| CBUS
|
|
83
|
+
CLI -->|dispatches to| QBUS
|
|
84
|
+
|
|
85
|
+
CBUS --> PIPE
|
|
86
|
+
QBUS --> PIPE
|
|
87
|
+
EBUS --> PIPE
|
|
88
|
+
|
|
89
|
+
PIPE -->|managed by| CORE
|
|
90
|
+
PIPE -. consumes from DI .-> DB
|
|
91
|
+
PIPE -. consumes from DI .-> LOG
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Explicit Dependencies (Direct)
|
|
95
|
+
- `hexastack-core`: Core kernel, DI container, base exceptions, and ports.
|
|
96
|
+
- `tenacity>=9.0.0`: Resilient retry policies with exponential backoff.
|
|
97
|
+
|
|
98
|
+
### Implied / Behavioral Relationships (DI-Mediated)
|
|
99
|
+
- **UnitOfWork Scoping**: `UnitOfWorkMiddleware` dynamically resolves `UnitOfWorkPort` from the DI container (injected by `hexastack-db`) to manage transactions.
|
|
100
|
+
- **Telemetry Integration**: `LoggingMiddleware` dynamically resolves `LoggerPort` from the DI container (injected by `hexastack-logging`).
|
|
101
|
+
- **Driving Adapters**: Consumed by `hexastack-fastapi` and `hexastack-cli` to route incoming user actions to business handlers.
|
|
102
|
+
|
|
103
|
+
### Optional Integrations (Extras)
|
|
104
|
+
- `[huey]`: Enables asynchronous, distributed task queue execution using `HueyCommandBus` and `HueyEventBus`.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 4. Installation
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# Standalone installation
|
|
112
|
+
pip install hexastack-cqrs
|
|
113
|
+
|
|
114
|
+
# With asynchronous distributed background worker (Huey)
|
|
115
|
+
pip install "hexastack-cqrs[huey]"
|
|
116
|
+
|
|
117
|
+
# Via umbrella package
|
|
118
|
+
pip install hexastack
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 5. Configuration Reference
|
|
124
|
+
|
|
125
|
+
```toml
|
|
126
|
+
[hexastack.cqrs]
|
|
127
|
+
enable_correlation = true
|
|
128
|
+
enable_timing = true
|
|
129
|
+
enable_logging = true
|
|
130
|
+
enable_uow = true
|
|
131
|
+
retry_attempts = 3
|
|
132
|
+
retry_backoff_base = 0.5
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## 6. Quickstart Example
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from dataclasses import dataclass
|
|
141
|
+
from hexastack_core.infra.bootstrap import bootstrap
|
|
142
|
+
from hexastack_cqrs.domain.command import Command
|
|
143
|
+
from hexastack_cqrs.infra.decorators import command_handler
|
|
144
|
+
from hexastack_cqrs.ports.buses import CommandBusPort
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass(frozen=True)
|
|
148
|
+
class CreateOrderCommand(Command):
|
|
149
|
+
order_id: str
|
|
150
|
+
amount: float
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@command_handler(CreateOrderCommand)
|
|
154
|
+
class CreateOrderHandler:
|
|
155
|
+
def __call__(self, cmd: CreateOrderCommand) -> str:
|
|
156
|
+
return f"Order {cmd.order_id} created for ${cmd.amount:.2f}"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
runtime = bootstrap(packages_to_scan=[__name__])
|
|
160
|
+
bus = runtime.container.get(CommandBusPort)
|
|
161
|
+
|
|
162
|
+
result = bus.dispatch(CreateOrderCommand(order_id="ord-99", amount=49.99))
|
|
163
|
+
print(result) # "Order ord-99 created for $49.99"
|
|
164
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hexastack-cqrs"
|
|
3
|
+
version = "0.0.0"
|
|
4
|
+
description = "CQRS commands, queries, events, buses, and execution pipeline for Hexastack"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"hexastack-core",
|
|
9
|
+
"tenacity>=9.0.0",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[[project.authors]]
|
|
13
|
+
name = "Richard West"
|
|
14
|
+
email = "dopplereffect.us@gmail.com"
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
huey = ["huey>=3.3.4"]
|
|
18
|
+
|
|
19
|
+
[project.entry-points."hexastack.bootstrappers"]
|
|
20
|
+
cqrs = "hexastack_cqrs.infra.bootstrap:CqrsBootstrapper"
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["uv_build>=0.12.3,<0.13.0"]
|
|
24
|
+
build-backend = "uv_build"
|
|
25
|
+
|
|
26
|
+
[tool.uv.sources.hexastack-core]
|
|
27
|
+
workspace = true
|
|
28
|
+
|
|
29
|
+
[tool.importlinter]
|
|
30
|
+
root_packages = ["hexastack_cqrs"]
|
|
31
|
+
|
|
32
|
+
[[tool.importlinter.contracts]]
|
|
33
|
+
name = "Hexagonal architecture layer hierarchy"
|
|
34
|
+
type = "layers"
|
|
35
|
+
containers = ["hexastack_cqrs"]
|
|
36
|
+
layers = [
|
|
37
|
+
"adapters",
|
|
38
|
+
"ports",
|
|
39
|
+
"domain",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[[tool.importlinter.contracts]]
|
|
43
|
+
name = "Forbidden imports for domain"
|
|
44
|
+
type = "forbidden"
|
|
45
|
+
source_modules = ["hexastack_cqrs.domain"]
|
|
46
|
+
forbidden_modules = [
|
|
47
|
+
"hexastack_cqrs.ports",
|
|
48
|
+
"hexastack_cqrs.adapters",
|
|
49
|
+
"hexastack_cqrs.infra",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[[tool.importlinter.contracts]]
|
|
53
|
+
name = "Forbidden imports for ports"
|
|
54
|
+
type = "forbidden"
|
|
55
|
+
source_modules = ["hexastack_cqrs.ports"]
|
|
56
|
+
forbidden_modules = [
|
|
57
|
+
"hexastack_cqrs.adapters",
|
|
58
|
+
"hexastack_cqrs.infra",
|
|
59
|
+
]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hexastack-cqrs"
|
|
3
|
+
version = "0.0.0"
|
|
4
|
+
description = "CQRS commands, queries, events, buses, and execution pipeline for Hexastack"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Richard West", email = "dopplereffect.us@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.13"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"hexastack-core",
|
|
12
|
+
"tenacity>=9.0.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
huey = [
|
|
17
|
+
"huey>=3.3.4",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.entry-points."hexastack.bootstrappers"]
|
|
21
|
+
cqrs = "hexastack_cqrs.infra.bootstrap:CqrsBootstrapper"
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["uv_build>=0.12.3,<0.13.0"]
|
|
25
|
+
build-backend = "uv_build"
|
|
26
|
+
|
|
27
|
+
[tool.uv.sources]
|
|
28
|
+
hexastack-core = { workspace = true }
|
|
29
|
+
|
|
30
|
+
[tool.importlinter]
|
|
31
|
+
root_packages = ["hexastack_cqrs"]
|
|
32
|
+
|
|
33
|
+
[[tool.importlinter.contracts]]
|
|
34
|
+
name = "Hexagonal architecture layer hierarchy"
|
|
35
|
+
type = "layers"
|
|
36
|
+
containers = ["hexastack_cqrs"]
|
|
37
|
+
layers = [
|
|
38
|
+
"adapters",
|
|
39
|
+
"ports",
|
|
40
|
+
"domain",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[[tool.importlinter.contracts]]
|
|
44
|
+
name = "Forbidden imports for domain"
|
|
45
|
+
type = "forbidden"
|
|
46
|
+
source_modules = ["hexastack_cqrs.domain"]
|
|
47
|
+
forbidden_modules = [
|
|
48
|
+
"hexastack_cqrs.ports",
|
|
49
|
+
"hexastack_cqrs.adapters",
|
|
50
|
+
"hexastack_cqrs.infra",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[[tool.importlinter.contracts]]
|
|
54
|
+
name = "Forbidden imports for ports"
|
|
55
|
+
type = "forbidden"
|
|
56
|
+
source_modules = ["hexastack_cqrs.ports"]
|
|
57
|
+
forbidden_modules = [
|
|
58
|
+
"hexastack_cqrs.adapters",
|
|
59
|
+
"hexastack_cqrs.infra",
|
|
60
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from hexastack_cqrs.adapters.buses import (
|
|
2
|
+
AsyncNativeCommandBus,
|
|
3
|
+
AsyncNativeEventBus,
|
|
4
|
+
HueyCommandBus,
|
|
5
|
+
HueyEventBus,
|
|
6
|
+
SynchronousCommandBus,
|
|
7
|
+
SynchronousEventBus,
|
|
8
|
+
SynchronousQueryBus,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"AsyncNativeCommandBus",
|
|
13
|
+
"AsyncNativeEventBus",
|
|
14
|
+
"HueyCommandBus",
|
|
15
|
+
"HueyEventBus",
|
|
16
|
+
"SynchronousCommandBus",
|
|
17
|
+
"SynchronousEventBus",
|
|
18
|
+
"SynchronousQueryBus",
|
|
19
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from hexastack_cqrs.adapters.buses.command import (
|
|
2
|
+
AsyncNativeCommandBus,
|
|
3
|
+
HueyCommandBus,
|
|
4
|
+
SynchronousCommandBus,
|
|
5
|
+
)
|
|
6
|
+
from hexastack_cqrs.adapters.buses.event import (
|
|
7
|
+
AsyncNativeEventBus,
|
|
8
|
+
HueyEventBus,
|
|
9
|
+
RecordingEventBus,
|
|
10
|
+
SynchronousEventBus,
|
|
11
|
+
)
|
|
12
|
+
from hexastack_cqrs.adapters.buses.query import SynchronousQueryBus
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"AsyncNativeCommandBus",
|
|
16
|
+
"AsyncNativeEventBus",
|
|
17
|
+
"HueyCommandBus",
|
|
18
|
+
"HueyEventBus",
|
|
19
|
+
"RecordingEventBus",
|
|
20
|
+
"SynchronousCommandBus",
|
|
21
|
+
"SynchronousEventBus",
|
|
22
|
+
"SynchronousQueryBus",
|
|
23
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from hexastack_cqrs.adapters.buses.command.asynchronous import (
|
|
2
|
+
AsyncNativeCommandBus,
|
|
3
|
+
HueyCommandBus,
|
|
4
|
+
)
|
|
5
|
+
from hexastack_cqrs.adapters.buses.command.synchronous import (
|
|
6
|
+
SynchronousCommandBus,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"AsyncNativeCommandBus",
|
|
11
|
+
"HueyCommandBus",
|
|
12
|
+
"SynchronousCommandBus",
|
|
13
|
+
]
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from concurrent.futures import Future, ThreadPoolExecutor
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from hexastack_core.domain import Command, Generic
|
|
7
|
+
from hexastack_core.domain.exceptions import MissingDependencyError
|
|
8
|
+
from hexastack_cqrs.infra.middleware.generic import GenericMiddleware
|
|
9
|
+
from hexastack_cqrs.infra.registries.handler import HandlerRegistry
|
|
10
|
+
from hexastack_cqrs.ports.buses import CommandBusPort
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from huey import Huey
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HueyCommandBus(CommandBusPort):
|
|
17
|
+
"""Asynchronous CommandBus adapter dispatching commands via Huey task queue.
|
|
18
|
+
|
|
19
|
+
Notes/Architectural Intent:
|
|
20
|
+
Delegates command execution to background worker processes via Huey,
|
|
21
|
+
enabling distributed, durable asynchronous background processing.
|
|
22
|
+
Requires hexastack-cqrs[huey].
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
huey: "Huey",
|
|
28
|
+
handler_registry: HandlerRegistry,
|
|
29
|
+
middleware: list[GenericMiddleware] | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Initialize Huey command bus with Huey instance and handler registry.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
huey: Initialized Huey task queue instance.
|
|
35
|
+
handler_registry: HandlerRegistry containing registered command handlers.
|
|
36
|
+
middleware: Optional ordered list of GenericMiddleware interceptors.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
MissingDependencyError: If huey package is not installed.
|
|
40
|
+
"""
|
|
41
|
+
if importlib.util.find_spec("huey") is None:
|
|
42
|
+
raise MissingDependencyError(
|
|
43
|
+
"huey is required to use HueyCommandBus. "
|
|
44
|
+
"Install via 'pip install hexastack-cqrs[huey]'."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
self._huey = huey
|
|
48
|
+
self._registry = handler_registry
|
|
49
|
+
self._middleware = list(middleware) if middleware is not None else []
|
|
50
|
+
|
|
51
|
+
@self._huey.task()
|
|
52
|
+
def _execute_command_task(cmd: Command) -> Any:
|
|
53
|
+
pipeline: Callable[[Generic], Any] = lambda inst: self._registry.handle(
|
|
54
|
+
inst, reraise=True
|
|
55
|
+
)
|
|
56
|
+
for mw in reversed(self._middleware):
|
|
57
|
+
next_fn = pipeline
|
|
58
|
+
pipeline = lambda inst, m=mw, n=next_fn: m(inst, n)
|
|
59
|
+
return pipeline(cmd)
|
|
60
|
+
|
|
61
|
+
self._task_fn = _execute_command_task
|
|
62
|
+
|
|
63
|
+
def dispatch(self, command: Command) -> Any:
|
|
64
|
+
"""Enqueue command to Huey background queue for execution.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
command: The command instance to enqueue.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Huey Task object representing the background job.
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
Exception: If enqueuing to Huey queue fails.
|
|
74
|
+
"""
|
|
75
|
+
return self._task_fn(command)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class AsyncNativeCommandBus(CommandBusPort):
|
|
79
|
+
"""Asynchronous in-process CommandBus using thread pool execution.
|
|
80
|
+
|
|
81
|
+
Notes/Architectural Intent:
|
|
82
|
+
Provides non-blocking in-process command execution without requiring external
|
|
83
|
+
queue backends, returning a standard concurrent.futures.Future.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(
|
|
87
|
+
self,
|
|
88
|
+
handler_registry: HandlerRegistry,
|
|
89
|
+
middleware: list[GenericMiddleware] | None = None,
|
|
90
|
+
executor: ThreadPoolExecutor | None = None,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Initialize native async command bus.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
handler_registry: HandlerRegistry containing registered command handlers.
|
|
96
|
+
middleware: Optional ordered list of GenericMiddleware interceptors.
|
|
97
|
+
executor: Optional ThreadPoolExecutor for managing worker threads.
|
|
98
|
+
"""
|
|
99
|
+
self._registry = handler_registry
|
|
100
|
+
self._middleware = list(middleware) if middleware is not None else []
|
|
101
|
+
self._executor = executor or ThreadPoolExecutor(thread_name_prefix="cqrs-async")
|
|
102
|
+
|
|
103
|
+
def dispatch(self, command: Command) -> Future[Any]:
|
|
104
|
+
"""Submit command for asynchronous background execution on thread pool.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
command: The command instance to dispatch.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Future resolving to the handler execution result.
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
Exception: If submitting task to thread pool executor fails.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def _run() -> Any:
|
|
117
|
+
pipeline: Callable[[Generic], Any] = lambda inst: self._registry.handle(
|
|
118
|
+
inst, reraise=True
|
|
119
|
+
)
|
|
120
|
+
for mw in reversed(self._middleware):
|
|
121
|
+
next_fn = pipeline
|
|
122
|
+
pipeline = lambda inst, m=mw, n=next_fn: m(inst, n)
|
|
123
|
+
return pipeline(command)
|
|
124
|
+
|
|
125
|
+
return self._executor.submit(_run)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
__all__ = [
|
|
129
|
+
"AsyncNativeCommandBus",
|
|
130
|
+
"HueyCommandBus",
|
|
131
|
+
]
|