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.