Skip to content
Menu

Documentation

sandloom.sdk.sagas

Sagas (ADR-0008): definitions, steps, start and status.

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

from sandloom.sdk.sagas 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).

Advanced

Name Kind Summary
CommandStep class A step that calls a command contract (call="<contract>.<method>").
Direction callable
local_step function A local step: await apply(db, ctx) inside the transition's UoW.
LocalStep class A step run in the owner's own Unit of Work (transactional, §1).
Saga class A saga definition (ADR-0008), discovered from the module's sagas submodule; every parameter is today's.
saga_contract_requirements function One ContractRequirement(name, methods=...) per contract it calls.
saga_status async function The instance of definition for saga_id (None if never started).
saga_step_key function The idempotency key the runtime injects into a step's command call.
SagaContext class What a step's argument function (or local step) sees.
SagaHandle class What start_saga / saga_status return.
SagaInputConflict exception start_saga of an existing business id with OTHER input (409).
SagaStep constant
start_saga async function Start definition for business id saga_id in the caller's UoW.
step function A command step: call a command contract with args(ctx):
StepKind class The kind of a saga step (Garcia-Molina & Salem; Richardson).
synthetic_instance function An instance of model built from its schema alone (no validation).

Advanced names

CommandStep

class CommandStep(
    name: str,
    call: str,
    args: ArgsFn,
    compensate: str | None = None,
    compensate_args: ArgsFn | None = None,
    kind: StepKind | None = None,
    refuse_on: frozenset[int] = frozenset(),
    check_args: bool = True,
    deadline: timedelta | None = None,
) -> None

A step that calls a command contract (call="<contract>.<method>").

CommandStep.local

local  (property)

CommandStep.contract

contract  (property)

CommandStep.method

method  (property)

CommandStep.compensate_contract

compensate_contract  (property)

CommandStep.compensate_method

compensate_method  (property)

CommandStep.has_compensation

has_compensation  (property)

CommandStep.undo_args

def undo_args(self) -> ArgsFn

The compensation's argument function (default: the forward one).

Direction

def Direction(*args, **kwargs)

(no docstring)

local_step

def local_step(
    name: str,
    *,
    apply: LocalFn,
    compensate: LocalFn | None = None,
    kind: StepKind | str | None = None,
    deadline: timedelta | None = None,
) -> LocalStep

A local step: await apply(db, ctx) inside the transition's UoW.

Its writes commit with the instance transition (transactional, no command log needed); raising rolls them back (a savepoint). Raise an exception with an http_status of 400/402/403/404/410/422 for a definitive refusal; anything else is retried. The returned value (JSON-able) is persisted as the step's result.

LocalStep

class LocalStep(
    name: str,
    apply: LocalFn,
    compensate: LocalFn | None = None,
    kind: StepKind | None = None,
    deadline: timedelta | None = None,
) -> None

A step run in the owner's own Unit of Work (transactional, §1).

LocalStep.local

local  (property)

LocalStep.has_compensation

has_compensation  (property)

Saga

class Saga(Saga)(
    name: str,
    *,
    version: int,
    input: type[InputT],
    deadline: timedelta,
    steps: Sequence[SagaStep],
    compensation_deadline: timedelta | None = None,
) -> None

A saga definition (ADR-0008), discovered from the module's sagas submodule; every parameter is today's.

saga_contract_requirements

def saga_contract_requirements(saga: Saga[Any]) -> list[ContractRequirement]

One ContractRequirement(name, methods=...) per contract it calls.

register_saga adds these to the module's consumes, so a missing provider is a ContractBindingError at startup like any consumed contract, and the rolling-upgrade check covers exactly these methods.

saga_status

async def saga_status(definition: Saga[Any], saga_id: object) -> SagaHandle | None

The instance of definition for saga_id (None if never started).

saga_step_key

def saga_step_key(saga_type: str, saga_id: str, step: str, direction: Direction) -> str

The idempotency key the runtime injects into a step's command call.

saga:<saga_type>:<saga_id>:<step>:do (forward) or …:undo (compensation) — stable across attempts, so the provider's durable command log replays a step that already ran instead of running it twice.

SagaContext

class SagaContext(Generic)(
    saga_type: str,
    saga_id: str,
    input: InputT,
    results: Mapping[str, Any] = <factory>,
    synthetic: bool = False,
) -> None

What a step's argument function (or local step) sees.

input is the definition's input model; results maps each applied step's name to its persisted (JSON) result. key(step) is the derived operation key of a step (e.g. to pass the charge's key to the refund); result(step, Model) decodes a step's result to Model.

SagaContext.key

def key(self, step: str, direction: Direction = 'do') -> str

The operation key the runtime uses for step (do / undo).

SagaContext.result

def result(self, step: str, model: Any = None) -> Any

The result step returned, decoded to model when given.

Raises KeyError for a step that has not been applied (yet).

SagaHandle

class SagaHandle(
    saga_type: str,
    saga_id: str,
    instance_id: uuid.UUID,
    status: str,
    created: bool = False,
    step: str | None = None,
    park_reason: str | None = None,
    version: int = 0,
) -> None

What start_saga / saga_status return.

SagaInputConflict

class SagaInputConflict(RuntimeError)

start_saga of an existing business id with OTHER input (409).

SagaStep

SagaStep: UnionType

(no docstring)

start_saga

async def start_saga(
    definition: Saga[Any],
    *,
    saga_id: object,
    input: BaseModel | Mapping[str, Any],
) -> SagaHandle

Start definition for business id saga_id in the caller's UoW.

Inserts the instance row and publishes its first nudge through the outbox in the SAME transaction as the business change. Starting the same saga_id again is a create-or-get: equal input returns the existing instance (created=False), other input raises SagaInputConflict (409). Outside a Unit of Work it raises UnitOfWorkRequiredError, like publish_event.

step

def step(
    name: str,
    *,
    call: CommandRef,
    args: ArgsFn,
    compensate: CommandRef | None = None,
    compensate_args: ArgsFn | None = None,
    kind: StepKind | str | None = None,
    refuse_on: Iterable[int] = (),
    check_args: bool = True,
    deadline: timedelta | None = None,
) -> CommandStep

A command step: call a command contract with args(ctx):

credits = Contract(BILLING_CREDITS_V1)
step("charge", call=credits.spend, args=lambda ctx: {...},
     compensate=credits.grant)

Identical to the L2 step with call="billing.credits.v1.spend".

StepKind

class StepKind(StrEnum)

The kind of a saga step (Garcia-Molina & Salem; Richardson).

synthetic_instance

def synthetic_instance(model: type[T]) -> T

An instance of model built from its schema alone (no validation).

Defaults are kept; required fields get a representative value of their type (1, "x", UUID(int=1), the first enum member, a nested synthetic model, an empty collection). Used to run a step's args function once at startup (check_args); a function that cannot run on such input opts out per step with check_args=False.