sandloom.sdk.module¶
Modules: define_module, the L2 register form (ModuleAPI,
ModuleDescriptor), requirements, health and loader results.
Public API 1.0 (spec §2.2, Appendix C).
from sandloom.sdk.module 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 |
|---|---|---|
define_module |
function | Build a module's register(core) from its declarations (spec §5.7). |
Advanced¶
| Name | Kind | Summary |
|---|---|---|
AUTO |
constant | |
CapabilityRequirement |
class | An infrastructure capability a module needs to function. |
ContractRequirement |
class | A sync service contract a module CALLS (core.get_service(name)). |
FailedModule |
class | A module that failed to load, with its error and whether it was required. |
HealthCheck |
callable | |
HealthResult |
class | Outcome of a module health check: a status plus optional details. |
HealthStatus |
callable | |
MigrationTargetError |
exception | A module's migration target database cannot be determined unambiguously. |
ModuleAPI |
class | Stable, minimal registration surface for business modules. |
ModuleDescriptor |
class | What a module returns from register(core) to describe itself. |
ModuleLoadSummary |
class | Aggregate result of a load pass: loaded, skipped, failed, and duplicates. |
SkippedModule |
class | A module the loader skipped, with the reason why. |
Beginner names¶
define_module¶
def define_module(
package: str | None = None,
/,
*,
version: str = '0.1.0',
optional: bool = False,
consumes: Sequence[Any] = (),
capabilities: Sequence[Any] = (),
depends_on: Sequence[str] = (),
migrations_database: str | None = None,
migrations_chain: str | None = None,
health_check: Any | None = None,
setup: Callable[[ModuleAPI], None] | None = None,
name: str | None = None,
router: Any = 'auto',
events: Any = 'auto',
issues_tokens: bool = False,
) -> Callable[[ModuleAPI], Any]
Build a module's register(core) from its declarations (spec §5.7).
In the module's __init__.py:
from sandloom.sdk import define_module
register = define_module(__name__, version="0.1.0")
Everything the module declares in its conventional submodules is
discovered (routers, events, consumers, contracts, sagas, projections,
consumed contracts). setup(core) stays for what has no declaration
(startup tasks, claims contributors, planner operations). router= /
events= override discovery as today; consumes adds to the derived
requirements. Modules are REQUIRED unless optional=True.
Advanced names¶
AUTO¶
AUTO: str = 'auto'
(no docstring)
CapabilityRequirement¶
class CapabilityRequirement(
capability: str,
instance: str = 'default',
version: str = 'v1',
features: frozenset[str] = <factory>,
required: bool = True,
) -> None
An infrastructure capability a module needs to function.
Declared on the ModuleDescriptor so the loader can preflight — at
startup, before serving traffic — that the capability exists, has a
configured instance backed by a registered driver, and that the driver
supports every required feature. A required requirement that cannot be
satisfied fails the module load (and aborts startup if the module itself is
required); an optional one only logs a warning.
ContractRequirement¶
class ContractRequirement(
name: str,
required: bool = True,
reason: str = '',
methods: tuple[str, ...] = (),
) -> None
A sync service contract a module CALLS (core.get_service(name)).
Declared on ModuleDescriptor.consumes so the loader can check at
startup — not at the first request — that every consumed contract is
either provided by a module loaded in this process or bound remotely in
SERVICE_BINDINGS. A required contract that is neither aborts
startup (ContractBindingError); an optional one (the module degrades
explicitly without it) only logs a warning. Access facts owned elsewhere
are consumed through the owner's <owner>.facts.v1 contract:
ContractRequirement("billing.facts.v1").
FailedModule¶
class FailedModule(name: str, error: str, required: bool) -> None
A module that failed to load, with its error and whether it was required.
HealthCheck¶
def HealthCheck(*args, **kwargs)
(no docstring)
HealthResult¶
class HealthResult(
status: HealthStatus,
details: dict[str, object] | None = None,
) -> None
Outcome of a module health check: a status plus optional details.
HealthStatus¶
def HealthStatus(*args, **kwargs)
(no docstring)
MigrationTargetError¶
class MigrationTargetError(ValueError)
A module's migration target database cannot be determined unambiguously.
ModuleAPI¶
class ModuleAPI(
facade: CoreFacade,
*,
draft: ModuleRegistrationDraft | None = None,
) -> None
Stable, minimal registration surface for business modules.
When constructed with a draft (the loader's transactional path) write
methods buffer into it and are applied only on a successful commit; read
methods always reflect the live facade. Without a draft (direct use in
tests) writes apply to the facade immediately, preserving the simple API.
ModuleAPI.include_router¶
def include_router(self, router: APIRouter, **kwargs: Any) -> None
Mount a module's router (replaces reaching into core.app).
ModuleAPI.register_service¶
def register_service(self, name: str, impl: Any, *, database: str | None = None) -> None
Register a sync service contract provided by this module.
database overrides the database the contract runtime opens the
provider's Unit of Work on; by default it is the database the module's
descriptor declares (ModuleDescriptor.database_instances()).
ModuleAPI.get_service¶
def get_service(self, name: str) -> Any
ModuleAPI.register_event_schema¶
def register_event_schema(
self,
channel: str,
name: str,
schema: dict[str, Any] | None = None,
) -> Any
ModuleAPI.register_event_schema_obj¶
def register_event_schema_obj(self, event_schema: Any) -> Any
ModuleAPI.get_schema_registry¶
def get_schema_registry(self) -> Any
ModuleAPI.add_startup_task¶
def add_startup_task(self, task: StartupTask, *, critical: bool = False) -> None
ModuleAPI.add_shutdown_task¶
def add_shutdown_task(self, task: ShutdownTask) -> None
ModuleAPI.register_health_check¶
def register_health_check(self, name: str, fn: HealthCheck) -> None
ModuleAPI.add_claims_contributor¶
def add_claims_contributor(self, fn: Any, *, critical: bool = False) -> None
Register a contributor of JWT claims (async (user_id) -> dict).
The contributor runs in the module's OWN Unit of Work on its own
database (read it with contract_session); it is also exposed as
the <module>.claims.v1 contract so a token issuer in another process
can collect it over RPC (SERVICE_BINDINGS). critical=True
aborts token minting (503) if the contributor fails (use for security
claims like roles/permissions and tenant allowlists).
ModuleAPI.register_query_projection¶
def register_query_projection(self, descriptor: Any) -> None
Declare a denormalised cross-module read model this module owns.
ModuleAPI.register_projection_consumer¶
def register_projection_consumer(self, descriptor: Any) -> None
Declare a durable consumer that applies a source channel to a projection.
ModuleAPI.register_projection_rebuilder¶
def register_projection_rebuilder(self, projection: str, rebuilder: Any) -> None
Register the rebuilder that reconstructs projection from source.
ModuleAPI.register_event_consumer¶
def register_event_consumer(
self,
channel: str,
handler: Any,
*,
name: str,
durable: bool = True,
database: str = 'default',
required: bool = True,
group: str | None = None,
start_from: str = 'earliest',
bind_tenant_from: str | None = None,
dead_letter: bool = True,
) -> None
Subscribe handler to channel as a durable consumer.
The sanctioned "react to another module's event with a side effect"
path: at startup the framework subscribes the handler under its own
durable consumer group <module>:<name> (namespaced by the
registering module, so two modules — or this module deployed in two
services — never share a group and split its events) and runs each
delivery in a Unit of Work on database (commit on success,
rollback+redelivery on error). With durable (default) the event id
is claimed for exactly-once apply. required=True (default) gates
application readiness on the live subscription; mark deliberately
best-effort consumers optional.
group pins an explicit group id instead (e.g. the bare pre-0.4
name, to keep draining an existing group). start_from applies when
the group is first created: "earliest" (default; every event still
within EVENT_STREAM_RETENTION_SECONDS), "latest" or an ISO-8601
timestamp. bind_tenant_from (opt-in) names the payload field holding
the tenant id: the handler then runs with that tenant bound, so
TenantScoped code works; a missing/invalid value fails the delivery.
The handler signature is async (session, envelope) -> None.
dead_letter=False retries a failing delivery FOREVER (capped
backoff) instead of dead-lettering it after the delivery budget — for
effects that must never be silently parked; the handler raises
PoisonEventError (sandloom.core.di) for an event that can never
apply, which is dead-lettered at once with a CRITICAL log.
ModuleAPI.register_saga¶
def register_saga(self, definition: Any) -> None
Register a saga this module owns (sandloom.core.module_api.Saga).
Binds it to the module (saga_type = "<module>.<name>") and adds a
ContractRequirement(name, methods=...) for every contract its
steps call to the module's consumes — a missing provider is then a
ContractBindingError at startup. The kernel checks the calls
against the providers' describe documents once every module loaded
and installs the advance consumer <module>:sagas@advance and the
timer sweep.
ModuleAPI.register_access_fact¶
def register_access_fact(self, fact: Any) -> None
Register an sandloom.shared.authz.facts.AccessFact.
Binds a fact name to its claim/projection/live resolution paths so
Entitled(name) policies resolve it by configuration.
ModuleAPI.register_operations¶
def register_operations(self, *operations: Any) -> None
Declare this module's planner operations (ReadOperation /
JoinRead): each is planned at startup (a critical startup task,
so a declaration that does not match the schema fails the boot), the
chosen executor and the reason are logged, and /diagnostics/core
lists them (planner).
ModuleAPI.logger_factory¶
def logger_factory(self, name: str) -> logging.Logger
ModuleAPI.logger¶
logger (property)
ModuleAPI.settings¶
settings (property)
ModuleDescriptor¶
class ModuleDescriptor(
name: str,
version: str,
optional: bool = False,
routes: APIRouter | None = None,
health_check: HealthCheck | None = None,
migrations_path: str | None = None,
depends_on: list[str] = <factory>,
capabilities: list[CapabilityRequirement] = <factory>,
migrations_database: str | None = None,
migrations_chain: str | None = None,
consumes: list[ContractRequirement] = <factory>,
issues_tokens: bool = False,
) -> None
What a module returns from register(core) to describe itself.
Carries the module's identity (name/version), whether it is
optional (modules are required by default: a load failure aborts
startup unless the module says optional=True), its routes, an
optional health_check, its migrations_path and depends_on
ordering, and the capabilities it needs. Reference other modules'
entities by plain UUID — never declare cross-module foreign keys.
ModuleDescriptor.effective_capabilities¶
def effective_capabilities(self) -> list[CapabilityRequirement]
Capabilities to preflight at load (the declared list).
ModuleDescriptor.migration_chain_key¶
def migration_chain_key(self) -> str
Key of this module's migration chain (version-table suffix).
ModuleDescriptor.database_instances¶
def database_instances(self) -> set[str]
The database instance this module's tables live in (one element).
Resolution:
migrations_databasewhen set (explicit, always wins);- otherwise the instances of the REQUIRED
databaserequirements: none ->default; exactly one -> that one; several ->defaultif it is among them (a module that readsdefaultand also usesanalyticskeeps its tables indefault), else ambiguous ->MigrationTargetError(declaremigrations_database).
Optional (required=False) database requirements never host the
chain, so an optional, unconfigured instance cannot break
upgrade-all. A module's chain is applied to exactly ONE database —
applying it to every declared instance duplicated its tables.
ModuleLoadSummary¶
class ModuleLoadSummary(
loaded: list[ModuleDescriptor] = <factory>,
skipped: list[SkippedModule] = <factory>,
failed: list[FailedModule] = <factory>,
duplicates: list[str] = <factory>,
explicit: list[str] = <factory>,
consumes: dict[str, list[dict[str, Any]]] = <factory>,
manifests: dict[str, Any] = <factory>,
) -> None
Aggregate result of a load pass: loaded, skipped, failed, and duplicates.
ModuleLoadSummary.success¶
def success(self) -> bool
ModuleLoadSummary.to_dict¶
def to_dict(self) -> dict[str, Any]
SkippedModule¶
class SkippedModule(name: str, reason: str) -> None
A module the loader skipped, with the reason why.