API Reference
This page is autogenerated from the source code's docstrings. Every public
function, class, and constant in tenantshield.* is documented here.
tenantshield.context
tenantshield.context
Synchronous tenant context management for TenantShield.
This module defines :class:TenantContext and the free functions to activate,
inspect, and construct contexts. The active context is stored in a module-level
:class:contextvars.ContextVar, which makes it inherently thread- and
asyncio-task-isolated without monkey-patching.
The asynchronous counterpart (atenant_scope) is added to this module in
sub-phase 1A.4.
TenantId
module-attribute
TenantId = NewType('TenantId', str)
Identifier for a tenant.
A NewType over str. At runtime it is exactly str; the wrapper
exists only to give type checkers a way to distinguish tenant identifiers
from arbitrary strings. Construct with TenantId("acme") (or
TenantId(str(value)) when coercing from non-string sources).
Equality is by string value. No normalization (case folding, trimming) is applied; callers are responsible for normalizing at the system boundary.
TenantContext
dataclass
A frozen, slotted tenant context.
Attributes:
| Name | Type | Description |
|---|---|---|
tenant_id |
TenantId
|
The tenant identifier. |
metadata |
Mapping[str, object]
|
Arbitrary metadata associated with this context. Defaults to
an empty mapping. Although the field is typed as |
tenant_scope
tenant_scope(
ctx: TenantContext,
) -> Generator[TenantContext, None, None]
Activate ctx as the current tenant for the duration of the block.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ctx
|
TenantContext
|
The tenant context to activate. |
required |
Yields:
| Type | Description |
|---|---|
TenantContext
|
The same context, for ergonomic |
Example
with tenant_scope(bind_tenant(TenantId("acme"))) as ctx: ... assert current_tenant() is ctx
current_tenant
current_tenant() -> TenantContext
Return the active tenant context.
Raises:
| Type | Description |
|---|---|
MissingTenantContextError
|
if no tenant scope is active. |
try_current_tenant
try_current_tenant() -> TenantContext | None
Return the active tenant context, or None if none is active.
bind_tenant
bind_tenant(
tenant_id: TenantId, /, **metadata: object
) -> TenantContext
Construct a :class:TenantContext without activating it.
The / makes tenant_id positional-only — callers must write
bind_tenant(TenantId("acme"), region="eu") and not
bind_tenant(tenant_id=TenantId("acme"), region="eu"). This forces
visual clarity at call sites.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id
|
TenantId
|
The tenant identifier (positional-only). |
required |
**metadata
|
object
|
Arbitrary metadata keyword arguments attached to the context. |
{}
|
Returns:
| Type | Description |
|---|---|
TenantContext
|
A new |
Example
ctx = bind_tenant(TenantId("acme"), region="eu") with tenant_scope(ctx): ... pass
atenant_scope
async
atenant_scope(
ctx: TenantContext,
) -> AsyncGenerator[TenantContext, None]
Activate ctx as the current tenant for the duration of the async block.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ctx
|
TenantContext
|
The tenant context to activate. |
required |
Yields:
| Type | Description |
|---|---|
AsyncGenerator[TenantContext, None]
|
The same context, for ergonomic |
Example
async with atenant_scope(bind_tenant(TenantId("acme"))) as ctx: ... assert current_tenant() is ctx
tenantshield.exceptions
tenantshield.exceptions
Exception hierarchy for TenantShield.
All errors raised by TenantShield are subclasses of :class:TenantShieldError.
The hierarchy is::
TenantShieldError
├── ConfigurationError
├── TenantContextError
│ ├── MissingTenantContextError
│ └── AmbiguousTenantContextError
├── EnforcementError
│ ├── CrossTenantAccessError
│ ├── UnscopedQueryError
│ └── CrossTenantJoinError
└── AdapterError
Errors with structured fields carry contextual information (tenant ids, model
names, operation labels). They expose a :meth:to_dict method for serialization
to audit sinks.
TenantShieldError
Bases: Exception
Base class for all TenantShield errors.
ConfigurationError
Bases: TenantShieldError
Raised when TenantShield is misconfigured or used in an unsupported way.
TenantContextError
Bases: TenantShieldError
Base class for tenant context problems.
EnforcementError
Bases: TenantShieldError
Base class for tenant isolation policy violations.
AdapterError
Bases: TenantShieldError
Base class for adapter-specific failures.
MissingTenantContextError
dataclass
Bases: TenantContextError
Raised when an operation requires a tenant scope and none is active.
The auto-generated message includes a canonical-pattern hint pointing callers at the documented entry-point shapes (raw API + Django shortcut) per Finding #2 (Counterbook ADR-0015 catalog).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
operation
|
str
|
A short label identifying the API or operation that needed
the tenant context (e.g. |
required |
stack_context
|
Mapping[str, object]
|
Optional dictionary with additional debugging context. |
_empty_metadata()
|
AmbiguousTenantContextError
dataclass
Bases: TenantContextError
Raised when nested tenant scopes carry conflicting tenant identifiers.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id_outer
|
TenantId
|
Tenant active in the surrounding scope. |
required |
tenant_id_inner
|
TenantId
|
Tenant the inner scope attempted to activate. |
required |
stack_context
|
Mapping[str, object]
|
Optional dictionary with additional debugging context. |
_empty_metadata()
|
CrossTenantAccessError
dataclass
Bases: EnforcementError
Raised when an access crosses tenant boundaries.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id_expected
|
TenantId | None
|
The tenant that should have scoped the access. |
required |
tenant_id_actual
|
TenantId | None
|
The tenant actually carried by the data. |
required |
model
|
str | None
|
Name of the model or table involved. |
required |
operation
|
str
|
Short label identifying the operation (e.g. |
required |
stack_context
|
Mapping[str, object]
|
Optional dictionary with additional debugging context. |
_empty_metadata()
|
UnscopedQueryError
dataclass
Bases: EnforcementError
Raised when a query touches a tenant-aware model without an active scope.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
str | None
|
Name of the tenant-aware model that was queried. |
required |
operation
|
str
|
Short label identifying the operation (e.g. |
required |
stack_context
|
Mapping[str, object]
|
Optional dictionary with additional debugging context. |
_empty_metadata()
|
CrossTenantJoinError
dataclass
Bases: EnforcementError
Raised when a query joins models that belong to different tenants.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id_expected
|
TenantId | None
|
The tenant under which the join was attempted. |
required |
model_left
|
str
|
Name of the left side of the join. |
required |
model_right
|
str
|
Name of the right side of the join. |
required |
stack_context
|
Mapping[str, object]
|
Optional dictionary with additional debugging context. |
_empty_metadata()
|
tenantshield.policies
tenantshield.policies
Tenant enforcement policies for TenantShield.
This module defines the :class:Policy protocol, the three :class:Decision
variants (Allow, Deny, RequireScope), and three built-in policies
(DenyByDefaultPolicy, AllowListPolicy, ChainPolicy). The
:func:evaluate_and_audit helper composes a policy evaluation with an audit
emission.
Policies are pure — they do not perform I/O or emit events by themselves.
The audit emission is the responsibility of :func:evaluate_and_audit or
of adapters in Phase 2+.
OperationType
Bases: StrEnum
Kind of operation being evaluated by a policy.
Inherits from str so it serializes cleanly to JSON.
Operation
dataclass
Describes an operation to be evaluated by a policy.
Attributes:
| Name | Type | Description |
|---|---|---|
model |
str
|
Fully-qualified model name (e.g. |
operation_type |
OperationType
|
Kind of operation. |
tenant_context |
TenantContext | None
|
The tenant context active during evaluation, or
|
extras |
Mapping[str, object]
|
Arbitrary additional data for custom policies. |
Allow
dataclass
Decision: allow the operation.
Note: slots=True is intentionally omitted because frozen=True +
slots=True on a dataclass with no fields triggers a CPython bug
(TypeError instead of FrozenInstanceError on setattr). Slots add no
benefit to an empty class, so we keep this class non-slotted while
Deny and RequireScope (which have fields) remain slotted.
Deny
dataclass
Decision: deny the operation.
Attributes:
| Name | Type | Description |
|---|---|---|
reason |
str
|
Human-readable reason. Surfaces in audit logs. |
RequireScope
dataclass
Decision: allow only if scoped by the given filter spec.
Attributes:
| Name | Type | Description |
|---|---|---|
filter_spec |
Mapping[str, object]
|
Mapping of constraints. In Sub-phase 1B this is a free-form dict; adapters in Phase 2+ may impose structure. |
Policy
Bases: Protocol
Contract for tenant enforcement policies.
Implementations should be stateless or stateful but thread-safe. Policies must not perform I/O — they receive the operation and return a decision synchronously.
DenyByDefaultPolicy
dataclass
A policy that denies any operation without an active tenant context.
If a tenant context IS active, the operation is allowed. This is the minimal enforcement: prevent operations from running in the absence of explicit tenancy.
AllowListPolicy
dataclass
A policy that allows operations only on a fixed set of models.
Operations on models outside the allowlist are denied regardless of
tenant context. Operations on allowlisted models still require tenant
context (delegate that to DenyByDefaultPolicy via composition).
Attributes:
| Name | Type | Description |
|---|---|---|
allowed_models |
frozenset[str]
|
Frozen set of fully-qualified model names. |
ChainPolicy
dataclass
Compose multiple policies; first non-Allow decision wins.
Iterates the policies in order. If any returns Deny or
RequireScope, the chain returns that decision (subsequent policies
are not evaluated). Only if all policies return Allow does the chain
return Allow.
Attributes:
| Name | Type | Description |
|---|---|---|
policies |
tuple[Policy, ...]
|
Tuple of policies to apply in order. |
evaluate_and_audit
evaluate_and_audit(
policy: Policy, operation: Operation
) -> Decision
Evaluate operation against policy and emit an audit event.
Emits POLICY_ALLOW or POLICY_DENY based on the decision.
RequireScope decisions emit POLICY_ALLOW (the scope is
informational, not a denial).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
policy
|
Policy
|
The policy to evaluate. |
required |
operation
|
Operation
|
The operation to evaluate. |
required |
Returns:
| Type | Description |
|---|---|
Decision
|
The decision returned by the policy. |
tenantshield.audit
tenantshield.audit
Audit event bus for TenantShield.
This module defines the categorical event types, the immutable
:class:AuditEvent record, and the :class:AuditSink protocol. The
registry, the emit() dispatcher, and the built-in sinks are added in
subsequent sub-tasks of Sub-phase 1B.
Subscribers ("sinks") consume :class:AuditEvent instances. Sinks must
be thread- and async-safe and should not raise. The bus catches sink
exceptions and emits SINK_FAILURE events to the remaining sinks.
AuditEventType
Bases: StrEnum
Categorical types of audit events.
Inherits from str so events serialize cleanly to JSON without a
custom encoder.
AuditEvent
dataclass
A single event emitted to the audit bus.
Attributes:
| Name | Type | Description |
|---|---|---|
event_type |
AuditEventType
|
Categorical type of the event. |
tenant_context |
TenantContext | None
|
The tenant context active when the event occurred,
or |
payload |
Mapping[str, object]
|
Arbitrary structured data. Treated as immutable from outside the package; mutating it does not break TenantShield invariants but is bad style. |
timestamp |
datetime
|
UTC timestamp at construction time. Generated by
|
AuditSink
Bases: Protocol
Contract for audit event consumers.
Implementations must be thread- and async-safe. Implementations should
not raise exceptions; if they do, the bus catches them and emits a
SINK_FAILURE event to the remaining sinks. Repeated failures from
the same sink do not cause infinite recursion: the failure event is
dispatched to sinks OTHER than the one that failed.
NullSink
A sink that discards all events. Useful for tests and as default.
InMemorySink
A sink that accumulates events in memory.
Not thread-safe by design for performance — intended for tests where the test controls concurrency. If you need thread-safety in production code, build a different sink.
Attributes:
| Name | Type | Description |
|---|---|---|
events |
list[AuditEvent]
|
List of emitted events, in order. |
clear
clear() -> None
Remove all accumulated events.
StructLogSink
An audit sink that forwards events to a structlog BoundLogger.
The sink does NOT configure structlog globally. The caller is responsible
for the structlog processor chain. If no logger is provided, the sink
uses structlog.get_logger("tenantshield.audit") which inherits the
caller's global structlog configuration.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
logger
|
BoundLogger | None
|
An optional pre-configured structlog logger. If |
None
|
Example
import structlog from tenantshield.audit import StructLogSink, register_sink logger = structlog.get_logger("my_app").bind(component="tenancy") register_sink(StructLogSink(logger=logger))
register_sink
register_sink(sink: AuditSink) -> None
Register a sink to receive audit events.
Idempotent: registering the same sink twice (verified by identity) has no effect on the second call.
unregister_sink
unregister_sink(sink: AuditSink) -> None
Unregister a sink. If not registered, this is a no-op.
emit
emit(event: AuditEvent) -> None
Dispatch an event to all registered sinks.
Sinks that raise exceptions do not interrupt other sinks. For each
failing sink, a SINK_FAILURE event is emitted to all OTHER
registered sinks (preventing infinite recursion). A second-level
failure (the SINK_FAILURE itself failing) is silently suppressed.
tenantshield.registry
tenantshield.registry
Tenant-aware model registry.
This module defines :class:RegistryEntry (metadata about a tenant-aware
model) and :class:ModelRegistry (a thread-safe container of entries).
The :data:default_registry instance and the module-level convenience
functions are added in sub-task 1C.2.
RegistryEntry
dataclass
Metadata about a tenant-aware model.
Attributes:
| Name | Type | Description |
|---|---|---|
model |
type
|
The model class. |
tenant_field |
str
|
Name of the attribute/column that carries the tenant id. |
ModelRegistry
Registry of tenant-aware models.
A registry maps model classes to their tenant metadata. Two registries
are independent; users who need isolation construct their own instance.
The package exposes a default_registry instance for the common case,
and module-level convenience functions delegate to it (added in 1C.2).
Thread-safety: register/unregister/iteration operations are protected
by an internal RLock. Read-only queries (is_registered, get)
are also locked for consistency; the performance impact is negligible at
registry sizes typical of real applications (hundreds of models).
register
register(
model: type, *, tenant_field: str = "tenant_id"
) -> None
Register model as tenant-aware.
Idempotent: registering the same model twice with the same
tenant_field is a no-op. Registering with a different
tenant_field raises :class:ConfigurationError.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
type
|
The model class to register. |
required |
tenant_field
|
str
|
Name of the attribute/column carrying the tenant id. |
'tenant_id'
|
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
if |
unregister
unregister(model: type) -> None
Unregister model. If not registered, this is a no-op.
is_registered
is_registered(model: type) -> bool
Return True if model is registered as tenant-aware.
get
get(model: type) -> RegistryEntry
Return the :class:RegistryEntry for model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
type
|
The model class to look up. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
The |
RegistryEntry
|
class: |
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
if |
clear
clear() -> None
Remove all registered models. Primarily for tests.
__iter__
__iter__() -> Iterator[RegistryEntry]
Iterate over registered entries.
Iteration takes a snapshot under lock; concurrent modifications during iteration do not affect the snapshot.
register_model
register_model(
model: type | None = None,
*,
tenant_field: str = "tenant_id",
) -> type | Callable[[type], type]
Register a model as tenant-aware in the :data:default_registry.
Can be used as a decorator (with or without arguments) or called directly with the model class.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
type | None
|
The model class to register. If |
None
|
tenant_field
|
str
|
Name of the attribute/column carrying the tenant id. |
'tenant_id'
|
Returns:
| Type | Description |
|---|---|
type | Callable[[type], type]
|
The model class (when called with a model), or a decorator function |
type | Callable[[type], type]
|
(when called without a model, e.g. |
Examples:
>>> @register_model
... class Invoice:
... tenant_id: str
...
>>> @register_model(tenant_field="org_id")
... class Org:
... org_id: str
...
>>> register_model(LegacyModel, tenant_field="account_id")
is_tenant_aware
is_tenant_aware(model: type) -> bool
Return True if model is registered as tenant-aware in the default registry.
get_tenant_field
get_tenant_field(model: type) -> str
Return the tenant field name for model from the default registry.
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
if |