sandloom.sdk.web¶
Routes: sessions and dependencies, the caller types, route deviations and the operation-intent primitives.
Public API 1.0 (spec §2.2, Appendix C). A name whose mechanism lands at a
later hand-off raises NotImplementedError naming it.
from sandloom.sdk.web 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 |
|---|---|---|
CurrentUser |
annotated type | |
module_router |
function | The module's router (spec §5.1), in <module>/api.py: |
public |
function | Declare a route unauthenticated, and why (ADR-0009 item 1). |
Advanced¶
| Name | Kind | Summary |
|---|---|---|
allow_multi_db_write |
function | The reviewed best-effort 1PC opt-out of the one-writer rule (spec §4.4 rule 4): the listed secondaries may write too; they commit first a... |
at_commit |
function | Re-check the caller's session before the COMMIT of EVERY unit the request opens, empty ones included (spec §4.5; today's `revocation_at_c... |
authenticates |
function | Mark fn (a FastAPI dependency) as authenticating the caller. |
authorizes |
function | Mark fn (a FastAPI dependency) as a custom authorization check. |
Capability |
function | Annotated dependency resolving any capability instance. |
current_service |
async function | The service principal of an internal call (SOURCE_INTERNAL): the internal token authenticates it (404 otherwise, existence hidden). A r... |
current_user_id |
async function | Extract the authenticated user id from the Bearer access token. |
CurrentService |
annotated type | |
Database |
function | Annotated dependency for a named database session. |
DbSession |
annotated type | |
external_call |
function | Run one external (PSP) call with no unit of the request open (§4.6). |
get_client_ip |
function | Return the real client IP for request (Starlette Request-like). |
idempotency_guard |
function | Public re-export of the Idempotency-Key dependency factory. |
ModuleRouter |
class | An APIRouter whose route decorators also take tenant: Literal["required", "optional", "none"] \| None = None. |
no_idempotency |
function | Opt a write route out of the automatic idempotency guard (streaming, non-JSON bodies). The reason is recorded in the route snapshot. |
operation_intent |
function | Declare the operation-intent mode (spec §4.6): no request unit, sequential open_uow() units, external calls in external_call(), and `... |
rate_limiter |
function | Expose rate limiter dependency routed through sandloom.core.ingress.rate_limit. |
ReadOnlyDbSession |
annotated type | |
release_check |
async function | The final re-read before an intent route releases a bearer value. |
replay_safe |
function | Opt a keyed write in to BODY replay (idempotency mode B, spec §4.7). |
request_operation_key |
function | The request's operation key: a stable digest of its route, caller, tenant and Idempotency-Key as claimed by the route's idempotency gua... |
require_platform_admin |
async function | |
ResolvedAccount |
class | The billing account a release check is for (spec §2.3, §4.6, §7). |
sensitive |
function | The escape hatch for a route whose authority check is hand-rolled: declare its risk (admin / money / security) so the framework app... |
TenantId |
annotated type | |
TenantScope |
function | FastAPI dependency that binds the request's tenant for its whole scope. |
writes |
function | Name the route's write store when it injects several writable sessions (spec §6.1, SANDLOOM-B009). |
Beginner names¶
CurrentUser¶
CurrentUser = Annotated[sandloom.shared.subject.UserContext, Depends(dependency=<function current_context>, use_cache=True, scope=None)]
(no docstring)
module_router¶
def module_router(
*,
prefix: str | None = None,
tags: Sequence[str] | None = None,
tenant: TenantBinding = 'derive',
dependencies: Sequence[params.Depends] = (),
) -> ModuleRouter
The module's router (spec §5.1), in <module>/api.py:
router = module_router()
@router.post("/", status_code=201)
async def create_note(body: NoteIn, user: CurrentUser) -> NoteOut: ...
prefix defaults to /api/v1/<module> and tags to
[<module>]. tenant="derive" binds the tenant (required) when
the module declares a TenantScoped model, else none; a route's own
tenant= overrides. The binding scopes data only; it never selects a
billing account.
public¶
def public(*, reason: str, issues_tokens: bool = False) -> params.Depends
Declare a route unauthenticated, and why (ADR-0009 item 1).
dependencies=[public(reason="liveness probe")]. A token issuer (login,
refresh, SSO callback) adds issues_tokens=True: its live issuance
check is unchanged, and an explicit idempotency guard on it must be the
release-and-re-execute form (SANDLOOM-B019). A PSP webhook is public too; its
verified signature, checked in the endpoint, is its authorization.
Advanced names¶
allow_multi_db_write¶
def allow_multi_db_write(*, secondaries: tuple[str, ...], reason: str) -> params.Depends
The reviewed best-effort 1PC opt-out of the one-writer rule (spec §4.4 rule 4): the listed secondaries may write too; they commit first and the write store last.
at_commit¶
def at_commit() -> params.Depends
Re-check the caller's session before the COMMIT of EVERY unit the
request opens, empty ones included (spec §4.5; today's
revocation_at_commit()). Writes are re-checked automatically; this is
for a non-write route with an explicit commit obligation.
authenticates¶
def authenticates(
fn: F | None = None,
*,
kind: Literal['user', 'service'] = 'user',
) -> F | Callable[[F], F]
Mark fn (a FastAPI dependency) as authenticating the caller.
@authenticates (a user subject) or @authenticates(kind="service")
(a system credential). Deny-by-default (ADR-0009, SANDLOOM-B008): every API
route resolves an authenticator or declares public(reason=...); a
module_router() route with a service authenticator gets no user
authentication injected.
authorizes¶
def authorizes(fn: _F) -> _F
Mark fn (a FastAPI dependency) as a custom authorization check.
Recorded, not enforced: the route snapshot lists the marked dependencies a
route resolves (authz, ADR-0009 "The route snapshot"), so dropping a
hand-rolled check shows up as a snapshot diff (SANDLOOM-T001). It neither
authenticates (@authenticates does) nor carries a risk
(sensitive(...) does).
Capability¶
def Capability(
capability: CapabilityRef[Any] | str,
name: str = 'default',
*,
features: set[str] | None = None,
) -> Any
Annotated dependency resolving any capability instance.
The universal alternative to a bespoke dependency per capability: it
resolves through the request app's core, enforcing any required features, and
the version when the reference carries a .vN suffix. A capability's package
publishes the reference and the Protocol:
from vector_index.contracts import VECTOR_INDEX, VectorIndex
async def handler(
index: Annotated[VectorIndex, Capability(VECTOR_INDEX, "products")],
):
...
current_service¶
async def current_service(x_internal_token: str | None = Header(None)) -> UserContext
The service principal of an internal call (SOURCE_INTERNAL): the
internal token authenticates it (404 otherwise, existence hidden). A route
taking it is service-authenticated, never user-authenticated (spec §2.3).
current_user_id¶
async def current_user_id(
subject: sandloom.shared.subject.UserContext = Depends(dependency=<function current_context>, use_cache=True, scope=None),
) -> uuid.UUID
Extract the authenticated user id from the Bearer access token.
CurrentService¶
CurrentService = Annotated[sandloom.shared.subject.UserContext, Depends(dependency=<function current_service>, use_cache=True, scope=None)]
(no docstring)
Database¶
def Database(name: str) -> Any
Annotated dependency for a named database session.
Usage in an endpoint signature:
async def handler(db: Annotated[AsyncSession, Database("analytics")]):
...
DbSession¶
DbSession = Annotated[sqlalchemy.ext.asyncio.session.AsyncSession, Depends(dependency=<function get_db>, use_cache=True, scope='function')]
(no docstring)
external_call¶
def external_call(name: str) -> AsyncIterator[None]
Run one external (PSP) call with no unit of the request open (§4.6).
Refused (SANDLOOM-R008) while a unit of the request is open; a held contract read-scope connection is released first. While it is active no unit, contract call or connection checkout may start in the request.
get_client_ip¶
def get_client_ip(request: Any, *, config: Any | None = None) -> str
Return the real client IP for request (Starlette Request-like).
Only needs request.client.host and request.headers.get, so it also
works for lightweight stand-ins in tests. config overrides the settings
object (defaults to the process settings).
idempotency_guard¶
def idempotency_guard(
*,
scope: str,
ttl_seconds: int = 86400,
required: bool = True,
fail_closed: bool = False,
store_response: bool = True,
durable: bool = False,
) -> Any
Public re-export of the Idempotency-Key dependency factory.
See sandloom.core.idempotency. Use as an endpoint dependency to make a
mutating operation safe against duplicate client retries. Pass
fail_closed=True for money / irreversible side effects so a cache
outage returns 503 instead of allowing a possible duplicate.
store_response=False: in-flight dedupe only — nothing is stored or
replayed (the endpoint owns durable idempotency; its response must not be
replayed without re-authorization).
durable=True: the record is claimed in the request's DbSession
Unit of Work and committed with the endpoint's effect (exactly-once
effect on that database, replay of the stored response; ADR-0005) —
the form a money route that stores responses needs (SANDLOOM-B019).
ModuleRouter¶
class ModuleRouter(APIRouter)(
*,
sandloom_owner: str,
sandloom_tenant: TenantBinding = 'derive',
prefix: str = '',
tags: list[str | Enum] | None = None,
dependencies: Sequence[params.Depends] | None = None,
**kwargs: Any,
) -> None
An APIRouter whose route decorators also take
tenant: Literal["required", "optional", "none"] | None = None.
Built by module_router; collected by discovery wherever it is
defined in the module's api package.
ModuleRouter.route_policy¶
def route_policy(
self,
path: str,
endpoint: Callable[..., Any],
dependencies: Sequence[params.Depends] | None,
tenant: RouteTenant,
) -> list[params.Depends]
The dependencies add_api_route adds in front of the route's.
ModuleRouter.add_api_route¶
def add_api_route(
self,
path: str,
endpoint: Callable[..., Any],
*,
tenant: RouteTenant = None,
dependencies: Sequence[params.Depends] | None = None,
**kwargs: Any,
) -> None
ModuleRouter.api_route¶
def api_route(
self,
path: str,
*,
tenant: RouteTenant = None,
**kwargs: Any,
) -> Callable[[DecoratedCallable], DecoratedCallable]
ModuleRouter.get¶
def get(
self,
path: str,
*,
tenant: RouteTenant = None,
**kwargs: Any,
) -> Callable[[DecoratedCallable], DecoratedCallable]
Add a path operation using an HTTP GET operation.
Example¶
``python from fastapi import APIRouter, FastAPI
app = FastAPI() router = APIRouter()
@router.get("/items/") def read_items(): return [{"name": "Empanada"}, {"name": "Arepa"}]
app.include_router(router) ``
ModuleRouter.put¶
def put(
self,
path: str,
*,
tenant: RouteTenant = None,
**kwargs: Any,
) -> Callable[[DecoratedCallable], DecoratedCallable]
Add a path operation using an HTTP PUT operation.
Example¶
``python from fastapi import APIRouter, FastAPI from pydantic import BaseModel
class Item(BaseModel): name: str description: str | None = None
app = FastAPI() router = APIRouter()
@router.put("/items/{item_id}") def replace_item(item_id: str, item: Item): return {"message": "Item replaced", "id": item_id}
app.include_router(router) ``
ModuleRouter.post¶
def post(
self,
path: str,
*,
tenant: RouteTenant = None,
**kwargs: Any,
) -> Callable[[DecoratedCallable], DecoratedCallable]
Add a path operation using an HTTP POST operation.
Example¶
``python from fastapi import APIRouter, FastAPI from pydantic import BaseModel
class Item(BaseModel): name: str description: str | None = None
app = FastAPI() router = APIRouter()
@router.post("/items/") def create_item(item: Item): return {"message": "Item created"}
app.include_router(router) ``
ModuleRouter.delete¶
def delete(
self,
path: str,
*,
tenant: RouteTenant = None,
**kwargs: Any,
) -> Callable[[DecoratedCallable], DecoratedCallable]
Add a path operation using an HTTP DELETE operation.
Example¶
``python from fastapi import APIRouter, FastAPI
app = FastAPI() router = APIRouter()
@router.delete("/items/{item_id}") def delete_item(item_id: str): return {"message": "Item deleted"}
app.include_router(router) ``
ModuleRouter.options¶
def options(
self,
path: str,
*,
tenant: RouteTenant = None,
**kwargs: Any,
) -> Callable[[DecoratedCallable], DecoratedCallable]
Add a path operation using an HTTP OPTIONS operation.
Example¶
``python from fastapi import APIRouter, FastAPI
app = FastAPI() router = APIRouter()
@router.options("/items/") def get_item_options(): return {"additions": ["Aji", "Guacamole"]}
app.include_router(router) ``
ModuleRouter.head¶
def head(
self,
path: str,
*,
tenant: RouteTenant = None,
**kwargs: Any,
) -> Callable[[DecoratedCallable], DecoratedCallable]
Add a path operation using an HTTP HEAD operation.
Example¶
``python from fastapi import APIRouter, FastAPI from pydantic import BaseModel
class Item(BaseModel): name: str description: str | None = None
app = FastAPI() router = APIRouter()
@router.head("/items/", status_code=204) def get_items_headers(response: Response): response.headers["X-Cat-Dog"] = "Alone in the world"
app.include_router(router) ``
ModuleRouter.patch¶
def patch(
self,
path: str,
*,
tenant: RouteTenant = None,
**kwargs: Any,
) -> Callable[[DecoratedCallable], DecoratedCallable]
Add a path operation using an HTTP PATCH operation.
Example¶
``python from fastapi import APIRouter, FastAPI from pydantic import BaseModel
class Item(BaseModel): name: str description: str | None = None
app = FastAPI() router = APIRouter()
@router.patch("/items/") def update_item(item: Item): return {"message": "Item updated in place"}
app.include_router(router) ``
ModuleRouter.trace¶
def trace(
self,
path: str,
*,
tenant: RouteTenant = None,
**kwargs: Any,
) -> Callable[[DecoratedCallable], DecoratedCallable]
Add a path operation using an HTTP TRACE operation.
Example¶
``python from fastapi import APIRouter, FastAPI from pydantic import BaseModel
class Item(BaseModel): name: str description: str | None = None
app = FastAPI() router = APIRouter()
@router.trace("/items/{item_id}") def trace_item(item_id: str): return None
app.include_router(router) ``
no_idempotency¶
def no_idempotency(*, reason: str) -> params.Depends
Opt a write route out of the automatic idempotency guard (streaming, non-JSON bodies). The reason is recorded in the route snapshot.
operation_intent¶
def operation_intent(*, reason: str) -> params.Depends
Declare the operation-intent mode (spec §4.6): no request unit,
sequential open_uow() units, external calls in external_call(),
and release_check() before a 2xx (SANDLOOM-R008 / SANDLOOM-R012). Only for
routes that call an external provider (billing checkout and portal).
rate_limiter¶
def rate_limiter(*, requests: int, period: int, fail_closed: bool = False) -> Any
Expose rate limiter dependency routed through sandloom.core.ingress.rate_limit.
Import lazily so test patches against
sandloom.core.ingress.rate_limit.rate_limiter
are respected. Pass fail_closed=True on admission-control endpoints
(login, password reset, payment) so a cache outage returns 503 rather than
silently lifting the limit.
ReadOnlyDbSession¶
ReadOnlyDbSession = Annotated[sqlalchemy.ext.asyncio.session.AsyncSession, Depends(dependency=<function get_read_only_db>, use_cache=True, scope='function')]
(no docstring)
release_check¶
async def release_check(
account: ResolvedAccount,
*,
authority: Callable[[], Awaitable[None]] | None = None,
) -> None
The final re-read before an intent route releases a bearer value.
authority (the authority re-read, e.g. billing's
reverify_authority) is REQUIRED for an organization account
(SANDLOOM-R012). Then the request subject's session is checked live (401
revoked / blocked; any error fails closed). For its whole duration no
unit, command or connection checkout may start in the request, except the
authority callback's own read-contract calls (SANDLOOM-R008).
replay_safe¶
def replay_safe(*, reason: str) -> params.Depends
Opt a keyed write in to BODY replay (idempotency mode B, spec §4.7).
Only when the response contains nothing whose visibility could change between the attempts (typically a creation echoing the caller's own fields and the new id) — never a secret, a bearer capability, other users' data or role / ownership dependent fields. The reason is reviewed in the route snapshot. Without it the automatic guard replays status only.
request_operation_key¶
def request_operation_key() -> str | None
The request's operation key: a stable digest of its route, caller,
tenant and Idempotency-Key as claimed by the route's idempotency guard,
or None (no key, or no guard). Pass it as a money effect's
operation_key (ADR-0009 item 11).
require_platform_admin¶
async def require_platform_admin(
subject: sandloom.shared.subject.UserContext = Depends(dependency=<function current_context>, use_cache=True, scope=None),
) -> None
(no docstring)
ResolvedAccount¶
class ResolvedAccount(kind: Literal['user', 'org'], id: UUID) -> None
The billing account a release check is for (spec §2.3, §4.6, §7).
sensitive¶
def sensitive(risk: Risk) -> params.Depends
The escape hatch for a route whose authority check is hand-rolled:
declare its risk (admin / money / security) so the framework
applies what that risk triggers (ADR-0009 item 9).
TenantId¶
TenantId = Annotated[uuid.UUID, Depends(dependency=<function TenantScope.<locals>._dep>, use_cache=True, scope=None)]
(no docstring)
TenantScope¶
def TenantScope(
*,
header: str = 'X-Tenant-Id',
claim_keys: tuple[str, ...] = ('tenant_id', 'org_id'),
required: bool = True,
) -> Any
FastAPI dependency that binds the request's tenant for its whole scope.
Resolves the tenant id from the X-Tenant-Id header, else a JWT claim
(tenant_id/org_id). Binds the contextvar before the endpoint runs and
resets it afterwards, so scoped() / automatic enforcement see it. With
required (default) a missing tenant is a 400; otherwise it yields
None and leaves the context unbound.
Usage:
async def handler(tenant: Annotated[UUID, TenantScope()]):
...
writes¶
def writes(database: str) -> params.Depends
Name the route's write store when it injects several writable sessions (spec §6.1, SANDLOOM-B009).