Coverage for src / lexigram / contracts / events / protocols.py: 0%
75 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-15 18:57 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-15 18:57 +0800
1"""Event sourcing protocol class definitions (CQRS/ES).
3Protocols for event buses, command buses, event stores,
4and related patterns.
5"""
7from __future__ import annotations
9from typing import TYPE_CHECKING, Any, Protocol, TypeVar, runtime_checkable
11if TYPE_CHECKING:
12 from datetime import datetime
13 from uuid import UUID
15 from lexigram.contracts.core.result import Result
16 from lexigram.contracts.exceptions.events import EventError
18TAggregate_co = TypeVar("TAggregate_co", covariant=True)
20# --- Domain Events ---
23@runtime_checkable
24class DomainEventPublisherProtocol(Protocol):
25 """Protocol for publishing domain events."""
27 async def publish(self, event: Any) -> None:
28 """Publish a domain event.
30 Args:
31 event: DomainEvent instance.
32 """
33 ...
36@runtime_checkable
37class EventHandlerProtocol(Protocol):
38 """Protocol for event handlers."""
40 async def handle(self, event: Any) -> Result[None, EventError]:
41 """Handle an event.
43 Args:
44 event: DomainEvent to handle.
46 Returns:
47 ``Ok(None)`` on success, ``Err(EventError)`` if handling fails
48 in an expected, recoverable way.
49 """
50 ...
53@runtime_checkable
54class MultiEventHandlerProtocol(Protocol):
55 """Protocol for handlers that handle multiple event types."""
57 def handles(self) -> list[type]:
58 """Get list of event types handled.
60 Returns:
61 List of event classes.
62 """
63 ...
65 async def handle(self, event: Any) -> Result[None, EventError]:
66 """Handle an event.
68 Args:
69 event: DomainEvent to handle.
71 Returns:
72 ``Ok(None)`` on success, ``Err(EventError)`` if handling fails
73 in an expected, recoverable way.
74 """
75 ...
78@runtime_checkable
79class EventBusProtocol(Protocol):
80 """Protocol for event bus implementations.
82 The event bus manages event publication and subscription.
84 Example:
85 ```python
86 class InMemoryEventBus:
87 async def publish(self, event: DomainEvent) -> "Result[None, EventError]":
88 for handler in self._handlers.get(type(event), []):
89 await handler.handle(event)
90 return Ok(None)
92 def subscribe(self, event_type, handler):
93 self._handlers.setdefault(event_type, []).append(handler)
94 ```
95 """
97 async def publish(self, event: Any) -> Result[None, EventError]:
98 r"""Publish an event to all subscribers.
100 Args:
101 event: DomainEvent to publish.
103 Returns:
104 Ok(None) when the event is successfully enqueued for dispatch.
105 Err(EventError) when the event cannot be accepted
106 (e.g.\ no handlers registered and the bus requires at least one).
107 """
108 ...
110 def subscribe(self, event_type: type, handler: EventHandlerProtocol) -> None:
111 """Subscribe a handler to an event type.
113 Args:
114 event_type: Type of event to subscribe to.
115 handler: Handler to call when event is published.
116 """
117 ...
119 def unsubscribe(self, event_type: type, handler: EventHandlerProtocol) -> None:
120 """Remove a handler subscription for an event type.
122 Args:
123 event_type: Type of event to unsubscribe from.
124 handler: Handler to remove.
125 """
126 ...
129@runtime_checkable
130class EventMiddlewareProtocol(Protocol):
131 """Protocol for event bus middleware.
133 Middleware intercepts event publication, allowing cross-cutting
134 concerns like logging, metrics, or error handling to be applied
135 transparently.
137 The middleware receives the event and a ``next_handler`` coroutine
138 that invokes the next middleware (or the actual handlers). The
139 middleware must call ``next_handler`` to continue the chain.
141 Example::
143 class LoggingMiddleware:
144 async def __call__(self, event: Any, next_handler: Any) -> None:
145 logger.info("publishing", event_type=type(event).__name__)
146 await next_handler(event)
147 logger.info("published", event_type=type(event).__name__)
148 """
150 async def __call__(self, event: Any, next_handler: Any) -> None:
151 """Process an event through this middleware.
153 Args:
154 event: The domain event being published.
155 next_handler: Coroutine to call to continue the middleware
156 chain. Must be awaited for the event to reach handlers.
157 """
158 ...
161# --- Commands (CQRS) ---
164@runtime_checkable
165class CommandHandlerProtocol(Protocol):
166 """Protocol for command handlers."""
168 async def handle(self, command: Any) -> Any:
169 """Handle a command.
171 Args:
172 command: Command to handle.
174 Returns:
175 Command result.
176 """
177 ...
180@runtime_checkable
181class CommandBusProtocol(Protocol):
182 """Protocol for command bus implementations.
184 Example:
185 ```python
186 class CommandBusProtocol:
187 async def dispatch(self, command: Command) -> Any:
188 handler = self._handlers[type(command)]
189 return await handler.handle(command)
190 ```
191 """
193 async def dispatch(self, command: Any) -> Any:
194 """Dispatch a command to its handler.
196 Args:
197 command: Command to dispatch.
199 Returns:
200 Result from the command handler.
201 """
202 ...
205# --- Queries (CQRS) ---
208@runtime_checkable
209class QueryHandlerProtocol(Protocol):
210 """Protocol for query handlers."""
212 async def handle(self, query: Any) -> Any:
213 """Handle a query.
215 Args:
216 query: Query to handle.
218 Returns:
219 Query result.
220 """
221 ...
224@runtime_checkable
225class QueryBusProtocol(Protocol):
226 """Protocol for query bus implementations.
228 Example:
229 ```python
230 class QueryBusProtocol:
231 async def execute(self, query: Query) -> Any:
232 handler = self._handlers[type(query)]
233 return await handler.handle(query)
234 ```
235 """
237 async def execute(self, query: Any) -> Any:
238 """Execute a query through its handler.
240 Args:
241 query: Query to execute.
243 Returns:
244 Query result.
245 """
246 ...
249# --- Event Store ---
252@runtime_checkable
253class EventStoreProtocol(Protocol):
254 """Protocol for event store implementations.
256 The event store persists and retrieves domain events.
258 Example:
259 ```python
260 class PostgresEventStore:
261 async def append(self, stream_id, events, expected_version=None):
262 async with self.db.transaction():
263 for event in events:
264 await self.db.insert("events", event.to_dict())
265 ```
266 """
268 async def append(
269 self,
270 stream_id: str,
271 events: list[Any],
272 expected_version: int | None = None,
273 ) -> int:
274 """Append events to a stream.
276 Args:
277 stream_id: Unique stream identifier.
278 events: List of events to append.
279 expected_version: Expected stream version for optimistic concurrency.
281 Returns:
282 New stream version.
284 Raises:
285 ConcurrencyError: If expected version doesn't match.
286 """
287 ...
289 async def read(
290 self,
291 stream_id: str,
292 start: int = 0,
293 count: int | None = None,
294 ) -> list[Any]:
295 """Read events from a stream.
297 Args:
298 stream_id: Unique stream identifier.
299 start: Starting position.
300 count: Maximum events to read.
302 Returns:
303 List of events.
304 """
305 ...
307 async def read_all(
308 self,
309 position: int = 0,
310 count: int | None = None,
311 ) -> list[Any]:
312 """Read events from all streams.
314 Args:
315 position: Starting global position.
316 count: Maximum events to read.
318 Returns:
319 List of events.
320 """
321 ...
324@runtime_checkable
325class SnapshotStoreProtocol(Protocol):
326 """Protocol for aggregate snapshot storage."""
328 async def save(self, aggregate_id: str, snapshot: Any, version: int) -> None:
329 """Save an aggregate snapshot.
331 Args:
332 aggregate_id: Aggregate identifier.
333 snapshot: Aggregate state snapshot.
334 version: Aggregate version at snapshot.
335 """
336 ...
338 async def load(self, aggregate_id: str) -> tuple[Any, int] | None:
339 """Load the latest snapshot.
341 Args:
342 aggregate_id: Aggregate identifier.
344 Returns:
345 Tuple of (snapshot, version) or None if not found.
346 """
347 ...
350# --- Event Sourced RepositoryProtocol ---
353@runtime_checkable
354class EventSourcedReadRepositoryProtocol(Protocol):
355 """Protocol for read-only repository access."""
357 async def get(self, aggregate_id: UUID | str) -> Any | None:
358 """Load an aggregate by ID."""
359 ...
361 async def exists(self, aggregate_id: UUID | str) -> bool:
362 """Check if an aggregate exists."""
363 ...
365 async def get_all(
366 self,
367 limit: int | None = None,
368 offset: int | None = None,
369 ) -> list[Any]:
370 """Get all aggregates (paginated)."""
371 ...
374@runtime_checkable
375class EventSourcedRepositoryProtocol(EventSourcedReadRepositoryProtocol, Protocol):
376 """Protocol for event-sourced repositories (Read/Write)."""
378 async def save(self, aggregate: Any) -> None:
379 """Save an aggregate."""
380 ...
383@runtime_checkable
384class AggregateFactoryProtocol(Protocol[TAggregate_co]):
385 """Factory for creating aggregates."""
387 def create(self, aggregate_id: UUID | str) -> TAggregate_co:
388 """Create a new aggregate instance.
390 Args:
391 aggregate_id: Aggregate ID.
393 Returns:
394 New aggregate instance.
395 """
396 ...
399# --- Projections ---
402@runtime_checkable
403class ProjectionProtocol(Protocol):
404 """ProjectionProtocol protocol for building read models from events."""
406 def apply(self, event: Any) -> None:
407 """Apply an event to the projection state.
409 Args:
410 event: Domain event to apply.
411 """
412 ...
415@runtime_checkable
416class PubSubProtocol(Protocol):
417 """Protocol for publish/subscribe backends.
419 Implementations must support publishing messages to topics and
420 subscribing handlers to receive messages from topics.
421 """
423 async def publish(self, topic: str, data: Any) -> None:
424 """Publish *data* to *topic*."""
425 ...
427 async def subscribe(
428 self,
429 topic: str,
430 handler: Any,
431 ) -> None:
432 """Subscribe *handler* to receive messages from *topic*."""
433 ...
435 async def unsubscribe(self, topic: str, handler: Any) -> None:
436 """Remove *handler* subscription from *topic*."""
437 ...
440@runtime_checkable
441class IntegrationEventProtocol(Protocol):
442 """Protocol formalising the bridge between domain events and transactional messaging.
444 Any event intended for cross-service communication should satisfy this
445 protocol. Implementations carry all metadata required for reliable,
446 idempotent delivery across bounded-context boundaries.
448 Attributes:
449 event_id: Unique, stable identifier for this event instance (e.g. a UUID4
450 string). Used by consumers for deduplication.
451 event_type: The event class name or type discriminator string. Consumers
452 use this to route / deserialise the ``payload``.
453 source_service: The bounded context or service that emitted this event
454 (e.g. ``"order-service"``).
455 correlation_id: Optional distributed-trace correlation identifier that
456 links this event to a broader request chain. ``None`` when tracing
457 is not active.
458 causation_id: Optional identifier of the event or command that directly
459 caused this event to be emitted. ``None`` for root events.
460 payload: The event data as a JSON-serialisable dictionary. Must not
461 contain non-serialisable types (e.g. ``datetime`` objects must be
462 ISO-formatted strings).
463 occurred_at: UTC timestamp recording when the event occurred in the
464 source service.
466 Example::
468 class UserRegisteredIntegrationEvent:
469 event_id: str = field(default_factory=lambda: str(uuid4()))
470 event_type: str = "UserRegistered"
471 source_service: str = "identity-service"
472 correlation_id: str | None = None
473 causation_id: str | None = None
474 payload: dict[str, Any] = field(default_factory=dict)
475 occurred_at: datetime = field(default_factory=lambda: datetime.now(UTC))
476 """
478 event_id: str
479 event_type: str
480 source_service: str
481 correlation_id: str | None
482 causation_id: str | None
483 payload: dict[str, Any]
484 occurred_at: datetime
487@runtime_checkable
488class WebhookSignatureVerifierProtocol(Protocol):
489 """Verifies the authenticity of inbound webhook payloads.
491 Implementations must provide both a verification method and a signature
492 computation method so callers can independently validate or generate
493 signatures without exposing the underlying algorithm.
495 The canonical implementation is HMAC-SHA256, but any MAC or asymmetric
496 scheme that fulfils this protocol is acceptable.
498 Typical usage::
500 verifier = HMACWebhookVerifier()
501 if not verifier.verify(payload=body, signature=sig_header, secret=secret):
502 raise PermissionError("Invalid webhook signature")
503 """
505 def verify(
506 self,
507 payload: bytes,
508 signature: str,
509 secret: str,
510 ) -> bool:
511 """Return ``True`` when the signature matches the payload.
513 Implementations must use a constant-time comparison to prevent
514 timing side-channel attacks.
516 Args:
517 payload: Raw request body bytes.
518 signature: Signature string as received in the request header
519 (may include algorithm prefix such as ``"sha256=..."``).
520 secret: Shared secret used to compute the expected signature.
522 Returns:
523 ``True`` if the signature is valid, ``False`` otherwise.
524 """
525 ...
527 def compute_signature(self, payload: bytes, secret: str) -> str:
528 """Compute the expected signature for *payload* using *secret*.
530 Args:
531 payload: Raw request body bytes.
532 secret: Shared secret.
534 Returns:
535 Hex-encoded signature string in the same format that
536 :meth:`verify` expects as input.
537 """
538 ...
541__all__ = [
542 "AggregateFactoryProtocol",
543 "CommandBusProtocol",
544 "CommandHandlerProtocol",
545 "DomainEventPublisherProtocol",
546 "EventBusProtocol",
547 "EventHandlerProtocol",
548 "EventMiddlewareProtocol",
549 "EventSourcedReadRepositoryProtocol",
550 "EventSourcedRepositoryProtocol",
551 "EventStoreProtocol",
552 "IntegrationEventProtocol",
553 "MultiEventHandlerProtocol",
554 "ProjectionProtocol",
555 "PubSubProtocol",
556 "QueryBusProtocol",
557 "QueryHandlerProtocol",
558 "SnapshotStoreProtocol",
559 "WebhookSignatureVerifierProtocol",
560]