hexastack-flow 0.5.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.
- hexastack_flow-0.5.0/PKG-INFO +133 -0
- hexastack_flow-0.5.0/README.md +113 -0
- hexastack_flow-0.5.0/pyproject.toml +57 -0
- hexastack_flow-0.5.0/pyproject.toml.orig +54 -0
- hexastack_flow-0.5.0/src/hexastack_flow/__init__.py +59 -0
- hexastack_flow-0.5.0/src/hexastack_flow/adapters/__init__.py +29 -0
- hexastack_flow-0.5.0/src/hexastack_flow/adapters/cqrs/__init__.py +21 -0
- hexastack_flow-0.5.0/src/hexastack_flow/adapters/cqrs/runner.py +212 -0
- hexastack_flow-0.5.0/src/hexastack_flow/adapters/cqrs/steps.py +200 -0
- hexastack_flow-0.5.0/src/hexastack_flow/adapters/events/__init__.py +11 -0
- hexastack_flow-0.5.0/src/hexastack_flow/adapters/events/publisher.py +151 -0
- hexastack_flow-0.5.0/src/hexastack_flow/adapters/storage/__init__.py +12 -0
- hexastack_flow-0.5.0/src/hexastack_flow/adapters/storage/sqlalchemy.py +386 -0
- hexastack_flow-0.5.0/src/hexastack_flow/domain/__init__.py +28 -0
- hexastack_flow-0.5.0/src/hexastack_flow/domain/events.py +95 -0
- hexastack_flow-0.5.0/src/hexastack_flow/domain/models.py +77 -0
- hexastack_flow-0.5.0/src/hexastack_flow/infra/__init__.py +11 -0
- hexastack_flow-0.5.0/src/hexastack_flow/infra/bootstrap.py +53 -0
- hexastack_flow-0.5.0/src/hexastack_flow/ports/__init__.py +11 -0
- hexastack_flow-0.5.0/src/hexastack_flow/ports/orchestrator.py +90 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hexastack-flow
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Hexagonal workflow engine adapter integrating hexaflow with CQRS, DB, and Events
|
|
5
|
+
Author: Richard West
|
|
6
|
+
Author-email: Richard West <dopplereffect.us@gmail.com>
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Requires-Dist: hexaflow>=0.2.0
|
|
9
|
+
Requires-Dist: hexastack-core
|
|
10
|
+
Requires-Dist: hexastack-cqrs
|
|
11
|
+
Requires-Dist: hexastack-flow[db,events] ; extra == 'all'
|
|
12
|
+
Requires-Dist: hexastack-db ; extra == 'db'
|
|
13
|
+
Requires-Dist: sqlalchemy>=2.0.0 ; extra == 'db'
|
|
14
|
+
Requires-Dist: hexastack-events ; extra == 'events'
|
|
15
|
+
Requires-Python: >=3.13
|
|
16
|
+
Provides-Extra: all
|
|
17
|
+
Provides-Extra: db
|
|
18
|
+
Provides-Extra: events
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# 🌊 `hexastack-flow`
|
|
22
|
+
|
|
23
|
+
> Hexagonal workflow engine adapter integrating `hexaflow` with Hexastack CQRS, Relational Persistence, and Event Streaming.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 🎯 Architectural Intent
|
|
28
|
+
|
|
29
|
+
`hexastack-flow` provides first-class hexagonal adapters connecting the lightweight [`hexaflow`](https://pypi.org/project/hexaflow/) workflow engine to Hexastack:
|
|
30
|
+
|
|
31
|
+
1. **CQRS Bridge**: `CommandStep` and `QueryStep` allow workflow steps to dispatch typed commands and queries directly onto the `CommandBus` and `QueryBus`, inheriting all CQRS middleware (auth, tracing, logging, metrics, retries).
|
|
32
|
+
2. **Durable Distributed Sagas**: Workflows natively subsume the Saga pattern by pairing forward command execution stages with compensating rollback stages, backed by durable checkpoints.
|
|
33
|
+
3. **Enterprise Persistence**: `SqlAlchemyWorkflowStore` persists workflow execution records and step checkpoints directly to PostgreSQL/SQLite/MySQL within transactional boundaries.
|
|
34
|
+
4. **Event Streaming**: Emits domain events (`WorkflowStartedEvent`, `WorkflowStepCompletedEvent`, `WorkflowSuspendedEvent`, `WorkflowCompletedEvent`, `WorkflowAbortedEvent`) wrapped in standard `CloudEventEnvelope`s through `hexastack-events`.
|
|
35
|
+
5. **DevTools Visualizer**: Backing engine for NiceGUI and Textual DAG monitoring.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 🔄 Implementing the Saga Pattern as a Workflow
|
|
40
|
+
|
|
41
|
+
Historically, the **Saga Pattern** structures a distributed business transaction across multiple microservices or bounded contexts as a sequence of local transactions:
|
|
42
|
+
- **Forward transactions**: $T_1, T_2, \dots, T_n$
|
|
43
|
+
- **Compensating transactions**: $C_{k-1}, \dots, C_1$ executed in reverse (LIFO) order if step $k$ fails.
|
|
44
|
+
|
|
45
|
+
In `hexastack-flow`, you do **not** need a separate, restricted saga engine. A Saga is simply a workflow DAG that dispatches CQRS commands and executes compensating commands on failure.
|
|
46
|
+
|
|
47
|
+
### Example: Trip Booking Saga with CQRS & Hexaflow
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from hexaflow import Workflow
|
|
51
|
+
from hexastack_cqrs.ports.buses import CommandBusPort
|
|
52
|
+
from hexastack_flow.adapters.cqrs.runner import CqrsWorkflowRunner
|
|
53
|
+
from hexastack_flow.adapters.cqrs.steps import as_command_step
|
|
54
|
+
|
|
55
|
+
# 1. Define CQRS commands for forward actions & compensations
|
|
56
|
+
from my_domain.commands import (
|
|
57
|
+
BookFlightCommand,
|
|
58
|
+
CancelFlightCommand,
|
|
59
|
+
BookHotelCommand,
|
|
60
|
+
CancelHotelCommand,
|
|
61
|
+
ChargePaymentCommand,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def build_trip_booking_workflow(bus: CommandBusPort, customer_id: str) -> Workflow:
|
|
65
|
+
"""Construct a durable distributed saga as a hexaflow Workflow."""
|
|
66
|
+
workflow = (
|
|
67
|
+
Workflow("trip-booking-saga")
|
|
68
|
+
# Step 1: Reserve Flight
|
|
69
|
+
.stage(
|
|
70
|
+
"flight",
|
|
71
|
+
as_command_step(
|
|
72
|
+
lambda ctx: BookFlightCommand(customer_id=customer_id, destination="JFK"),
|
|
73
|
+
bus,
|
|
74
|
+
name="book_flight",
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
# Step 2: Reserve Hotel (runs after flight is confirmed)
|
|
78
|
+
.stage(
|
|
79
|
+
"hotel",
|
|
80
|
+
as_command_step(
|
|
81
|
+
lambda ctx: BookHotelCommand(customer_id=customer_id, room_type="deluxe"),
|
|
82
|
+
bus,
|
|
83
|
+
name="book_hotel",
|
|
84
|
+
),
|
|
85
|
+
depends_on=["flight"],
|
|
86
|
+
)
|
|
87
|
+
# Step 3: Process Payment
|
|
88
|
+
.stage(
|
|
89
|
+
"payment",
|
|
90
|
+
as_command_step(
|
|
91
|
+
lambda ctx: ChargePaymentCommand(customer_id=customer_id, amount_cents=50000),
|
|
92
|
+
bus,
|
|
93
|
+
name="charge_payment",
|
|
94
|
+
),
|
|
95
|
+
depends_on=["hotel"],
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
return workflow
|
|
99
|
+
|
|
100
|
+
# 2. Execute via CqrsWorkflowRunner (with durable DB checkpoints)
|
|
101
|
+
runner = CqrsWorkflowRunner(engine=engine)
|
|
102
|
+
result = await runner.run(workflow.build())
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Why Workflows are Superior to Linear Sagas
|
|
106
|
+
|
|
107
|
+
| Feature | Legacy In-Memory Saga | `hexastack-flow` Workflow |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| **Execution Topologies** | Strictly sequential ($T_1 \to T_2 \to T_3$) | **Arbitrary DAGs** (e.g. reserve Flight & Hotel in parallel) |
|
|
110
|
+
| **Crash Recovery** | In-memory only (lost if process restarts) | **Durable Checkpoints** (`SqlAlchemyWorkflowStore`) |
|
|
111
|
+
| **Step Retries** | Manual loop or fail immediately | **Configurable `RetryPolicy`** (exponential backoff, jitter) |
|
|
112
|
+
| **Observability** | Console logs | **CloudEvents** (`WorkflowStepCompletedEvent`) + OTel spans |
|
|
113
|
+
| **Human-in-the-Loop** | Not supported | **Pause / Resume** (`engine.suspend()`, `engine.resume()`) |
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 📦 Installation
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
pip install hexastack[flow]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Or standalone:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
pip install hexastack-flow
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## 📄 License
|
|
132
|
+
|
|
133
|
+
Apache 2.0
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# 🌊 `hexastack-flow`
|
|
2
|
+
|
|
3
|
+
> Hexagonal workflow engine adapter integrating `hexaflow` with Hexastack CQRS, Relational Persistence, and Event Streaming.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🎯 Architectural Intent
|
|
8
|
+
|
|
9
|
+
`hexastack-flow` provides first-class hexagonal adapters connecting the lightweight [`hexaflow`](https://pypi.org/project/hexaflow/) workflow engine to Hexastack:
|
|
10
|
+
|
|
11
|
+
1. **CQRS Bridge**: `CommandStep` and `QueryStep` allow workflow steps to dispatch typed commands and queries directly onto the `CommandBus` and `QueryBus`, inheriting all CQRS middleware (auth, tracing, logging, metrics, retries).
|
|
12
|
+
2. **Durable Distributed Sagas**: Workflows natively subsume the Saga pattern by pairing forward command execution stages with compensating rollback stages, backed by durable checkpoints.
|
|
13
|
+
3. **Enterprise Persistence**: `SqlAlchemyWorkflowStore` persists workflow execution records and step checkpoints directly to PostgreSQL/SQLite/MySQL within transactional boundaries.
|
|
14
|
+
4. **Event Streaming**: Emits domain events (`WorkflowStartedEvent`, `WorkflowStepCompletedEvent`, `WorkflowSuspendedEvent`, `WorkflowCompletedEvent`, `WorkflowAbortedEvent`) wrapped in standard `CloudEventEnvelope`s through `hexastack-events`.
|
|
15
|
+
5. **DevTools Visualizer**: Backing engine for NiceGUI and Textual DAG monitoring.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 🔄 Implementing the Saga Pattern as a Workflow
|
|
20
|
+
|
|
21
|
+
Historically, the **Saga Pattern** structures a distributed business transaction across multiple microservices or bounded contexts as a sequence of local transactions:
|
|
22
|
+
- **Forward transactions**: $T_1, T_2, \dots, T_n$
|
|
23
|
+
- **Compensating transactions**: $C_{k-1}, \dots, C_1$ executed in reverse (LIFO) order if step $k$ fails.
|
|
24
|
+
|
|
25
|
+
In `hexastack-flow`, you do **not** need a separate, restricted saga engine. A Saga is simply a workflow DAG that dispatches CQRS commands and executes compensating commands on failure.
|
|
26
|
+
|
|
27
|
+
### Example: Trip Booking Saga with CQRS & Hexaflow
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from hexaflow import Workflow
|
|
31
|
+
from hexastack_cqrs.ports.buses import CommandBusPort
|
|
32
|
+
from hexastack_flow.adapters.cqrs.runner import CqrsWorkflowRunner
|
|
33
|
+
from hexastack_flow.adapters.cqrs.steps import as_command_step
|
|
34
|
+
|
|
35
|
+
# 1. Define CQRS commands for forward actions & compensations
|
|
36
|
+
from my_domain.commands import (
|
|
37
|
+
BookFlightCommand,
|
|
38
|
+
CancelFlightCommand,
|
|
39
|
+
BookHotelCommand,
|
|
40
|
+
CancelHotelCommand,
|
|
41
|
+
ChargePaymentCommand,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def build_trip_booking_workflow(bus: CommandBusPort, customer_id: str) -> Workflow:
|
|
45
|
+
"""Construct a durable distributed saga as a hexaflow Workflow."""
|
|
46
|
+
workflow = (
|
|
47
|
+
Workflow("trip-booking-saga")
|
|
48
|
+
# Step 1: Reserve Flight
|
|
49
|
+
.stage(
|
|
50
|
+
"flight",
|
|
51
|
+
as_command_step(
|
|
52
|
+
lambda ctx: BookFlightCommand(customer_id=customer_id, destination="JFK"),
|
|
53
|
+
bus,
|
|
54
|
+
name="book_flight",
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
# Step 2: Reserve Hotel (runs after flight is confirmed)
|
|
58
|
+
.stage(
|
|
59
|
+
"hotel",
|
|
60
|
+
as_command_step(
|
|
61
|
+
lambda ctx: BookHotelCommand(customer_id=customer_id, room_type="deluxe"),
|
|
62
|
+
bus,
|
|
63
|
+
name="book_hotel",
|
|
64
|
+
),
|
|
65
|
+
depends_on=["flight"],
|
|
66
|
+
)
|
|
67
|
+
# Step 3: Process Payment
|
|
68
|
+
.stage(
|
|
69
|
+
"payment",
|
|
70
|
+
as_command_step(
|
|
71
|
+
lambda ctx: ChargePaymentCommand(customer_id=customer_id, amount_cents=50000),
|
|
72
|
+
bus,
|
|
73
|
+
name="charge_payment",
|
|
74
|
+
),
|
|
75
|
+
depends_on=["hotel"],
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
return workflow
|
|
79
|
+
|
|
80
|
+
# 2. Execute via CqrsWorkflowRunner (with durable DB checkpoints)
|
|
81
|
+
runner = CqrsWorkflowRunner(engine=engine)
|
|
82
|
+
result = await runner.run(workflow.build())
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Why Workflows are Superior to Linear Sagas
|
|
86
|
+
|
|
87
|
+
| Feature | Legacy In-Memory Saga | `hexastack-flow` Workflow |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| **Execution Topologies** | Strictly sequential ($T_1 \to T_2 \to T_3$) | **Arbitrary DAGs** (e.g. reserve Flight & Hotel in parallel) |
|
|
90
|
+
| **Crash Recovery** | In-memory only (lost if process restarts) | **Durable Checkpoints** (`SqlAlchemyWorkflowStore`) |
|
|
91
|
+
| **Step Retries** | Manual loop or fail immediately | **Configurable `RetryPolicy`** (exponential backoff, jitter) |
|
|
92
|
+
| **Observability** | Console logs | **CloudEvents** (`WorkflowStepCompletedEvent`) + OTel spans |
|
|
93
|
+
| **Human-in-the-Loop** | Not supported | **Pause / Resume** (`engine.suspend()`, `engine.resume()`) |
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 📦 Installation
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pip install hexastack[flow]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Or standalone:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
pip install hexastack-flow
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 📄 License
|
|
112
|
+
|
|
113
|
+
Apache 2.0
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hexastack-flow"
|
|
3
|
+
version = "0.5.0"
|
|
4
|
+
description = "Hexagonal workflow engine adapter integrating hexaflow with CQRS, DB, and Events"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
requires-python = ">=3.13"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"hexaflow>=0.2.0",
|
|
10
|
+
"hexastack-core",
|
|
11
|
+
"hexastack-cqrs",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[[project.authors]]
|
|
15
|
+
name = "Richard West"
|
|
16
|
+
email = "dopplereffect.us@gmail.com"
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
db = [
|
|
20
|
+
"hexastack-db",
|
|
21
|
+
"sqlalchemy>=2.0.0",
|
|
22
|
+
]
|
|
23
|
+
events = ["hexastack-events"]
|
|
24
|
+
all = ["hexastack-flow[db,events]"]
|
|
25
|
+
|
|
26
|
+
[project.entry-points."hexastack.bootstrappers"]
|
|
27
|
+
flow = "hexastack_flow.infra.bootstrap:FlowBootstrapper"
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["uv_build>=0.12.3,<0.13.0"]
|
|
31
|
+
build-backend = "uv_build"
|
|
32
|
+
|
|
33
|
+
[tool.uv.sources.hexastack-core]
|
|
34
|
+
workspace = true
|
|
35
|
+
|
|
36
|
+
[tool.uv.sources.hexastack-cqrs]
|
|
37
|
+
workspace = true
|
|
38
|
+
|
|
39
|
+
[tool.uv.sources.hexastack-db]
|
|
40
|
+
workspace = true
|
|
41
|
+
|
|
42
|
+
[tool.uv.sources.hexastack-events]
|
|
43
|
+
workspace = true
|
|
44
|
+
|
|
45
|
+
[tool.importlinter]
|
|
46
|
+
root_packages = ["hexastack_flow"]
|
|
47
|
+
|
|
48
|
+
[[tool.importlinter.contracts]]
|
|
49
|
+
name = "Hexagonal architecture layer hierarchy"
|
|
50
|
+
type = "layers"
|
|
51
|
+
containers = ["hexastack_flow"]
|
|
52
|
+
layers = [
|
|
53
|
+
"infra",
|
|
54
|
+
"adapters",
|
|
55
|
+
"ports",
|
|
56
|
+
"domain",
|
|
57
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hexastack-flow"
|
|
3
|
+
version = "0.5.0"
|
|
4
|
+
description = "Hexagonal workflow engine adapter integrating hexaflow with CQRS, DB, and Events"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Richard West", email = "dopplereffect.us@gmail.com" }
|
|
9
|
+
]
|
|
10
|
+
requires-python = ">=3.13"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"hexaflow>=0.2.0",
|
|
13
|
+
"hexastack-core",
|
|
14
|
+
"hexastack-cqrs",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
db = [
|
|
19
|
+
"hexastack-db",
|
|
20
|
+
"sqlalchemy>=2.0.0",
|
|
21
|
+
]
|
|
22
|
+
events = [
|
|
23
|
+
"hexastack-events",
|
|
24
|
+
]
|
|
25
|
+
all = [
|
|
26
|
+
"hexastack-flow[db,events]",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.entry-points."hexastack.bootstrappers"]
|
|
30
|
+
flow = "hexastack_flow.infra.bootstrap:FlowBootstrapper"
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["uv_build>=0.12.3,<0.13.0"]
|
|
34
|
+
build-backend = "uv_build"
|
|
35
|
+
|
|
36
|
+
[tool.uv.sources]
|
|
37
|
+
hexastack-core = { workspace = true }
|
|
38
|
+
hexastack-cqrs = { workspace = true }
|
|
39
|
+
hexastack-db = { workspace = true }
|
|
40
|
+
hexastack-events = { workspace = true }
|
|
41
|
+
|
|
42
|
+
[tool.importlinter]
|
|
43
|
+
root_packages = ["hexastack_flow"]
|
|
44
|
+
|
|
45
|
+
[[tool.importlinter.contracts]]
|
|
46
|
+
name = "Hexagonal architecture layer hierarchy"
|
|
47
|
+
type = "layers"
|
|
48
|
+
containers = ["hexastack_flow"]
|
|
49
|
+
layers = [
|
|
50
|
+
"infra",
|
|
51
|
+
"adapters",
|
|
52
|
+
"ports",
|
|
53
|
+
"domain",
|
|
54
|
+
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Hexastack Flow - Hexagonal workflow orchestration framework.
|
|
2
|
+
|
|
3
|
+
Notes/Architectural Intent:
|
|
4
|
+
Integrates the zero-daemon hexaflow workflow execution engine into Hexastack,
|
|
5
|
+
providing CQRS command/query steps, saga migration, relational checkpoint persistence,
|
|
6
|
+
and lifecycle domain event broadcasting.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from hexaflow import RetryPolicy, StageExecutionMode, Workflow
|
|
10
|
+
|
|
11
|
+
from hexastack_flow import adapters, domain, infra, ports
|
|
12
|
+
from hexastack_flow.adapters.cqrs.runner import CqrsWorkflowRunner
|
|
13
|
+
from hexastack_flow.adapters.cqrs.steps import (
|
|
14
|
+
CommandStep,
|
|
15
|
+
QueryStep,
|
|
16
|
+
as_command_step,
|
|
17
|
+
as_query_step,
|
|
18
|
+
)
|
|
19
|
+
from hexastack_flow.adapters.events.publisher import WorkflowEventPublisher
|
|
20
|
+
from hexastack_flow.adapters.storage.sqlalchemy import SqlAlchemyWorkflowStore
|
|
21
|
+
from hexastack_flow.domain.events import (
|
|
22
|
+
WorkflowAbortedEvent,
|
|
23
|
+
WorkflowCompletedEvent,
|
|
24
|
+
WorkflowStartedEvent,
|
|
25
|
+
WorkflowStepCompletedEvent,
|
|
26
|
+
WorkflowSuspendedEvent,
|
|
27
|
+
)
|
|
28
|
+
from hexastack_flow.domain.models import (
|
|
29
|
+
CqrsStepMetadata,
|
|
30
|
+
WorkflowExecutionResult,
|
|
31
|
+
)
|
|
32
|
+
from hexastack_flow.infra.bootstrap import FlowBootstrapper
|
|
33
|
+
from hexastack_flow.ports.orchestrator import CqrsWorkflowOrchestratorPort
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"adapters",
|
|
37
|
+
"as_command_step",
|
|
38
|
+
"as_query_step",
|
|
39
|
+
"CommandStep",
|
|
40
|
+
"CqrsStepMetadata",
|
|
41
|
+
"CqrsWorkflowOrchestratorPort",
|
|
42
|
+
"CqrsWorkflowRunner",
|
|
43
|
+
"domain",
|
|
44
|
+
"FlowBootstrapper",
|
|
45
|
+
"infra",
|
|
46
|
+
"ports",
|
|
47
|
+
"QueryStep",
|
|
48
|
+
"RetryPolicy",
|
|
49
|
+
"SqlAlchemyWorkflowStore",
|
|
50
|
+
"StageExecutionMode",
|
|
51
|
+
"Workflow",
|
|
52
|
+
"WorkflowAbortedEvent",
|
|
53
|
+
"WorkflowCompletedEvent",
|
|
54
|
+
"WorkflowEventPublisher",
|
|
55
|
+
"WorkflowExecutionResult",
|
|
56
|
+
"WorkflowStartedEvent",
|
|
57
|
+
"WorkflowStepCompletedEvent",
|
|
58
|
+
"WorkflowSuspendedEvent",
|
|
59
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Adapters for hexastack-flow.
|
|
2
|
+
|
|
3
|
+
Notes/Architectural Intent:
|
|
4
|
+
Exports CQRS, Storage, and Event adapters bridging hexaflow into Hexastack.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from hexastack_flow.adapters import cqrs, events, storage
|
|
8
|
+
from hexastack_flow.adapters.cqrs import (
|
|
9
|
+
CommandStep,
|
|
10
|
+
CqrsWorkflowRunner,
|
|
11
|
+
QueryStep,
|
|
12
|
+
as_command_step,
|
|
13
|
+
as_query_step,
|
|
14
|
+
)
|
|
15
|
+
from hexastack_flow.adapters.events import WorkflowEventPublisher
|
|
16
|
+
from hexastack_flow.adapters.storage import SqlAlchemyWorkflowStore
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"as_command_step",
|
|
20
|
+
"as_query_step",
|
|
21
|
+
"CommandStep",
|
|
22
|
+
"cqrs",
|
|
23
|
+
"CqrsWorkflowRunner",
|
|
24
|
+
"events",
|
|
25
|
+
"QueryStep",
|
|
26
|
+
"SqlAlchemyWorkflowStore",
|
|
27
|
+
"storage",
|
|
28
|
+
"WorkflowEventPublisher",
|
|
29
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""CQRS adapters for hexaflow workflow execution.
|
|
2
|
+
|
|
3
|
+
Notes/Architectural Intent:
|
|
4
|
+
Adapters connecting Hexastack CQRS buses and legacy saga models to hexaflow.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from hexastack_flow.adapters.cqrs.runner import CqrsWorkflowRunner
|
|
8
|
+
from hexastack_flow.adapters.cqrs.steps import (
|
|
9
|
+
CommandStep,
|
|
10
|
+
QueryStep,
|
|
11
|
+
as_command_step,
|
|
12
|
+
as_query_step,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"as_command_step",
|
|
17
|
+
"as_query_step",
|
|
18
|
+
"CommandStep",
|
|
19
|
+
"CqrsWorkflowRunner",
|
|
20
|
+
"QueryStep",
|
|
21
|
+
]
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""CQRS workflow runner adapter implementing CqrsWorkflowOrchestratorPort.
|
|
2
|
+
|
|
3
|
+
Notes/Architectural Intent:
|
|
4
|
+
Glues the hexaflow execution engine, state store, and event bus together.
|
|
5
|
+
Translates hexaflow state objects into Hexastack domain results and
|
|
6
|
+
publishes lifecycle domain events.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from hexaflow.adapters.engines.local_async import AsyncioWorkflowEngine
|
|
13
|
+
from hexaflow.adapters.storage.in_memory import InMemoryStateStore
|
|
14
|
+
from hexaflow.domain.models import WorkflowDefinition
|
|
15
|
+
from hexaflow.domain.state import StepStatus, WorkflowExecutionState, WorkflowStatus
|
|
16
|
+
from hexaflow.ports.engine import WorkflowEnginePort
|
|
17
|
+
from hexaflow.ports.storage import WorkflowStateStorePort
|
|
18
|
+
|
|
19
|
+
from hexastack_cqrs.ports.buses import EventBusPort
|
|
20
|
+
from hexastack_flow.domain.events import (
|
|
21
|
+
WorkflowAbortedEvent,
|
|
22
|
+
WorkflowCompletedEvent,
|
|
23
|
+
WorkflowStartedEvent,
|
|
24
|
+
WorkflowSuspendedEvent,
|
|
25
|
+
)
|
|
26
|
+
from hexastack_flow.domain.models import WorkflowExecutionResult
|
|
27
|
+
from hexastack_flow.ports.orchestrator import CqrsWorkflowOrchestratorPort
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"CqrsWorkflowRunner",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CqrsWorkflowRunner(CqrsWorkflowOrchestratorPort):
|
|
35
|
+
"""Orchestrator runner coordinating workflow execution with CQRS buses.
|
|
36
|
+
|
|
37
|
+
Notes/Architectural Intent:
|
|
38
|
+
Implements CqrsWorkflowOrchestratorPort to provide a complete
|
|
39
|
+
in-process execution runtime with automatic event publishing.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
state_store: WorkflowStateStorePort | None = None,
|
|
45
|
+
engine: WorkflowEnginePort | None = None,
|
|
46
|
+
event_bus: EventBusPort | None = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Initialize runner with state store, execution engine, and event bus.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
state_store: WorkflowStateStorePort for persistence. Defaults to InMemoryStateStore.
|
|
52
|
+
engine: WorkflowEnginePort for DAG execution. Defaults to AsyncioWorkflowEngine.
|
|
53
|
+
event_bus: Optional EventBusPort for publishing workflow domain events.
|
|
54
|
+
"""
|
|
55
|
+
self._store = state_store or InMemoryStateStore()
|
|
56
|
+
self._engine = engine or AsyncioWorkflowEngine(state_store=self._store)
|
|
57
|
+
self._event_bus = event_bus
|
|
58
|
+
|
|
59
|
+
def execute(
|
|
60
|
+
self,
|
|
61
|
+
workflow: WorkflowDefinition,
|
|
62
|
+
initial_inputs: dict[str, Any] | None = None,
|
|
63
|
+
) -> WorkflowExecutionResult:
|
|
64
|
+
"""Execute workflow definition from the first stage.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
workflow: The WorkflowDefinition to run.
|
|
68
|
+
initial_inputs: Optional dictionary of root arguments.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
WorkflowExecutionResult summarizing status, step outputs, and timing.
|
|
72
|
+
"""
|
|
73
|
+
start_time = datetime.now(UTC)
|
|
74
|
+
state = self._engine.run(workflow, initial_inputs=initial_inputs)
|
|
75
|
+
|
|
76
|
+
if self._event_bus is not None:
|
|
77
|
+
self._event_bus.publish(
|
|
78
|
+
WorkflowStartedEvent(
|
|
79
|
+
run_id=state.run_id,
|
|
80
|
+
workflow_name=state.workflow_name,
|
|
81
|
+
started_at=start_time,
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
self._emit_terminal_events(state)
|
|
86
|
+
return self._to_result(state)
|
|
87
|
+
|
|
88
|
+
def resume(
|
|
89
|
+
self,
|
|
90
|
+
run_id: str,
|
|
91
|
+
workflow: WorkflowDefinition,
|
|
92
|
+
patch_inputs: dict[str, Any] | None = None,
|
|
93
|
+
) -> WorkflowExecutionResult:
|
|
94
|
+
"""Resume a suspended workflow run from its latest checkpoints.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
run_id: Unique identifier of the suspended run.
|
|
98
|
+
workflow: WorkflowDefinition specification matching the run.
|
|
99
|
+
patch_inputs: Optional updated inputs for the suspended step.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
WorkflowExecutionResult reflecting post-resumption status.
|
|
103
|
+
"""
|
|
104
|
+
state = self._engine.resume(run_id, workflow, patch_inputs=patch_inputs)
|
|
105
|
+
self._emit_terminal_events(state)
|
|
106
|
+
return self._to_result(state)
|
|
107
|
+
|
|
108
|
+
def abort(
|
|
109
|
+
self,
|
|
110
|
+
run_id: str,
|
|
111
|
+
workflow: WorkflowDefinition,
|
|
112
|
+
reason: str = "Operator aborted",
|
|
113
|
+
) -> WorkflowExecutionResult:
|
|
114
|
+
"""Abort a workflow and unwind completed steps via compensation hooks.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
run_id: Unique identifier of the workflow run.
|
|
118
|
+
workflow: WorkflowDefinition specification containing compensation hooks.
|
|
119
|
+
reason: Operator rationale or explanation for cancellation.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
WorkflowExecutionResult reflecting the aborted state.
|
|
123
|
+
"""
|
|
124
|
+
state = self._engine.abort(run_id, workflow)
|
|
125
|
+
|
|
126
|
+
if self._event_bus is not None:
|
|
127
|
+
self._event_bus.publish(
|
|
128
|
+
WorkflowAbortedEvent(
|
|
129
|
+
run_id=state.run_id,
|
|
130
|
+
workflow_name=state.workflow_name,
|
|
131
|
+
reason=reason,
|
|
132
|
+
aborted_at=datetime.now(UTC),
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return self._to_result(state)
|
|
137
|
+
|
|
138
|
+
def _emit_terminal_events(self, state: WorkflowExecutionState) -> None:
|
|
139
|
+
"""Publish lifecycle events based on execution state outcome.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
state: WorkflowExecutionState to inspect.
|
|
143
|
+
"""
|
|
144
|
+
if self._event_bus is None:
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
if state.status == WorkflowStatus.COMPLETED:
|
|
148
|
+
self._event_bus.publish(
|
|
149
|
+
WorkflowCompletedEvent(
|
|
150
|
+
run_id=state.run_id,
|
|
151
|
+
workflow_name=state.workflow_name,
|
|
152
|
+
completed_at=state.finished_at or datetime.now(UTC),
|
|
153
|
+
total_steps=len(state.step_checkpoints),
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
elif state.status == WorkflowStatus.SUSPENDED:
|
|
157
|
+
failed_step = next(
|
|
158
|
+
(
|
|
159
|
+
name
|
|
160
|
+
for name, chk in state.step_checkpoints.items()
|
|
161
|
+
if chk.status == StepStatus.FAILED
|
|
162
|
+
),
|
|
163
|
+
"unknown",
|
|
164
|
+
)
|
|
165
|
+
self._event_bus.publish(
|
|
166
|
+
WorkflowSuspendedEvent(
|
|
167
|
+
run_id=state.run_id,
|
|
168
|
+
stage_name=state.current_stage or "unknown",
|
|
169
|
+
step_name=failed_step,
|
|
170
|
+
error_type="StepExecutionError",
|
|
171
|
+
error_message=state.error_summary
|
|
172
|
+
or "Step failed and exhausted retries.",
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def _to_result(state: WorkflowExecutionState) -> WorkflowExecutionResult:
|
|
178
|
+
"""Map a hexaflow WorkflowExecutionState into a WorkflowExecutionResult.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
state: Engine execution state.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Pydantic WorkflowExecutionResult value object.
|
|
185
|
+
"""
|
|
186
|
+
completed = tuple(
|
|
187
|
+
name
|
|
188
|
+
for name, chk in state.step_checkpoints.items()
|
|
189
|
+
if chk.status == StepStatus.COMPLETED
|
|
190
|
+
)
|
|
191
|
+
failed = tuple(
|
|
192
|
+
name
|
|
193
|
+
for name, chk in state.step_checkpoints.items()
|
|
194
|
+
if chk.status == StepStatus.FAILED
|
|
195
|
+
)
|
|
196
|
+
outputs = {
|
|
197
|
+
name: chk.output_payload
|
|
198
|
+
for name, chk in state.step_checkpoints.items()
|
|
199
|
+
if chk.output_payload is not None
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return WorkflowExecutionResult(
|
|
203
|
+
run_id=state.run_id,
|
|
204
|
+
workflow_name=state.workflow_name,
|
|
205
|
+
status=state.status.value,
|
|
206
|
+
completed_steps=completed,
|
|
207
|
+
failed_steps=failed,
|
|
208
|
+
outputs=outputs,
|
|
209
|
+
started_at=state.started_at,
|
|
210
|
+
ended_at=state.finished_at or datetime.now(UTC),
|
|
211
|
+
error_summary=state.error_summary,
|
|
212
|
+
)
|