Skip to content
Menu

Documentation

sandloom.sdk.events

Events: typed Event classes and on consumers, and the L2 publishing primitives.

Public API 1.0 (spec §2.2, Appendix C).

from sandloom.sdk.events import <name>. Signatures in a terminal: sandloom docs <name>. Beginner names are the ones generated code and the examples use; any other name is advanced (mark its import line # sandloom: advanced <reason> in generated code and examples).

Start with (beginner)

Name Kind Summary
Event class Base class of typed events: class E(Event, channel=..., role="auto").
on function Register the decorated coroutine as a consumer of event (§5.4).

Advanced

Name Kind Summary
EventEnvelope callable A Mapping is a generic container for associating key/value pairs.
EventMeta class Delivery metadata of an event; mirrors the envelope, never part of the payload (event.meta on a delivered event).
PoisonEventError exception Raised by a consumer handler for an event that can NEVER be applied (malformed payload): the delivery is dead-lettered at once (CRITICAL...
publish_event async function Publish a domain event through the transactional outbox.
publish_event_versions async function Publish one domain event under several versions of its channel.

Beginner names

Event

class Event(BaseModel)() -> None

Base class of typed events: class E(Event, channel=..., role="auto").

class NoteCreated(Event, channel="notes.note.created.v1"):
    note_id: UUID
    tenant_id: UUID

await NoteCreated(note_id=note.id, tenant_id=note.tenant_id).publish()

A class without channel is an abstract base (shared fields).

Event.publish

async def publish(self, *, database: str | None = None) -> None

Publish this event in the running unit (outbox, spec §5.2).

Allowed on a producer only; a view (another module's channel) is refused (SANDLOOM-C025). Raises UnitOfWorkRequiredError outside a unit.

Event.wire_schema

@classmethod
def wire_schema(cls) -> dict[str, Any]

The schema a producer registers and exports (the L2 form is core.register_event_schema(C, E.__name__, E.wire_schema())).

Event.meta

meta  (property)

Delivery metadata (EventMetaUnavailable before delivery).

Event.model_post_init

def model_post_init(self: BaseModel, context: Any, /) -> None

This function is meant to behave like a BaseModel method to initialize private attributes.

It takes context as an argument since that's what pydantic-core passes when calling it.

Args:

  • self: The BaseModel instance.
  • context: The context.

on

def on(
    event: type[E],
    *,
    name: str | None = None,
    durable: bool = True,
    required: bool = True,
    dead_letter: bool = True,
    start_from: Literal['earliest', 'latest'] | datetime = 'earliest',
    group: str | None = None,
    database: str = 'default',
    tenant: str | None = 'derive',
) -> Callable[[Callable[[E], Awaitable[None]]], Callable[[E], Awaitable[None]]]

Register the decorated coroutine as a consumer of event (§5.4).

@on(NoteCreated)
async def count_note(event: NoteCreated) -> None:
    await counters.increment(event.tenant_id)

Each delivery runs in its own unit (db()), claimed once per envelope id with durable=True. tenant="derive" binds the tenant from a tenant_id: UUID field; tenant="<field>" names another UUID field; tenant=None binds none. name defaults to the function name.

Advanced names

EventEnvelope

def EventEnvelope(*args, **kwargs)

A Mapping is a generic container for associating key/value pairs.

This class provides concrete generic implementations of all methods except for getitem, iter, and len.

EventMeta

class EventMeta(
    id: str,
    channel: str,
    schema_version: str,
    trace_id: str | None,
    emitted_at: str,
    tenant_id: UUID | None,
) -> None

Delivery metadata of an event; mirrors the envelope, never part of the payload (event.meta on a delivered event).

PoisonEventError

class PoisonEventError(Exception)

Raised by a consumer handler for an event that can NEVER be applied (malformed payload): the delivery is dead-lettered at once (CRITICAL log) instead of being retried — the one way out for a dead_letter=False consumer, whose transient failures are retried forever.

publish_event

async def publish_event(
    channel: str,
    payload: dict[str, Any],
    schema_version: str = 'v1',
    *,
    database: str | None = None,
    required: bool | None = None,
) -> None

Publish a domain event through the transactional outbox.

Inside a UoW-managed scope (request via DbSession, db_session(), an event consumer / contract command / Celery task UoW) the event is written to the transactional outbox in the SAME transaction as the business change — it becomes visible on the bus only after commit, and is never lost or phantom-published on rollback.

Each database owns its own transactional outbox. When an endpoint holds more than one database session at once (e.g. DbSession plus Database("analytics")) pass database= to choose which database's outbox the event is written to — the business change and its event then land in the same transaction. With a single active database database may be omitted; with several active it is REQUIRED (an omitted choice raises rather than guessing and risking the wrong outbox).

Outside a UoW scope the call RAISES UnitOfWorkRequiredError (required defaults to True since 0.7). Best-effort is explicit: required=False still uses the outbox when a UoW is active, and otherwise publishes straight to the event bus — no transaction, no relay retry, lost if the broker is down. Business modules need an # sandloom: best-effort <reason> marker on such a call (sandloom check rule best-effort-effects).

publish_event_versions

async def publish_event_versions(
    payloads: Mapping[str, dict[str, Any]],
    *,
    database: str | None = None,
    bus: Any = None,
    required: bool | None = None,
) -> None

Publish one domain event under several versions of its channel.

payloads maps each versioned channel of ONE event to its payload, e.g. {"billing.paid.v1": old_shape, "billing.paid.v2": new_shape}. Inside a Unit of Work every version is preflighted (channel name + its own schema + size) and written to the transactional outbox in the current transaction — all versions commit together or none do. Publishing is in ascending version order. bus (scripts/tests) publishes straight to that bus. Outside a Unit of Work (and without bus) it raises like publish_event; required=False opts into the best-effort direct publish (same semantics and sandloom check marker as publish_event).