sandloom.sdk.contracts¶
Sync contracts: typed providers and callers, contract_method and its
helpers, and (advanced) the provider runtime.
Public API 1.0 (spec §2.2, Appendix C).
from sandloom.sdk.contracts 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 |
|---|---|---|
command |
function | Mark a provider method as a command: deduplicated by its idempotency_key argument, committed in the provider's own unit, not atomic wit... |
Contract |
function | The caller of contract ref (spec §5.3), declared at module level: |
contract |
function | Declare a stateless provider class of contract name: |
Advanced¶
| Name | Kind | Summary |
|---|---|---|
BACKWARD |
constant | |
bind_contract |
function | Wrap impl in a ContractProvider (idempotent). |
caller_user_id |
function | The end user on whose behalf the running contract call executes, or None (a system / service caller, a task, a consumer). |
contract_dto_problems |
function | DTOs of contract name no caller process could hold (SANDLOOM-C012). |
contract_error |
function | Build the caller-side exception for a contract error (local or RPC). |
contract_kind |
function | How a BOUND contract member runs: read / command, or None for a plain (non-contract) member of the provider. |
contract_method |
function | Mark an async method as a service-contract entry point. |
contract_read_scope |
function | Open a ContractReadScope for the block (reuses an outer one). |
contract_reads |
function | The declared-operations handle (sandloom.core.orm.Reads) of the running contract call. |
contract_schema |
function | The validated ContractSchema of a contract provider (cached). |
contract_session_bound |
function | Whether a test seam session (bind_contract_session) is bound. |
contract_spec |
function | The ContractMethodSpec of a (bound) contract method, if any. |
ContractMethodRuntime |
class | The validated, wire-portable shape of one exposed contract method. |
ContractMethodSpec |
class | How the contract runtime runs one contract method. |
ContractProvider |
class | A registered contract bound to its provider's core and database. |
ContractReadScope |
class | A request-scoped scope in which LOCAL read-contract calls share work. |
ContractReadScopeMiddleware |
class | Pure-ASGI middleware opening a ContractReadScope per request. |
ContractRef |
class | A typed reference to a contract (emitted by the stub generator): `STATS_QUOTA_V1: ContractRef[StatsQuotaV1] = ContractRef("stats.quota.v1... |
ContractSchema |
class | Every exposed method of one registered contract provider + a stable hash. |
current_contract_read_scope |
function | The read scope bound to the current context, if any. |
DEFAULT_CONTRACT_DTO_PACKAGES |
constant | |
FORWARD |
constant | |
invalidate_contract_read_memo |
function | Clear the current read scope's memo (a write happened elsewhere). |
normalize_contract_error |
function | The caller-side form of a provider exception, or None (a 5xx bug). |
OptionalContract |
class | Contract(REF, optional=True): client() is None only when the provider is neither local nor bound in this deployment (decided at sta... |
run_contract_call |
async function | Run one contract call in the provider's own Unit of Work. |
service |
function | The proxy of contract name (spec §2.3, §5.3). |
Beginner names¶
command¶
def command(
*,
idempotency_key: str,
idempotency_scope: tuple[str, ...] = (),
database: str | None = None,
risk: Literal['money'] | None = None,
) -> Callable[[Callable[PS, R]], Callable[PS, R]]
Mark a provider method as a command: deduplicated by its
idempotency_key argument, committed in the provider's own unit, not
atomic with the caller (a saga step):
@command(idempotency_key="operation_key")
async def reserve(self, amount: int, operation_key: str) -> int: ...
risk="money" declares a money source (ADR-0009).
Contract¶
def Contract(ref: ContractRef[P], *, optional: bool = False) -> P | OptionalContract[P]
The caller of contract ref (spec §5.3), declared at module level:
quota = Contract(STATS_QUOTA_V1) # required: startup fails without it
if not await quota.may_create_note(): ...
define_module adds it to the module's consumes (name and
required exact; methods from the direct quota.<method>( calls in
the defining module, else every method).
contract¶
def contract(name: str, *, database: str | None = None) -> Callable[[C], C]
Declare a stateless provider class of contract name:
@contract("stats.quota.v1")
class Quota:
async def may_create_note(self) -> bool: ...
Public async def methods are reads; @command(...) marks commands;
@contract.read(planned=..., database=...) keeps today's read options.
Advanced names¶
BACKWARD¶
BACKWARD: str = 'undo'
(no docstring)
bind_contract¶
def bind_contract(impl: Any, *, core: Any, database: str, name: str) -> Any
Wrap impl in a ContractProvider (idempotent).
caller_user_id¶
def caller_user_id() -> UUID | None
The end user on whose behalf the running contract call executes, or
None (a system / service caller, a task, a consumer).
Locally the authenticated subject of the calling request; over RPC the same subject, carried hop after hop in the signed caller facts of each call's context (the provider binds them as its call's request scope). A provider holds a personal resource to it (billing's personal spend, Public API 1.0 spec §7.4).
contract_dto_problems¶
def contract_dto_problems(
name: str,
impl: Any,
*,
provider_module: str | None,
module_packages: Iterable[str] = (),
shared_packages: Iterable[str] = (),
) -> list[str]
DTOs of contract name no caller process could hold (SANDLOOM-C012).
A provider authors its contract DTOs (pydantic models, Enums) in its own
module package; sandloom generate copies each one a signature names
into the shared DTO package, where every caller imports the copy (the
stub names it, and a remote caller decodes into it). A DTO a module
package holds is therefore allowed in its OWN provider's signatures; what
no copy can come from is refused: a class of ANOTHER module's package
(use its generated copy from the shared DTO package), or one defined in a
function or __main__. Classes of a shared package (sandloom.shared,
CONTRACT_DTO_PACKAGES) are referenced as they are. The closed set of
DTO shapes is checked when @contract decorates the provider.
Checked only for contracts a MODULE registers (provider_module known);
kernel-level test registrations are exempt.
contract_error¶
def contract_error(
*,
status_code: int,
detail: Any,
headers: Mapping[str, str] | None = None,
code: str | None = None,
) -> ContractCallError
Build the caller-side exception for a contract error (local or RPC).
contract_kind¶
def contract_kind(method: Any) -> Literal['read', 'command'] | None
How a BOUND contract member runs: read / command, or None
for a plain (non-contract) member of the provider.
contract_method¶
def contract_method(
fn: _F | None = None,
*,
command: bool = False,
idempotency_key: str | None = None,
idempotency_scope: tuple[str, ...] = (),
database: str | None = None,
planned: bool = False,
risk: Literal['admin', 'money', 'security'] | None = None,
) -> _F | Callable[[_F], _F]
Mark an async method as a service-contract entry point.
Every call — local (core.get_service) or remote (internal RPC server) —
runs through run_contract_call: a command's idempotency_key
argument is required, and the
method body runs in the provider's own Unit of Work
(contract_session). Stack it UNDER @classmethod.
command=True commits the provider's UoW; such a call is NOT atomic with
the caller's transaction (saga semantics — the caller compensates or uses
events). database only applies to direct calls of an UNREGISTERED
provider (tests): a direct command call of a registered provider runs on
its registration's database under its contract name, sharing the
registry path's dedupe record.
idempotency_scope partitions a command's operation keys by named domain
arguments, for example ("tenant_type", "tenant_id") for personal and
organization billing accounts. Changing this tuple changes the dedupe
namespace; retain domain UNIQUE operation keys when rolling out a change.
The signature must be wire-portable (checked when the provider is
registered): every parameter and the return annotated with a contract wire
type (sandloom.core.contract_wire), no *args / **kwargs.
planned=True (reads only) declares a contract whose body runs only
declared planner operations through contract_reads: no Unit of
Work is opened; the reads lease from the contract pool in a read-only
scope (publish_event / record_audit raise), with the tenant bound
exactly as for any contract call (the caller's locally, the verified RPC
context remotely).
risk (commands only) declares the risk of the command's effect at its
source, once (ADR-0009 item 7): risk="money" for a command that moves
money. It is recorded in the contract's describe document and snapshot;
a local call in a request records the effect there (its own
idempotency_key is its dedupe record), which triggers the automatic
audit row of a sensitive write.
contract_read_scope¶
def contract_read_scope(
*,
memo: bool | None = None,
idle_ms: int | None = None,
) -> AsyncIterator[ContractReadScope]
Open a ContractReadScope for the block (reuses an outer one).
The framework opens one per HTTP request; open one explicitly in a
worker / script that makes several local read-contract calls in a row.
Memoisation is on unless memo=False; idle_ms defaults to
CONTRACT_READ_SCOPE_IDLE_MS, and idle_ms < 0 holds the shared
connection until the block ends.
contract_reads¶
def contract_reads() -> Any
The declared-operations handle (sandloom.core.orm.Reads) of the running
contract call.
In a PLANNED read contract (@contract_method(planned=True)) the reads
run in the call's read-only scope on the provider's contract pool (one
shared read-only transaction for general-executor reads; the optimized
executor when enabled and eligible) — never in the caller's transaction,
locally and over RPC alike. In any other contract call they run on
contract_session. Under the test seam
(bind_contract_session) they run on the bound test session.
contract_schema¶
def contract_schema(impl: Any) -> ContractSchema
The validated ContractSchema of a contract provider (cached).
Every public member must be a contract_method with a
wire-portable signature, or a ClassVar constant (spec §5.3: never
exposed); private members (_name) are never exposed. Raises
sandloom.core.contract_wire.ContractTypeError listing every problem —
called at registration, so a non-portable contract never deploys.
contract_session_bound¶
def contract_session_bound() -> bool
Whether a test seam session (bind_contract_session) is bound.
Contract calls then all run on that ONE session, so callers must not run them concurrently.
contract_spec¶
def contract_spec(method: Any) -> ContractMethodSpec | None
The ContractMethodSpec of a (bound) contract method, if any.
ContractMethodRuntime¶
class ContractMethodRuntime(
name: str,
kind: Literal['read', 'command'],
signature: inspect.Signature,
annotations: dict[str, Any],
returns: Any,
idempotency_key: str | None = None,
idempotency_scope: tuple[str, ...] = (),
risk: str | None = None,
) -> None
The validated, wire-portable shape of one exposed contract method.
Built once at registration; the local path and the RPC server bind, validate and encode every call through it, so both see the same argument rules and the same result types.
ContractMethodRuntime.describe¶
def describe(self) -> dict[str, Any]
JSON-able schema published by the RPC describe endpoint.
ContractMethodRuntime.bind¶
def bind(
self,
label: str,
args: Sequence[Any],
kwargs: Mapping[str, Any],
*,
local: bool = False,
) -> tuple[tuple[Any, ...], dict[str, Any]]
Bind + validate UNTAGGED arguments exactly like a Python call would.
Returns (positional_only_args, keyword_args). Unknown / missing
arguments, positional-only or keyword-only violations and values that
do not validate against their annotation raise
ContractArgumentError (422) before the provider runs.
local=True binds the caller's ORIGINAL values of an in-process
call: each goes through its parameter's WirePlan (immutable
exact-type values pass through, fully typed ones round-trip through
pydantic-core, the rest through the tagged tree) — what the provider
would receive over RPC.
ContractMethodRuntime.decode_result¶
def decode_result(self, wire: Any, codec: int = 1) -> Any
Rebuild a result from its wire form using the return annotation.
ContractMethodRuntime.local_result¶
def local_result(self, label: str, result: Any) -> Any
What an in-process caller receives for result (see bind(local=)).
ContractMethodSpec¶
class ContractMethodSpec(
kind: Literal['read', 'command'] = 'read',
idempotency_key: str | None = None,
database: str | None = None,
planned: bool = False,
idempotency_scope: tuple[str, ...] = (),
risk: str | None = None,
) -> None
How the contract runtime runs one contract method.
ContractMethodSpec.readonly¶
readonly (property)
ContractProvider¶
class ContractProvider(impl: Any, *, core: Any, database: str, name: str) -> None
A registered contract bound to its provider's core and database.
Returned by core.get_service for a locally provided contract and used
by the internal RPC server, so both paths run every call through
run_contract_call. Only the contract's exposed methods are visible
(exactly what a remote proxy can call); each local call is bound with
Python's rules, its arguments and result round-trip through the wire codec
and are validated against the annotations, and provider errors surface as
ContractCallError — so a local call is observably identical to a
remote one.
ContractProvider.contract_impl¶
contract_impl (property)
The registered provider object this binding wraps.
ContractProvider.contract_schema¶
contract_schema (property)
The exposed methods (and hash) of this contract.
ContractProvider.contract_name¶
contract_name (property)
ContractProvider.call_local¶
async def call_local(
self,
runtime: ContractMethodRuntime,
args: tuple[Any, ...],
kwargs: dict[str, Any],
) -> Any
An in-process call: exactly what the provider/caller see over RPC.
Arguments and result go through their :class:~sandloom.core.contract_wire.
WirePlan (validated copies; immutable exact-type values pass through
untouched). Inside a ContractReadScope an identical read call
is served from the scope's memo, and any command clears it.
ContractProvider.encode_result¶
def encode_result(
self,
runtime: ContractMethodRuntime,
result: Any,
*,
typed: bool = False,
) -> Any
The tagged wire form of result (see encode_result_as).
ContractProvider.encode_result_as¶
def encode_result_as(
self,
runtime: ContractMethodRuntime,
result: Any,
*,
typed: bool = False,
) -> tuple[int, Any]
(codec, wire) of result; codec v2 when typed and it fits.
ContractProvider.invoke¶
async def invoke(
self,
runtime: ContractMethodRuntime,
args: Sequence[Any],
kwargs: Mapping[str, Any],
) -> Any
Bind + validate UNTAGGED arguments and run the provider (both paths).
Raises ContractCallError for argument errors and for every
provider HTTPException / http_status exception; anything else is
a provider failure and propagates unchanged.
ContractReadScope¶
class ContractReadScope(*, memo: bool = True, idle_seconds: float | None = 0.0) -> None
A request-scoped scope in which LOCAL read-contract calls share work.
Opened per HTTP request by the framework (ContractReadScopeMiddleware)
or explicitly with contract_read_scope. Inside it:
- all local read-contract calls on the same database reuse ONE connection
/ transaction from the provider's contract pool (READ COMMITTED: every
statement sees the latest committed data, exactly like a fresh UoW per
call). It is NEVER the caller's transaction, so — as over RPC — the
provider does not see the caller's uncommitted writes. It is always
rolled back: when the scope ends, when a call raises or leaves ORM
writes pending, and once the request stops making read calls —
idle_seconds=0(default) releases it on the next event-loop turn after a call, i.e. as soon as the request awaits anything else, so back-to-back calls share it but it never idles in the pool's absence while the request does other I/O;>0keeps it that long;Noneholds it until the scope ends. A call that finds the shared connection busy (concurrent tasks, a read contract calling another on the same database) falls back to its own Unit of Work. A call that finds the shared transaction aborted by an earlier call's swallowed database error (SQLSTATE 25P02) drops that connection and reruns in its own Unit of Work (stats["replaced"]); - identical read calls (local or RPC; same contract, method, bound tenant and
arguments) are memoised for the rest of the scope — each caller still
receives its own copy. Any command call (local or remote) clears the
memo. External commits are visible after invalidation
or in a new scope; use
memo=Falsewhen every read must be fresh.
A read contract must not write: a raw DML statement it executed would be visible to later read contracts of the same scope until rollback.
ContractReadScope.stats¶
stats (property)
Counters (diagnostics / performance budget tests).
ContractReadScope.memo_enabled¶
memo_enabled (property)
ContractReadScope.memo_get¶
def memo_get(self, key: Any) -> Any
ContractReadScope.memo_set¶
def memo_set(self, key: Any, value: Any) -> None
ContractReadScope.invalidate¶
def invalidate(self) -> None
Forget memoised reads (a command may have changed what they return).
ContractReadScope.run¶
async def run(
self,
func: Callable[..., Awaitable[Any]],
args: tuple[Any, ...],
kwargs: dict[str, Any],
*,
core: Any,
database: str,
qualname: str,
) -> Any
Run a read contract on the shared unit, or _SCOPE_FALLBACK.
ContractReadScope.release_connections¶
async def release_connections(self) -> None
Roll back and release every shared connection NOW; the scope stays
usable (a later read reopens one). The operation-intent windows
(external_call / release_check) release it first (spec §4.6).
A read still running on a shared connection (another task) is waited for, then its unit is closed: no read connection survives the drain. A read that cannot finish first — the caller runs inside it — is refused (SANDLOOM-R008) instead of waited for.
ContractReadScope.aclose¶
async def aclose(self) -> None
End the scope: roll back and release every shared connection.
ContractReadScopeMiddleware¶
class ContractReadScopeMiddleware(app: Any) -> None
Pure-ASGI middleware opening a ContractReadScope per request.
Lazy: nothing is checked out unless the request makes a local read
contract call. Disabled by CONTRACT_READ_SCOPE_ENABLED=false.
ContractRef¶
class ContractRef(Generic)(name: str, protocol: type[P] | None = None) -> None
A typed reference to a contract (emitted by the stub generator):
STATS_QUOTA_V1: ContractRef[StatsQuotaV1] = ContractRef("stats.quota.v1",
StatsQuotaV1).
protocol is the stub's Protocol: a Contract(REF) caller receives
results as its methods' annotations name them (the generated DTO copies),
locally and over RPC. None (a hand-written reference): results as the
runtime returns them.
ContractSchema¶
class ContractSchema(methods: dict[str, ContractMethodRuntime], hash: str) -> None
Every exposed method of one registered contract provider + a stable hash.
ContractSchema.describe¶
def describe(self) -> dict[str, Any]
current_contract_read_scope¶
def current_contract_read_scope() -> ContractReadScope | None
The read scope bound to the current context, if any.
DEFAULT_CONTRACT_DTO_PACKAGES¶
DEFAULT_CONTRACT_DTO_PACKAGES: tuple = ('sandloom.shared',)
(no docstring)
FORWARD¶
FORWARD: str = 'do'
(no docstring)
invalidate_contract_read_memo¶
def invalidate_contract_read_memo() -> None
Clear the current read scope's memo (a write happened elsewhere).
normalize_contract_error¶
def normalize_contract_error(exc: BaseException) -> ContractCallError | None
The caller-side form of a provider exception, or None (a 5xx bug).
HTTPException and http_status-protocol exceptions become a
ContractCallError with the same status, JSON-encoded detail,
headers and code — exactly what the RPC server transmits.
OptionalContract¶
class OptionalContract(Generic)(ref: ContractRef[P]) -> None
Contract(REF, optional=True): client() is None only when the
provider is neither local nor bound in this deployment (decided at
startup); a deployed provider that is down still raises (503).
Use it only where the caller has a correct answer without the provider
(a feature off, an enrichment skipped); a gate whose absence must deny is
a required Contract(REF) (startup fails without it, SANDLOOM-B005).
OptionalContract.contract_name¶
contract_name (property)
OptionalContract.required¶
required (property)
OptionalContract.client¶
def client(self) -> P | None
The provider's proxy, or None when it is not deployed. Its
results arrive as the stub annotates them, like Contract(REF)'s.
run_contract_call¶
async def run_contract_call(
func: Callable[..., Awaitable[Any]],
args: tuple[Any, ...],
kwargs: dict[str, Any],
*,
spec: ContractMethodSpec,
core: Any = None,
database: str = 'default',
contract: str | None = None,
) -> Any
Run one contract call in the provider's own Unit of Work.
The single code path for local and RPC-served contract calls: enforces a
command's idempotency key and
deduplicates commands through the provider database's durable command log
(claimed and completed inside the command's own UoW), opens
the provider's UoW on database (rolled back for a read, committed +
outbox dispatched for a command) and exposes it via
contract_session. contract is the registered contract name (the
dedupe namespace; defaults to the method's qualified name).
service¶
def service(name: str, *, optional: bool = False) -> Any
The proxy of contract name (spec §2.3, §5.3).
optional=True returns None only when the provider is neither local
nor bound in this deployment; a deployed provider that is down still
raises when called.