TenancyManager¶
TenancyManager ¶
TenancyManager(
config: TenancyConfig,
store: TenantStore[Tenant],
custom_resolver: TenantResolver | None = None,
isolation_provider: BaseIsolationProvider | None = None,
audit_writer: Any | None = None,
)
Central orchestrator wiring resolver, store, and isolation provider.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
TenancyConfig
|
Application-wide tenancy configuration. |
required |
store
|
TenantStore[Tenant]
|
Tenant metadata storage backend. |
required |
custom_resolver
|
TenantResolver | None
|
Optional custom resolver (required when
|
None
|
isolation_provider
|
BaseIsolationProvider | None
|
Optional pre-built isolation provider. When
|
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
config |
The |
|
store |
The underlying |
|
resolver |
TenantResolver
|
The active |
isolation_provider |
BaseIsolationProvider
|
The active |
Source code in src/fastapi_tenancy/manager.py
initialize
async
¶
Initialise all components.
Call this once at application startup — typically inside a FastAPI lifespan context manager. It:
- Initialises the store (creates tables if applicable).
- Warms the Redis cache when configured.
- Starts the L1 cache background purge task (when cache is enabled).
- Establishes the Redis rate-limiter connection (when rate limiting is enabled).
Safe to call multiple times — all operations are idempotent. A second call while the purge task is already running will not start a duplicate.
Source code in src/fastapi_tenancy/manager.py
close
async
¶
Dispose all resources.
Call this inside a FastAPI lifespan finally block or on SIGTERM.
Disposes engine pools, closes Redis connections, and cancels the
background L1 cache purge task.
Both isolation_provider.close() and store.close() are called
unconditionally — TenantStore now declares a concrete no-op
close() that subclasses override when they hold external resources,
so the old hasattr guard is no longer necessary.
Source code in src/fastapi_tenancy/manager.py
create_lifespan ¶
Return an async context manager suitable for FastAPI's lifespan parameter.
Example::
app = FastAPI(lifespan=manager.create_lifespan())
Returns:
| Type | Description |
|---|---|
Any
|
An async context manager that calls |
Any
|
and |
Source code in src/fastapi_tenancy/manager.py
register_tenant
async
¶
register_tenant(
identifier: str,
name: str,
metadata: dict[str, Any] | None = None,
isolation_strategy: IsolationStrategy | None = None,
app_metadata: MetaData | None = None,
) -> Tenant
Register a new tenant and provision its database namespace.
This is the recommended way to onboard tenants programmatically. It:
- Validates the identifier.
- Generates a cryptographically secure
id. - Persists the tenant in the store.
- Calls
isolation_provider.initialize_tenant()to create the schema/database.
.. note:: This does not auto-seed demo tenants. Call this explicitly from your onboarding flow or admin CLI.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
identifier
|
str
|
Human-readable slug (must pass
:func: |
required |
name
|
str
|
Display name. |
required |
metadata
|
dict[str, Any] | None
|
Optional initial metadata dict. |
None
|
isolation_strategy
|
IsolationStrategy | None
|
Per-tenant isolation override. |
None
|
app_metadata
|
MetaData | None
|
SQLAlchemy |
None
|
Returns:
| Type | Description |
|---|---|
Tenant
|
The newly created, stored :class: |
Raises:
| Type | Description |
|---|---|
ValueError
|
When identifier is invalid or already taken. |
TenancyError
|
When store or isolation provider raises. |
Source code in src/fastapi_tenancy/manager.py
| Python | |
|---|---|
525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 | |
suspend_tenant
async
¶
suspend_tenant(tenant_id: str) -> Tenant
Suspend a tenant, blocking all future requests.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id
|
str
|
ID of the tenant to suspend. |
required |
Returns:
| Type | Description |
|---|---|
Tenant
|
The updated tenant with |
Raises:
| Type | Description |
|---|---|
TenantNotFoundError
|
When tenant_id does not exist. |
Source code in src/fastapi_tenancy/manager.py
activate_tenant
async
¶
activate_tenant(tenant_id: str) -> Tenant
Reinstate a suspended tenant.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id
|
str
|
ID of the tenant to activate. |
required |
Returns:
| Type | Description |
|---|---|
Tenant
|
The updated tenant with |
Raises:
| Type | Description |
|---|---|
TenantNotFoundError
|
When tenant_id does not exist. |
Source code in src/fastapi_tenancy/manager.py
delete_tenant
async
¶
delete_tenant(
tenant_id: str,
destroy_data: bool = False,
app_metadata: MetaData | None = None,
) -> None
Delete a tenant, optionally destroying all associated data.
When destroy_data=True and config.enable_soft_delete=False,
the tenant's isolation namespace (schema/database/rows) is permanently
removed. This is irreversible.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id
|
str
|
ID of the tenant to delete. |
required |
destroy_data
|
bool
|
When |
False
|
app_metadata
|
MetaData | None
|
SQLAlchemy |
None
|
Raises:
| Type | Description |
|---|---|
TenantNotFoundError
|
When tenant_id does not exist. |
TenancyError
|
When data destruction fails. |
Source code in src/fastapi_tenancy/manager.py
check_rate_limit
async
¶
check_rate_limit(tenant: Tenant) -> None
Check and atomically increment the sliding-window rate limit for tenant.
Uses a single Lua script executed server-side by Redis, replacing the
previous two-pipeline approach that had an off-by-one race condition:
concurrent requests at the exact boundary could all read count =
limit - 1, all pass the check, and then all add their timestamps,
silently breaching the limit.
The Lua script is atomic — Redis executes it without interleaving any other commands — so the check-and-increment is always consistent.
Each call generates a unique member string ("{now}:{uuid4}") so
that two requests arriving within the same microsecond each add a
distinct sorted-set entry rather than overwriting each other.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant
|
Tenant
|
The tenant whose rate limit to check. |
required |
Raises:
| Type | Description |
|---|---|
RateLimitExceededError
|
When the tenant has exceeded its limit. |
Source code in src/fastapi_tenancy/manager.py
write_audit_log
async
¶
write_audit_log(entry: AuditLog) -> None
Persist an audit log entry via the configured AuditLogWriter.
Delegates to the AuditLogWriter supplied at construction time.
The default writer logs the entry at INFO level. Supply a custom
writer to persist to a database, message queue, or external service::
class CloudWatchAuditWriter:
async def write(self, entry: AuditLog) -> None:
await cw.put_log_events(...)
manager = TenancyManager(config, store, audit_writer=CloudWatchAuditWriter())
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entry
|
AuditLog
|
The audit log entry to persist. |
required |