Upgrading to Public API 1.0¶
Public API 1.0 gives modules one namespace, sandloom.sdk, and a declarative
form for routes, events and contracts. The migration replaces the earlier
internal API without compatibility aliases: an old import path stops working and the error names
the new one. This page lists every change a module author meets, the
replacement, and the check that reports the old form. The full name-by-name
table is generated by tools/packaging/public_api_migration.py.
Old names on this page
This is the one page that names the removed and renamed APIs, to map them to their replacements.
High impact¶
Imports move to sandloom.sdk¶
A module imports only public sandloom.sdk modules, generated contract stubs and
DTOs, and its own package (SANDLOOM-C002,
tests: SANDLOOM-C013). Every existing name
keeps its name and all its parameters at its new home, except the renames and
removals below.
| Old home | New home |
|---|---|
sandloom.core.di (sessions, dependencies) |
sandloom.sdk.web |
sandloom.core.di (effects: record_audit, on_commit, before_commit, durable_inbox, operation_key, run_async) |
sandloom.sdk.effects (record_audit, on_commit also from sandloom.sdk) |
sandloom.core.di (events: publish_event, publish_event_versions, EventEnvelope, PoisonEventError) |
sandloom.sdk.events |
sandloom.core.di (tenancy: bind_tenant, unscoped, scoped, tenant_filter, current_tenant_id) |
sandloom.sdk.tenant |
sandloom.core.di (security: token, revocation and block helpers, password hashing) |
sandloom.sdk.security |
sandloom.core.di (cache, notifier, get_core, settings) |
sandloom.sdk.infra |
sandloom.core.di (sagas: start_saga, saga_status) and sandloom.core.module_api (Saga, step, local_step) |
sandloom.sdk.sagas |
sandloom.core.di exceptions |
sandloom.sdk.errors |
sandloom.core.module_api (define_module, ModuleAPI) and sandloom.core.descriptors |
sandloom.sdk.module (define_module also from sandloom.sdk) |
sandloom.core.module_api (contract_method, the contract runtime) |
sandloom.sdk.contracts |
sandloom.core.orm |
sandloom.sdk.data (Base, TenantScoped also from sandloom.sdk) |
sandloom.core.projections |
sandloom.sdk.projections |
sandloom.core.facade |
sandloom.sdk.infra |
sandloom.shared.authz |
sandloom.sdk.authz (the beginner atoms also from sandloom.sdk) |
sandloom.shared.guards.auth, sandloom.shared.guards.platform |
sandloom.sdk.web |
sandloom.core.testing, sandloom.core.contract_testing, sandloom.core.testing.fixtures |
sandloom.sdk.testing, sandloom.sdk.testing.fixtures |
sandloom.core.app_factory.create_app |
sandloom.sdk.app |
sandloom.core.migrations.run_module_migrations |
sandloom.sdk.migrations |
sandloom docs <name> prints any name's new home and signature.
Route policy: no operation classes¶
ADR-0009 removed route classes and freshness knobs. Every route is
authenticated unless it says public(reason=...), and every authenticated
route reads the session record live
(Route policy and sessions).
| Removed | Replacement |
|---|---|
operation_class("P") |
public(reason=...); issuers public(reason=..., issues_tokens=True) |
operation_class("R" \| "RS" \| "W" \| "F" \| "A") |
nothing: the policy is derived; risk is declared at its source |
strict_revocation(), revocation_freshness(...), MAX_REVOCATION_STALENESS_SECONDS |
nothing: every route is strict |
AUTH_REVOCATION_STRICT_PATH_PREFIXES |
nothing (in config_tiers.REMOVED, inert) |
revocation_at_commit() |
automatic on writes; at_commit() for a non-write route that commits |
the class-F guard check (FinancialGuardError) |
mandatory money dedupe (SANDLOOM-R009, SANDLOOM-R010) and SANDLOOM-B019 |
assert_operation_classes(app), operation_classes(app) |
assert_route_snapshot(app), route_policy(route) |
A route that is neither authenticated nor public fails at startup (SANDLOOM-B008). Retired codes: SANDLOOM-B003, SANDLOOM-B004, SANDLOOM-C016.
Billing atoms name their account¶
Subscribed, SubscriptionActive, Credits, Feature and WithinLimit
take a required of= (SANDLOOM-B017):
| Old | New |
|---|---|
Subscribed() (default tenant_type="user") |
Subscribed(of=Account.user()) |
Subscribed(tenant_type="organization", org_param="org_id") |
Subscribed(of=Account.org(param="org_id")) |
SubscriptionActive(tenant_type="organization") on a route without the parameter |
SubscriptionActive(of=Account.org(param=None)) |
Feature(key), WithinLimit(key, used) |
Feature(key, of=Account.user()), WithinLimit(key, used, of=Account.user()) |
An organization route with of=Account.user() needs a reason
(SANDLOOM-C027). See
Authorization.
Billing API behaviour change: without org_id, /subscription,
/credits, checkout and portal use the caller's personal account. A member of
exactly one organization no longer gets that organization's account
implicitly: pass org_id.
Commands¶
| Old | New |
|---|---|
sandloom-scaffold <name>, sandloom scaffold |
sandloom add module <name> (how-to) |
sandloom-scaffold saga <module> <name> |
sandloom add saga <module> <name> (how-to) |
sandloom-migrate <command> |
sandloom migrate <command> (how-to) |
sandloom-migrate revision <module> -m "..." --autogenerate |
sandloom migrate new <module> "..." (adds row-level security) (how-to) |
adding the module to SANDLOOM_MODULES / the default module list and testpaths |
nothing: modules are discovered (app modules statically, module packages by their sandloom.modules entry point); SANDLOOM_MODULES selects a subset |
Medium impact¶
Declarative module surface¶
The level-2 forms still work; level 1 is shorter and is what the generators write (Module internals).
| Old (level 2) | New (level 1) |
|---|---|
define_module(__name__, events=EVENT_SCHEMAS, setup=...) with hand-written JSON Schemas |
class E(Event, channel="m.e.v1") in events.py; discovered |
await publish_event("m.e.v1", {...}) |
await E(...).publish() |
core.register_event_consumer(channel, handler, name=..., bind_tenant_from="tenant_id") in setup |
@on(View) on async def f(event: View) -> None in consumers.py |
@classmethod @contract_method(...) methods and core.register_service(...) |
@contract("x.y.v1") class with plain async def methods; @command(idempotency_key=...) |
ContractRequirement(...) in consumes= and core.get_service("x.y.v1") |
quota = Contract(X_Y_V1) at module level (consumes is derived); Contract(X_Y_V1, optional=True).client() when absence is a correct answer |
step("s", call="billing.credits.v1.spend", ...) |
step("s", call=credits.spend, ...) |
QueryProjectionDescriptor + consumers + monotonic_upsert + ProjectionRebuilder |
Projection(...) with SourceGroups and mappers |
router = APIRouter(...) with auth and tenant dependencies |
router = module_router() |
Sessions¶
| Old | New |
|---|---|
db: DbSession parameters threaded through services |
db() inside services (DbSession stays, advanced) |
async with db_session(): |
async with open_uow(): |
contract_session() inside a contract method |
db() |
a session passed to asyncio.gather children |
each child opens open_uow() (SANDLOOM-R013) |
| a second writing session in one request | refused at its first write (SANDLOOM-R007); writes("x") or allow_multi_db_write(...) |
Idempotency¶
Every authenticated write that carries Idempotency-Key is deduplicated
automatically, and the retry gets the stored status only (mode S). A
client that relied on a replayed body re-reads the resource with a GET, or
the route declares replay_safe(reason=...) after review. Explicit
idempotency_guard(...) calls keep all their options
(Make a write idempotent).
Tests¶
| Old | New |
|---|---|
make_user_headers(...) |
as_user(...) (same parameters and result) |
deliver_event(channel, payload, ...) |
deliver(event_or_channel, payload=None, ...) (same parameters; also takes an Event) |
capture_effects(...) for published events |
published(E) |
| a hand-built fake service bound in the registry | fake_contract(REF, impl) |
-p sandloom.core.testing.plugin |
-p sandloom.sdk.testing.plugin |
See the Testing reference.
Low impact¶
| Old | New |
|---|---|
Depends(get_current_user_id) |
user: CurrentUser (user.user_id); the function stays as sandloom.sdk.web.current_user_id |
| function-local imports in modules | top-level imports (SANDLOOM-C021); a marked optional dependency is allowed |
routes.snapshot.json regenerated by hand |
regenerated by sandloom generate with a compatibility check |
Verify¶
uv run sandloom check # reports every old import and form with its fix
uv run sandloom test --full # tests, gates and generated files