hexastack-events 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.
- hexastack_events-0.0.0/PKG-INFO +117 -0
- hexastack_events-0.0.0/README.md +100 -0
- hexastack_events-0.0.0/pyproject.toml +65 -0
- hexastack_events-0.0.0/pyproject.toml.orig +66 -0
- hexastack_events-0.0.0/src/hexastack_events/__init__.py +8 -0
- hexastack_events-0.0.0/src/hexastack_events/adapters/__init__.py +31 -0
- hexastack_events-0.0.0/src/hexastack_events/adapters/buses/__init__.py +5 -0
- hexastack_events-0.0.0/src/hexastack_events/adapters/buses/in_memory.py +52 -0
- hexastack_events-0.0.0/src/hexastack_events/adapters/cloudevents/__init__.py +15 -0
- hexastack_events-0.0.0/src/hexastack_events/adapters/cloudevents/serializer.py +155 -0
- hexastack_events-0.0.0/src/hexastack_events/adapters/outbox/__init__.py +17 -0
- hexastack_events-0.0.0/src/hexastack_events/adapters/outbox/asyncio.py +117 -0
- hexastack_events-0.0.0/src/hexastack_events/adapters/outbox/huey.py +81 -0
- hexastack_events-0.0.0/src/hexastack_events/adapters/outbox/in_memory.py +52 -0
- hexastack_events-0.0.0/src/hexastack_events/adapters/outbox/sqlalchemy.py +190 -0
- hexastack_events-0.0.0/src/hexastack_events/domain/__init__.py +25 -0
- hexastack_events-0.0.0/src/hexastack_events/domain/context.py +27 -0
- hexastack_events-0.0.0/src/hexastack_events/domain/exceptions.py +35 -0
- hexastack_events-0.0.0/src/hexastack_events/domain/models.py +93 -0
- hexastack_events-0.0.0/src/hexastack_events/infra/__init__.py +13 -0
- hexastack_events-0.0.0/src/hexastack_events/infra/bootstrap.py +102 -0
- hexastack_events-0.0.0/src/hexastack_events/infra/config.py +56 -0
- hexastack_events-0.0.0/src/hexastack_events/infra/middleware.py +84 -0
- hexastack_events-0.0.0/src/hexastack_events/ports/__init__.py +11 -0
- hexastack_events-0.0.0/src/hexastack_events/ports/buses.py +42 -0
- hexastack_events-0.0.0/src/hexastack_events/ports/outbox.py +90 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: hexastack-events
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: CloudEvents serialization, Transactional Outbox pattern, and distributed event streaming for Hexastack
|
|
5
|
+
Author: Richard West
|
|
6
|
+
Author-email: Richard West <dopplereffect.us@gmail.com>
|
|
7
|
+
Requires-Dist: cloudevents>=1.11.0
|
|
8
|
+
Requires-Dist: hexastack-core
|
|
9
|
+
Requires-Dist: hexastack-cqrs
|
|
10
|
+
Requires-Dist: pydantic>=2.10.0
|
|
11
|
+
Requires-Dist: huey>=3.3.4 ; extra == 'huey'
|
|
12
|
+
Requires-Dist: sqlalchemy>=2.0.0 ; extra == 'sql'
|
|
13
|
+
Requires-Python: >=3.13
|
|
14
|
+
Provides-Extra: huey
|
|
15
|
+
Provides-Extra: sql
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# hexastack-events
|
|
19
|
+
|
|
20
|
+
**CNCF CloudEvents 1.0 serialization, Transactional Outbox pattern, and distributed event streaming for Hexastack.**
|
|
21
|
+
|
|
22
|
+
Part of the [Hexastack Framework](https://github.com/TheTrueSCU/hexastack).
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 1. Architectural Overview
|
|
27
|
+
|
|
28
|
+
`hexastack-events` extends Hexastack's in-process CQRS buses with enterprise distributed event streaming and reliability patterns:
|
|
29
|
+
|
|
30
|
+
1. **CNCF CloudEvents 1.0 Protocol**: Automatic serialization and deserialization of domain `Event` models into standardized CloudEvents JSON envelopes with W3C correlation ID and multi-tenant partitioning.
|
|
31
|
+
2. **Transactional Outbox Engine (Gap 6)**: Guarantees at-least-once delivery by staging uncommitted domain events in an `OutboxStoragePort` within the same transaction as business state mutations.
|
|
32
|
+
3. **Dual Relay Engines**:
|
|
33
|
+
- **Native Asyncio (`AsyncioOutboxRelay`)**: In-process background task running with zero external dependencies.
|
|
34
|
+
- **Huey Worker (`HueyOutboxRelay`)**: Multi-process worker executing outbox polling in separate worker nodes (`pip install hexastack-events[huey]`).
|
|
35
|
+
4. **Relational Database Outbox Storage (`SqlAlchemyOutboxStorage`)**: Pluggable storage adapter supporting SQLAlchemy tables and transactions (`pip install hexastack-events[sql]`).
|
|
36
|
+
5. **Distributed Event Bus ([`DistributedEventBusPort`](file:///home/rjdw/Projects/hexastack/packages/hexastack_events/src/hexastack_events/ports/buses.py))**: Standardized cross-service messaging interface for Redis, NATS, Kafka, and in-memory brokers.
|
|
37
|
+
|
|
38
|
+
```mermaid
|
|
39
|
+
graph TD
|
|
40
|
+
CMD["CQRS Command Handler"]
|
|
41
|
+
UOW["Unit of Work Transaction"]
|
|
42
|
+
OUTBOX_MW["OutboxCaptureMiddleware"]
|
|
43
|
+
OUTBOX_STORE["OutboxStoragePort\n(SQLAlchemy / DB Outbox Table)"]
|
|
44
|
+
RELAY["OutboxRelayPort\n(Asyncio / Huey Worker)"]
|
|
45
|
+
BUS["DistributedEventBusPort\n(Redis / NATS / Kafka)"]
|
|
46
|
+
|
|
47
|
+
CMD --> UOW
|
|
48
|
+
CMD --> OUTBOX_MW
|
|
49
|
+
OUTBOX_MW -->|Stages Record| OUTBOX_STORE
|
|
50
|
+
UOW -->|Atomic Commit| OUTBOX_STORE
|
|
51
|
+
RELAY -->|Polls Pending| OUTBOX_STORE
|
|
52
|
+
RELAY -->|Publishes CloudEvent| BUS
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 2. Key Exports & Package Structure
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
hexastack_events/
|
|
61
|
+
├── domain/ # EventContext, OutboxRecord, OutboxStatus, CloudEventEnvelope, EventError
|
|
62
|
+
├── ports/ # OutboxStoragePort, OutboxRelayPort, DistributedEventBusPort
|
|
63
|
+
├── adapters/
|
|
64
|
+
│ ├── cloudevents/ # to_cloudevent, from_cloudevent, cloudevent_to_json/dict
|
|
65
|
+
│ ├── outbox/ # AsyncioOutboxRelay, HueyOutboxRelay, InMemoryOutboxStorage, SqlAlchemyOutboxStorage
|
|
66
|
+
│ └── buses/ # InMemoryDistributedEventBus
|
|
67
|
+
└── infra/ # EventsBootstrapper (order=22), HexastackEventsConfig, OutboxCaptureMiddleware
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 3. Quickstart & Installation
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Core CloudEvents & Asyncio Outbox Relay (zero extra dependencies)
|
|
76
|
+
pip install hexastack-events
|
|
77
|
+
|
|
78
|
+
# With SQLAlchemy relational database outbox storage
|
|
79
|
+
pip install "hexastack-events[sql]"
|
|
80
|
+
|
|
81
|
+
# With Huey multi-process worker support
|
|
82
|
+
pip install "hexastack-events[huey]"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Configuration (`hexastack.toml` or `pyproject.toml`)
|
|
86
|
+
|
|
87
|
+
```toml
|
|
88
|
+
[hexastack.events]
|
|
89
|
+
source = "billing-service"
|
|
90
|
+
relay_mode = "asyncio" # "asyncio", "huey", "manual", "disabled"
|
|
91
|
+
poll_interval_seconds = 1.0
|
|
92
|
+
batch_size = 50
|
|
93
|
+
max_retries = 5
|
|
94
|
+
enabled = true
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Transactional Outbox Example with SQLAlchemy
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from hexastack_core.domain import Event
|
|
101
|
+
from hexastack_events.adapters.outbox.sqlalchemy import OutboxEventMixin
|
|
102
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class Base(DeclarativeBase):
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class OrderOutboxEvent(Base, OutboxEventMixin):
|
|
110
|
+
__tablename__ = "outbox_events"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class OrderPlacedEvent(Event):
|
|
114
|
+
order_id: str
|
|
115
|
+
total_amount: float
|
|
116
|
+
customer_id: str
|
|
117
|
+
```
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# hexastack-events
|
|
2
|
+
|
|
3
|
+
**CNCF CloudEvents 1.0 serialization, Transactional Outbox pattern, and distributed event streaming for Hexastack.**
|
|
4
|
+
|
|
5
|
+
Part of the [Hexastack Framework](https://github.com/TheTrueSCU/hexastack).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Architectural Overview
|
|
10
|
+
|
|
11
|
+
`hexastack-events` extends Hexastack's in-process CQRS buses with enterprise distributed event streaming and reliability patterns:
|
|
12
|
+
|
|
13
|
+
1. **CNCF CloudEvents 1.0 Protocol**: Automatic serialization and deserialization of domain `Event` models into standardized CloudEvents JSON envelopes with W3C correlation ID and multi-tenant partitioning.
|
|
14
|
+
2. **Transactional Outbox Engine (Gap 6)**: Guarantees at-least-once delivery by staging uncommitted domain events in an `OutboxStoragePort` within the same transaction as business state mutations.
|
|
15
|
+
3. **Dual Relay Engines**:
|
|
16
|
+
- **Native Asyncio (`AsyncioOutboxRelay`)**: In-process background task running with zero external dependencies.
|
|
17
|
+
- **Huey Worker (`HueyOutboxRelay`)**: Multi-process worker executing outbox polling in separate worker nodes (`pip install hexastack-events[huey]`).
|
|
18
|
+
4. **Relational Database Outbox Storage (`SqlAlchemyOutboxStorage`)**: Pluggable storage adapter supporting SQLAlchemy tables and transactions (`pip install hexastack-events[sql]`).
|
|
19
|
+
5. **Distributed Event Bus ([`DistributedEventBusPort`](file:///home/rjdw/Projects/hexastack/packages/hexastack_events/src/hexastack_events/ports/buses.py))**: Standardized cross-service messaging interface for Redis, NATS, Kafka, and in-memory brokers.
|
|
20
|
+
|
|
21
|
+
```mermaid
|
|
22
|
+
graph TD
|
|
23
|
+
CMD["CQRS Command Handler"]
|
|
24
|
+
UOW["Unit of Work Transaction"]
|
|
25
|
+
OUTBOX_MW["OutboxCaptureMiddleware"]
|
|
26
|
+
OUTBOX_STORE["OutboxStoragePort\n(SQLAlchemy / DB Outbox Table)"]
|
|
27
|
+
RELAY["OutboxRelayPort\n(Asyncio / Huey Worker)"]
|
|
28
|
+
BUS["DistributedEventBusPort\n(Redis / NATS / Kafka)"]
|
|
29
|
+
|
|
30
|
+
CMD --> UOW
|
|
31
|
+
CMD --> OUTBOX_MW
|
|
32
|
+
OUTBOX_MW -->|Stages Record| OUTBOX_STORE
|
|
33
|
+
UOW -->|Atomic Commit| OUTBOX_STORE
|
|
34
|
+
RELAY -->|Polls Pending| OUTBOX_STORE
|
|
35
|
+
RELAY -->|Publishes CloudEvent| BUS
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 2. Key Exports & Package Structure
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
hexastack_events/
|
|
44
|
+
├── domain/ # EventContext, OutboxRecord, OutboxStatus, CloudEventEnvelope, EventError
|
|
45
|
+
├── ports/ # OutboxStoragePort, OutboxRelayPort, DistributedEventBusPort
|
|
46
|
+
├── adapters/
|
|
47
|
+
│ ├── cloudevents/ # to_cloudevent, from_cloudevent, cloudevent_to_json/dict
|
|
48
|
+
│ ├── outbox/ # AsyncioOutboxRelay, HueyOutboxRelay, InMemoryOutboxStorage, SqlAlchemyOutboxStorage
|
|
49
|
+
│ └── buses/ # InMemoryDistributedEventBus
|
|
50
|
+
└── infra/ # EventsBootstrapper (order=22), HexastackEventsConfig, OutboxCaptureMiddleware
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 3. Quickstart & Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Core CloudEvents & Asyncio Outbox Relay (zero extra dependencies)
|
|
59
|
+
pip install hexastack-events
|
|
60
|
+
|
|
61
|
+
# With SQLAlchemy relational database outbox storage
|
|
62
|
+
pip install "hexastack-events[sql]"
|
|
63
|
+
|
|
64
|
+
# With Huey multi-process worker support
|
|
65
|
+
pip install "hexastack-events[huey]"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Configuration (`hexastack.toml` or `pyproject.toml`)
|
|
69
|
+
|
|
70
|
+
```toml
|
|
71
|
+
[hexastack.events]
|
|
72
|
+
source = "billing-service"
|
|
73
|
+
relay_mode = "asyncio" # "asyncio", "huey", "manual", "disabled"
|
|
74
|
+
poll_interval_seconds = 1.0
|
|
75
|
+
batch_size = 50
|
|
76
|
+
max_retries = 5
|
|
77
|
+
enabled = true
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Transactional Outbox Example with SQLAlchemy
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from hexastack_core.domain import Event
|
|
84
|
+
from hexastack_events.adapters.outbox.sqlalchemy import OutboxEventMixin
|
|
85
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Base(DeclarativeBase):
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class OrderOutboxEvent(Base, OutboxEventMixin):
|
|
93
|
+
__tablename__ = "outbox_events"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class OrderPlacedEvent(Event):
|
|
97
|
+
order_id: str
|
|
98
|
+
total_amount: float
|
|
99
|
+
customer_id: str
|
|
100
|
+
```
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hexastack-events"
|
|
3
|
+
version = "0.0.0"
|
|
4
|
+
description = "CloudEvents serialization, Transactional Outbox pattern, and distributed event streaming for Hexastack"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"cloudevents>=1.11.0",
|
|
9
|
+
"hexastack-core",
|
|
10
|
+
"hexastack-cqrs",
|
|
11
|
+
"pydantic>=2.10.0",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[[project.authors]]
|
|
15
|
+
name = "Richard West"
|
|
16
|
+
email = "dopplereffect.us@gmail.com"
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
huey = ["huey>=3.3.4"]
|
|
20
|
+
sql = ["sqlalchemy>=2.0.0"]
|
|
21
|
+
|
|
22
|
+
[project.entry-points."hexastack.bootstrappers"]
|
|
23
|
+
events = "hexastack_events.infra.bootstrap:EventsBootstrapper"
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["uv_build>=0.12.3,<0.13.0"]
|
|
27
|
+
build-backend = "uv_build"
|
|
28
|
+
|
|
29
|
+
[tool.uv.sources.hexastack-core]
|
|
30
|
+
workspace = true
|
|
31
|
+
|
|
32
|
+
[tool.uv.sources.hexastack-cqrs]
|
|
33
|
+
workspace = true
|
|
34
|
+
|
|
35
|
+
[tool.importlinter]
|
|
36
|
+
root_packages = ["hexastack_events"]
|
|
37
|
+
|
|
38
|
+
[[tool.importlinter.contracts]]
|
|
39
|
+
name = "Hexagonal architecture layer hierarchy"
|
|
40
|
+
type = "layers"
|
|
41
|
+
containers = ["hexastack_events"]
|
|
42
|
+
layers = [
|
|
43
|
+
"adapters",
|
|
44
|
+
"ports",
|
|
45
|
+
"domain",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[[tool.importlinter.contracts]]
|
|
49
|
+
name = "Forbidden imports for domain"
|
|
50
|
+
type = "forbidden"
|
|
51
|
+
source_modules = ["hexastack_events.domain"]
|
|
52
|
+
forbidden_modules = [
|
|
53
|
+
"hexastack_events.ports",
|
|
54
|
+
"hexastack_events.adapters",
|
|
55
|
+
"hexastack_events.infra",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[[tool.importlinter.contracts]]
|
|
59
|
+
name = "Forbidden imports for ports"
|
|
60
|
+
type = "forbidden"
|
|
61
|
+
source_modules = ["hexastack_events.ports"]
|
|
62
|
+
forbidden_modules = [
|
|
63
|
+
"hexastack_events.adapters",
|
|
64
|
+
"hexastack_events.infra",
|
|
65
|
+
]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hexastack-events"
|
|
3
|
+
version = "0.0.0"
|
|
4
|
+
description = "CloudEvents serialization, Transactional Outbox pattern, and distributed event streaming 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
|
+
"cloudevents>=1.11.0",
|
|
12
|
+
"hexastack-core",
|
|
13
|
+
"hexastack-cqrs",
|
|
14
|
+
"pydantic>=2.10.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
huey = [
|
|
19
|
+
"huey>=3.3.4",
|
|
20
|
+
]
|
|
21
|
+
sql = [
|
|
22
|
+
"sqlalchemy>=2.0.0",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.entry-points."hexastack.bootstrappers"]
|
|
26
|
+
events = "hexastack_events.infra.bootstrap:EventsBootstrapper"
|
|
27
|
+
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = ["uv_build>=0.12.3,<0.13.0"]
|
|
30
|
+
build-backend = "uv_build"
|
|
31
|
+
|
|
32
|
+
[tool.uv.sources]
|
|
33
|
+
hexastack-core = { workspace = true }
|
|
34
|
+
hexastack-cqrs = { workspace = true }
|
|
35
|
+
|
|
36
|
+
[tool.importlinter]
|
|
37
|
+
root_packages = ["hexastack_events"]
|
|
38
|
+
|
|
39
|
+
[[tool.importlinter.contracts]]
|
|
40
|
+
name = "Hexagonal architecture layer hierarchy"
|
|
41
|
+
type = "layers"
|
|
42
|
+
containers = ["hexastack_events"]
|
|
43
|
+
layers = [
|
|
44
|
+
"adapters",
|
|
45
|
+
"ports",
|
|
46
|
+
"domain",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[[tool.importlinter.contracts]]
|
|
50
|
+
name = "Forbidden imports for domain"
|
|
51
|
+
type = "forbidden"
|
|
52
|
+
source_modules = ["hexastack_events.domain"]
|
|
53
|
+
forbidden_modules = [
|
|
54
|
+
"hexastack_events.ports",
|
|
55
|
+
"hexastack_events.adapters",
|
|
56
|
+
"hexastack_events.infra",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[[tool.importlinter.contracts]]
|
|
60
|
+
name = "Forbidden imports for ports"
|
|
61
|
+
type = "forbidden"
|
|
62
|
+
source_modules = ["hexastack_events.ports"]
|
|
63
|
+
forbidden_modules = [
|
|
64
|
+
"hexastack_events.adapters",
|
|
65
|
+
"hexastack_events.infra",
|
|
66
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from hexastack_events.adapters.buses import InMemoryDistributedEventBus
|
|
2
|
+
from hexastack_events.adapters.cloudevents import (
|
|
3
|
+
cloudevent_to_dict,
|
|
4
|
+
cloudevent_to_json,
|
|
5
|
+
from_cloudevent,
|
|
6
|
+
to_cloudevent,
|
|
7
|
+
to_envelope,
|
|
8
|
+
)
|
|
9
|
+
from hexastack_events.adapters.outbox import (
|
|
10
|
+
AsyncioOutboxRelay,
|
|
11
|
+
HueyOutboxRelay,
|
|
12
|
+
InMemoryOutboxStorage,
|
|
13
|
+
OutboxEventBaseModel,
|
|
14
|
+
OutboxEventMixin,
|
|
15
|
+
SqlAlchemyOutboxStorage,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"AsyncioOutboxRelay",
|
|
20
|
+
"cloudevent_to_dict",
|
|
21
|
+
"cloudevent_to_json",
|
|
22
|
+
"from_cloudevent",
|
|
23
|
+
"HueyOutboxRelay",
|
|
24
|
+
"InMemoryDistributedEventBus",
|
|
25
|
+
"InMemoryOutboxStorage",
|
|
26
|
+
"OutboxEventBaseModel",
|
|
27
|
+
"OutboxEventMixin",
|
|
28
|
+
"SqlAlchemyOutboxStorage",
|
|
29
|
+
"to_cloudevent",
|
|
30
|
+
"to_envelope",
|
|
31
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from hexastack_core.domain import Event
|
|
6
|
+
from hexastack_events.domain.models import CloudEventEnvelope
|
|
7
|
+
from hexastack_events.ports.buses import DistributedEventBusPort
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InMemoryDistributedEventBus(DistributedEventBusPort):
|
|
11
|
+
"""In-memory implementation of DistributedEventBusPort for testing and local dev.
|
|
12
|
+
|
|
13
|
+
Notes/Architectural Intent:
|
|
14
|
+
Records published CloudEvents envelopes, maintains topic subscriptions,
|
|
15
|
+
and allows verifying published events without external message brokers.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
self.published_events: list[Event] = []
|
|
20
|
+
self.published_envelopes: list[CloudEventEnvelope] = []
|
|
21
|
+
self._subscribers: dict[str, list[Callable[[Any], Any]]] = defaultdict(list)
|
|
22
|
+
|
|
23
|
+
def clear(self) -> None:
|
|
24
|
+
"""Clear recorded events and envelopes."""
|
|
25
|
+
self.published_events.clear()
|
|
26
|
+
self.published_envelopes.clear()
|
|
27
|
+
|
|
28
|
+
def publish(self, event: Event) -> None:
|
|
29
|
+
"""Publish a domain event and invoke local subscribers."""
|
|
30
|
+
self.published_events.append(event)
|
|
31
|
+
event_name = event.__class__.__name__
|
|
32
|
+
for handler in self._subscribers.get(event_name, []):
|
|
33
|
+
handler(event)
|
|
34
|
+
|
|
35
|
+
def publish_envelope(self, envelope: CloudEventEnvelope) -> None:
|
|
36
|
+
"""Publish a CloudEvents envelope and invoke matching subscribers."""
|
|
37
|
+
self.published_envelopes.append(envelope)
|
|
38
|
+
for handler in self._subscribers.get(envelope.type, []):
|
|
39
|
+
handler(envelope)
|
|
40
|
+
|
|
41
|
+
def subscribe(
|
|
42
|
+
self,
|
|
43
|
+
event_type: str,
|
|
44
|
+
handler: Callable[[Any], Any],
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Register a subscriber callback for a specific event type."""
|
|
47
|
+
self._subscribers[event_type].append(handler)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"InMemoryDistributedEventBus",
|
|
52
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from hexastack_events.adapters.cloudevents.serializer import (
|
|
2
|
+
cloudevent_to_dict,
|
|
3
|
+
cloudevent_to_json,
|
|
4
|
+
from_cloudevent,
|
|
5
|
+
to_cloudevent,
|
|
6
|
+
to_envelope,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"cloudevent_to_dict",
|
|
11
|
+
"cloudevent_to_json",
|
|
12
|
+
"from_cloudevent",
|
|
13
|
+
"to_cloudevent",
|
|
14
|
+
"to_envelope",
|
|
15
|
+
]
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import uuid
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from cloudevents.v1.http import CloudEvent, from_dict, from_json
|
|
7
|
+
|
|
8
|
+
from hexastack_core.domain import Event
|
|
9
|
+
from hexastack_core.utils.context import get_correlation_id, get_user_context
|
|
10
|
+
from hexastack_events.domain.exceptions import EventSerializationError
|
|
11
|
+
from hexastack_events.domain.models import CloudEventEnvelope
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"cloudevent_to_dict",
|
|
15
|
+
"cloudevent_to_json",
|
|
16
|
+
"from_cloudevent",
|
|
17
|
+
"to_cloudevent",
|
|
18
|
+
"to_envelope",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def cloudevent_to_dict(ce: CloudEvent) -> dict[str, Any]:
|
|
23
|
+
"""Serialize a CloudEvent instance to a standard dictionary format."""
|
|
24
|
+
result = dict(ce.get_attributes())
|
|
25
|
+
result["data"] = ce.data
|
|
26
|
+
return result
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def cloudevent_to_json(ce: CloudEvent) -> str:
|
|
30
|
+
"""Serialize a CloudEvent instance to a valid JSON string."""
|
|
31
|
+
return json.dumps(cloudevent_to_dict(ce))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def from_cloudevent[T: Event](
|
|
35
|
+
cloudevent_data: CloudEvent | dict[str, Any] | str,
|
|
36
|
+
event_cls: type[T],
|
|
37
|
+
) -> T:
|
|
38
|
+
"""Deserialize a CloudEvent envelope or JSON payload into a typed domain Event.
|
|
39
|
+
|
|
40
|
+
Notes/Architectural Intent:
|
|
41
|
+
Reconstructs the domain Event model from CloudEvent data payload while
|
|
42
|
+
preserving structural validation via Pydantic model_validate.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
cloudevent_data: CloudEvent instance, dictionary payload, or JSON string.
|
|
46
|
+
event_cls: Target Event class to instantiate.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Instantiated and validated domain Event model.
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
EventSerializationError: If deserialization fails.
|
|
53
|
+
"""
|
|
54
|
+
try:
|
|
55
|
+
if isinstance(cloudevent_data, str):
|
|
56
|
+
ce = from_json(cloudevent_data)
|
|
57
|
+
elif isinstance(cloudevent_data, dict):
|
|
58
|
+
ce = from_dict(cloudevent_data)
|
|
59
|
+
else:
|
|
60
|
+
ce = cloudevent_data
|
|
61
|
+
|
|
62
|
+
payload = ce.data
|
|
63
|
+
if isinstance(payload, str):
|
|
64
|
+
payload = json.loads(payload)
|
|
65
|
+
|
|
66
|
+
return event_cls.model_validate(payload)
|
|
67
|
+
except Exception as exc:
|
|
68
|
+
raise EventSerializationError(
|
|
69
|
+
f"Failed to deserialize CloudEvent into '{event_cls.__name__}': {exc}"
|
|
70
|
+
) from exc
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def to_cloudevent(
|
|
74
|
+
event: Event,
|
|
75
|
+
*,
|
|
76
|
+
source: str = "hexastack",
|
|
77
|
+
event_type: str | None = None,
|
|
78
|
+
event_id: str | None = None,
|
|
79
|
+
time: datetime | None = None,
|
|
80
|
+
extensions: dict[str, Any] | None = None,
|
|
81
|
+
) -> CloudEvent:
|
|
82
|
+
"""Wrap a domain Event into a standard CNCF CloudEvent envelope.
|
|
83
|
+
|
|
84
|
+
Notes/Architectural Intent:
|
|
85
|
+
Standardizes domain event serialization for cross-service, cloud-native
|
|
86
|
+
messaging. Automatically extracts active correlation_id and tenant_id
|
|
87
|
+
from context into CloudEvents extension attributes.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
event: Domain Event Pydantic model instance.
|
|
91
|
+
source: URI identifier of the event producer (defaults to 'hexastack').
|
|
92
|
+
event_type: Event type string (defaults to event class name).
|
|
93
|
+
event_id: Unique event ID (defaults to new UUID4).
|
|
94
|
+
time: Event timestamp in UTC (defaults to current UTC datetime).
|
|
95
|
+
extensions: Optional custom extension attributes.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Populated CNCF CloudEvent instance.
|
|
99
|
+
"""
|
|
100
|
+
now = time or datetime.now(UTC)
|
|
101
|
+
cid = get_correlation_id()
|
|
102
|
+
user_ctx = get_user_context()
|
|
103
|
+
|
|
104
|
+
attributes: dict[str, Any] = {
|
|
105
|
+
"id": event_id or str(uuid.uuid4()),
|
|
106
|
+
"source": source,
|
|
107
|
+
"type": event_type or event.__class__.__name__,
|
|
108
|
+
"specversion": "1.0",
|
|
109
|
+
"time": now.isoformat(),
|
|
110
|
+
"datacontenttype": "application/json",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if cid:
|
|
114
|
+
attributes["correlationid"] = cid
|
|
115
|
+
|
|
116
|
+
if user_ctx and user_ctx.tenant_id:
|
|
117
|
+
attributes["tenantid"] = user_ctx.tenant_id
|
|
118
|
+
|
|
119
|
+
if extensions:
|
|
120
|
+
attributes.update(extensions)
|
|
121
|
+
|
|
122
|
+
data = event.model_dump(mode="json")
|
|
123
|
+
return CloudEvent(attributes, data)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def to_envelope(
|
|
127
|
+
event: Event,
|
|
128
|
+
*,
|
|
129
|
+
source: str = "hexastack",
|
|
130
|
+
event_type: str | None = None,
|
|
131
|
+
event_id: str | None = None,
|
|
132
|
+
) -> CloudEventEnvelope:
|
|
133
|
+
"""Convert domain Event to a typed CloudEventEnvelope Pydantic model.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
event: Domain Event instance.
|
|
137
|
+
source: Event source URI.
|
|
138
|
+
event_type: Event type identifier.
|
|
139
|
+
event_id: Optional unique event ID.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Populated CloudEventEnvelope instance.
|
|
143
|
+
"""
|
|
144
|
+
ce = to_cloudevent(event, source=source, event_type=event_type, event_id=event_id)
|
|
145
|
+
attrs = ce.get_attributes()
|
|
146
|
+
return CloudEventEnvelope(
|
|
147
|
+
id=str(attrs.get("id")),
|
|
148
|
+
source=str(attrs.get("source")),
|
|
149
|
+
type=str(attrs.get("type")),
|
|
150
|
+
time=str(attrs.get("time")),
|
|
151
|
+
datacontenttype=str(attrs.get("datacontenttype", "application/json")),
|
|
152
|
+
correlationid=attrs.get("correlationid"),
|
|
153
|
+
tenantid=attrs.get("tenantid"),
|
|
154
|
+
data=ce.data if isinstance(ce.data, dict) else {},
|
|
155
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from hexastack_events.adapters.outbox.asyncio import AsyncioOutboxRelay
|
|
2
|
+
from hexastack_events.adapters.outbox.huey import HueyOutboxRelay
|
|
3
|
+
from hexastack_events.adapters.outbox.in_memory import InMemoryOutboxStorage
|
|
4
|
+
from hexastack_events.adapters.outbox.sqlalchemy import (
|
|
5
|
+
OutboxEventBaseModel,
|
|
6
|
+
OutboxEventMixin,
|
|
7
|
+
SqlAlchemyOutboxStorage,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"AsyncioOutboxRelay",
|
|
12
|
+
"HueyOutboxRelay",
|
|
13
|
+
"InMemoryOutboxStorage",
|
|
14
|
+
"OutboxEventBaseModel",
|
|
15
|
+
"OutboxEventMixin",
|
|
16
|
+
"SqlAlchemyOutboxStorage",
|
|
17
|
+
]
|