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.
Files changed (26) hide show
  1. hexastack_events-0.0.0/PKG-INFO +117 -0
  2. hexastack_events-0.0.0/README.md +100 -0
  3. hexastack_events-0.0.0/pyproject.toml +65 -0
  4. hexastack_events-0.0.0/pyproject.toml.orig +66 -0
  5. hexastack_events-0.0.0/src/hexastack_events/__init__.py +8 -0
  6. hexastack_events-0.0.0/src/hexastack_events/adapters/__init__.py +31 -0
  7. hexastack_events-0.0.0/src/hexastack_events/adapters/buses/__init__.py +5 -0
  8. hexastack_events-0.0.0/src/hexastack_events/adapters/buses/in_memory.py +52 -0
  9. hexastack_events-0.0.0/src/hexastack_events/adapters/cloudevents/__init__.py +15 -0
  10. hexastack_events-0.0.0/src/hexastack_events/adapters/cloudevents/serializer.py +155 -0
  11. hexastack_events-0.0.0/src/hexastack_events/adapters/outbox/__init__.py +17 -0
  12. hexastack_events-0.0.0/src/hexastack_events/adapters/outbox/asyncio.py +117 -0
  13. hexastack_events-0.0.0/src/hexastack_events/adapters/outbox/huey.py +81 -0
  14. hexastack_events-0.0.0/src/hexastack_events/adapters/outbox/in_memory.py +52 -0
  15. hexastack_events-0.0.0/src/hexastack_events/adapters/outbox/sqlalchemy.py +190 -0
  16. hexastack_events-0.0.0/src/hexastack_events/domain/__init__.py +25 -0
  17. hexastack_events-0.0.0/src/hexastack_events/domain/context.py +27 -0
  18. hexastack_events-0.0.0/src/hexastack_events/domain/exceptions.py +35 -0
  19. hexastack_events-0.0.0/src/hexastack_events/domain/models.py +93 -0
  20. hexastack_events-0.0.0/src/hexastack_events/infra/__init__.py +13 -0
  21. hexastack_events-0.0.0/src/hexastack_events/infra/bootstrap.py +102 -0
  22. hexastack_events-0.0.0/src/hexastack_events/infra/config.py +56 -0
  23. hexastack_events-0.0.0/src/hexastack_events/infra/middleware.py +84 -0
  24. hexastack_events-0.0.0/src/hexastack_events/ports/__init__.py +11 -0
  25. hexastack_events-0.0.0/src/hexastack_events/ports/buses.py +42 -0
  26. 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,8 @@
1
+ from hexastack_events import adapters, domain, infra, ports
2
+
3
+ __all__ = [
4
+ "adapters",
5
+ "domain",
6
+ "infra",
7
+ "ports",
8
+ ]
@@ -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,5 @@
1
+ from hexastack_events.adapters.buses.in_memory import InMemoryDistributedEventBus
2
+
3
+ __all__ = [
4
+ "InMemoryDistributedEventBus",
5
+ ]
@@ -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
+ ]