Tenant Stores¶
Abstract base¶
TenantStore ¶
Bases: ABC, Generic[TenantT]
Abstract base class for tenant metadata storage backends.
Implementations must be:
- Fully async — every method is a coroutine.
- Concurrency-safe — instances are created once at startup and shared across all requests; no mutable instance state after init.
- Raise on not-found — methods that look up a single tenant must
raise
TenantNotFoundError, never returnNone. - Wrap unexpected errors — unexpected storage errors should be wrapped
in
TenancyErrorwith adetailsdict safe to log.
Type parameter
TenantT: The concrete tenant domain model (defaults to Tenant).
get_by_id
abstractmethod
async
¶
Fetch a tenant by its opaque unique ID.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id
|
str
|
Opaque tenant primary key. |
required |
Returns:
| Type | Description |
|---|---|
TenantT
|
The matching |
Raises:
| Type | Description |
|---|---|
TenantNotFoundError
|
When no tenant with tenant_id exists. |
Source code in src/fastapi_tenancy/storage/tenant_store.py
| Python | |
|---|---|
get_by_identifier
abstractmethod
async
¶
Fetch a tenant by its human-readable slug identifier.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
identifier
|
str
|
The tenant slug (e.g. |
required |
Returns:
| Type | Description |
|---|---|
TenantT
|
The matching |
Raises:
| Type | Description |
|---|---|
TenantNotFoundError
|
When no tenant with identifier exists. |
Source code in src/fastapi_tenancy/storage/tenant_store.py
create
abstractmethod
async
¶
Persist a new tenant record.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant
|
TenantT
|
A fully-populated |
required |
Returns:
| Type | Description |
|---|---|
TenantT
|
The stored tenant (may include server-generated timestamps). |
Raises:
| Type | Description |
|---|---|
ValueError
|
When the |
TenancyError
|
On unexpected storage failure. |
Source code in src/fastapi_tenancy/storage/tenant_store.py
update
abstractmethod
async
¶
Replace all mutable fields of an existing tenant.
Because Tenant is immutable, callers must first build a modified
copy with model_copy::
updated = tenant.model_copy(update={"name": "New Name"})
result = await store.update(updated)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant
|
TenantT
|
Modified tenant object. |
required |
Returns:
| Type | Description |
|---|---|
TenantT
|
The updated tenant as stored (timestamps refreshed). |
Raises:
| Type | Description |
|---|---|
TenantNotFoundError
|
When |
TenancyError
|
On unexpected storage failure. |
Source code in src/fastapi_tenancy/storage/tenant_store.py
delete
abstractmethod
async
¶
Remove a tenant from the store.
Behaviour depends on TenancyConfig.enable_soft_delete:
implementations should mark the tenant as DELETED rather than
removing the row when soft-delete is enabled.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id
|
str
|
ID of the tenant to delete. |
required |
Raises:
| Type | Description |
|---|---|
TenantNotFoundError
|
When tenant_id does not exist. |
TenancyError
|
On unexpected storage failure. |
Source code in src/fastapi_tenancy/storage/tenant_store.py
list
abstractmethod
async
¶
list(
skip: int = 0,
limit: int = 100,
status: TenantStatus | None = None,
) -> Sequence[TenantT]
Return a page of tenants, ordered by creation date (newest first).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skip
|
int
|
Number of records to skip (offset-based pagination). |
0
|
limit
|
int
|
Maximum number of records to return. |
100
|
status
|
TenantStatus | None
|
When provided, only tenants with this status are returned. |
None
|
Returns:
| Type | Description |
|---|---|
Sequence[TenantT]
|
A sequence of |
Source code in src/fastapi_tenancy/storage/tenant_store.py
count
abstractmethod
async
¶
count(status: TenantStatus | None = None) -> int
Return the total number of tenants, optionally filtered by status.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
status
|
TenantStatus | None
|
When provided, count only tenants with this status. |
None
|
Returns:
| Type | Description |
|---|---|
int
|
Non-negative integer count. |
Source code in src/fastapi_tenancy/storage/tenant_store.py
| Python | |
|---|---|
exists
abstractmethod
async
¶
Return True if a tenant with tenant_id exists.
More efficient than get_by_id when only existence is needed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id
|
str
|
Opaque tenant primary key. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
Source code in src/fastapi_tenancy/storage/tenant_store.py
| Python | |
|---|---|
set_status
abstractmethod
async
¶
set_status(tenant_id: str, status: TenantStatus) -> TenantT
Change the lifecycle status of a tenant.
This is the preferred method for status transitions (activate, suspend, etc.) because it only touches the status field.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id
|
str
|
ID of the tenant to update. |
required |
status
|
TenantStatus
|
The new |
required |
Returns:
| Type | Description |
|---|---|
TenantT
|
The updated tenant. |
Raises:
| Type | Description |
|---|---|
TenantNotFoundError
|
When tenant_id does not exist. |
Source code in src/fastapi_tenancy/storage/tenant_store.py
update_metadata
abstractmethod
async
¶
Atomically merge metadata into the tenant's metadata dictionary.
The merge is shallow: top-level keys in metadata overwrite existing keys; keys absent from metadata are preserved.
Implementations should perform this as an atomic operation at the
database level (e.g. PostgreSQL jsonb || operator) to avoid the
read-modify-write race that a naïve in-application merge introduces.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id
|
str
|
ID of the tenant to update. |
required |
metadata
|
dict[str, Any]
|
Key-value pairs to merge. |
required |
Returns:
| Type | Description |
|---|---|
TenantT
|
The updated tenant. |
Raises:
| Type | Description |
|---|---|
TenantNotFoundError
|
When tenant_id does not exist. |
Source code in src/fastapi_tenancy/storage/tenant_store.py
close
async
¶
Release any resources held by this store (connections, pools, etc.).
The base implementation is a no-op — subclasses that hold external resources (database engines, Redis connections) must override this method and dispose them properly.
Called automatically by TenancyManager.close() on application
shutdown. If your store holds connection pools, failing to override
this will leak file descriptors and connections on process exit.
Example override::
async def close(self) -> None:
await self._engine.dispose()
Source code in src/fastapi_tenancy/storage/tenant_store.py
get_by_ids
async
¶
Fetch multiple tenants by their IDs in one logical call.
The base implementation issues one get_by_id call per ID.
Override this for production backends to issue a single query.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_ids
|
Iterable[str]
|
Iterable of opaque tenant IDs. |
required |
Returns:
| Type | Description |
|---|---|
Sequence[TenantT]
|
Tenants that were found; IDs with no matching tenant are silently |
Sequence[TenantT]
|
skipped. |
Source code in src/fastapi_tenancy/storage/tenant_store.py
search
async
¶
Search for tenants whose name or identifier contains query.
The base implementation loads up to _scan_limit records and filters
them in Python. Override for production backends with a
database-level ILIKE/LIKE query.
Result ordering is backend-defined. Concrete implementations may
apply relevance ranking (e.g. InMemoryTenantStore sorts by exact
match → prefix match → substring match), or may return results in
arbitrary order (e.g. the SQLAlchemyTenantStore override sorts
alphabetically by identifier). Do not rely on a specific ordering
unless the concrete implementation documents it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str
|
Case-insensitive substring to search for. |
required |
limit
|
int
|
Maximum number of results to return. |
10
|
_scan_limit
|
int
|
Maximum records fetched for in-memory filtering. |
100
|
Returns:
| Type | Description |
|---|---|
Sequence[TenantT]
|
Matching tenants, up to limit results. |
Warning
The base implementation is O(n) in the number of tenants. For deployments with thousands of tenants, override with a DB-level search in the concrete subclass.
Source code in src/fastapi_tenancy/storage/tenant_store.py
bulk_update_status
async
¶
bulk_update_status(
tenant_ids: Iterable[str], status: TenantStatus
) -> Sequence[TenantT]
Update the status of multiple tenants in one logical operation.
The base implementation calls set_status once per ID.
Override for production backends to issue a single SQL UPDATE
with an IN clause.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_ids
|
Iterable[str]
|
IDs of the tenants to update. |
required |
status
|
TenantStatus
|
New |
required |
Returns:
| Type | Description |
|---|---|
Sequence[TenantT]
|
Updated tenants; IDs with no matching tenant are silently skipped. |
Source code in src/fastapi_tenancy/storage/tenant_store.py
SQLAlchemy¶
SQLAlchemyTenantStore ¶
SQLAlchemyTenantStore(
database_url: str,
pool_size: int = 10,
max_overflow: int = 20,
pool_pre_ping: bool = True,
pool_recycle: int = 3600,
echo: bool = False,
)
Bases: TenantStore[Tenant]
Async SQLAlchemy-backed tenant store compatible with all major databases.
This is the recommended production store for fastapi-tenancy.
Lifecycle::
store = SQLAlchemyTenantStore(
database_url="postgresql+asyncpg://user:pass@localhost/myapp",
)
await store.initialize() # create table if not exists
# ... serve requests ...
await store.close() # dispose pool on shutdown
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
database_url
|
str
|
Async SQLAlchemy connection URL. |
required |
pool_size
|
int
|
Number of persistent connections in the pool. |
10
|
max_overflow
|
int
|
Extra connections allowed under burst load. |
20
|
pool_pre_ping
|
bool
|
Verify connections before checkout (recommended). |
True
|
pool_recycle
|
int
|
Seconds after which a connection is proactively replaced. |
3600
|
echo
|
bool
|
Log every SQL statement (development only). |
False
|
Source code in src/fastapi_tenancy/storage/database.py
initialize
async
¶
Create the tenants table if it does not already exist (idempotent).
Source code in src/fastapi_tenancy/storage/database.py
close
async
¶
get_by_id
async
¶
get_by_id(tenant_id: str) -> Tenant
Fetch a tenant by its opaque unique ID.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id
|
str
|
Opaque tenant primary key. |
required |
Returns:
| Type | Description |
|---|---|
Tenant
|
The matching |
Raises:
| Type | Description |
|---|---|
TenantNotFoundError
|
When no tenant with tenant_id exists. |
Source code in src/fastapi_tenancy/storage/database.py
get_by_identifier
async
¶
get_by_identifier(identifier: str) -> Tenant
Fetch a tenant by its human-readable slug identifier.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
identifier
|
str
|
The tenant slug (e.g. |
required |
Returns:
| Type | Description |
|---|---|
Tenant
|
The matching |
Raises:
| Type | Description |
|---|---|
TenantNotFoundError
|
When no tenant with identifier exists. |
Source code in src/fastapi_tenancy/storage/database.py
list
async
¶
list(
skip: int = 0,
limit: int = 100,
status: TenantStatus | None = None,
) -> list[Tenant]
Return a page of tenants ordered by creation date (newest first).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skip
|
int
|
Number of records to skip (offset-based pagination). |
0
|
limit
|
int
|
Maximum number of records to return. |
100
|
status
|
TenantStatus | None
|
Optional status filter. |
None
|
Returns:
| Type | Description |
|---|---|
list[Tenant]
|
List of |
Source code in src/fastapi_tenancy/storage/database.py
count
async
¶
count(status: TenantStatus | None = None) -> int
Return the total number of tenants, optionally filtered by status.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
status
|
TenantStatus | None
|
Optional status filter. |
None
|
Returns:
| Type | Description |
|---|---|
int
|
Non-negative integer count. |
Source code in src/fastapi_tenancy/storage/database.py
exists
async
¶
Return True when a tenant with tenant_id exists.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id
|
str
|
Opaque tenant primary key. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
Existence flag. |
Source code in src/fastapi_tenancy/storage/database.py
create
async
¶
Persist a new tenant record.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant
|
Tenant
|
Fully-populated tenant object. |
required |
Returns:
| Type | Description |
|---|---|
Tenant
|
The stored tenant with server-generated timestamps. |
Raises:
| Type | Description |
|---|---|
ValueError
|
When the |
TenancyError
|
On unexpected storage failure. |
Source code in src/fastapi_tenancy/storage/database.py
update
async
¶
Replace all mutable fields of an existing tenant.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant
|
Tenant
|
Updated tenant object. |
required |
Returns:
| Type | Description |
|---|---|
Tenant
|
The updated tenant with a refreshed |
Raises:
| Type | Description |
|---|---|
TenantNotFoundError
|
When |
TenancyError
|
On unexpected storage failure. |
Source code in src/fastapi_tenancy/storage/database.py
delete
async
¶
Remove a tenant from the store.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id
|
str
|
ID of the tenant to delete. |
required |
Raises:
| Type | Description |
|---|---|
TenantNotFoundError
|
When tenant_id does not exist. |
TenancyError
|
On unexpected storage failure. |
Source code in src/fastapi_tenancy/storage/database.py
set_status
async
¶
set_status(tenant_id: str, status: TenantStatus) -> Tenant
Change the lifecycle status of a tenant.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id
|
str
|
ID of the tenant to update. |
required |
status
|
TenantStatus
|
The new |
required |
Returns:
| Type | Description |
|---|---|
Tenant
|
The updated tenant. |
Raises:
| Type | Description |
|---|---|
TenantNotFoundError
|
When tenant_id does not exist. |
TenancyError
|
On unexpected storage failure. |
Source code in src/fastapi_tenancy/storage/database.py
update_metadata
async
¶
update_metadata(
tenant_id: str, metadata: dict[str, Any]
) -> Tenant
Atomically merge metadata into the tenant's metadata blob.
Uses a database-level atomic JSON merge on PostgreSQL (|| operator)
and a serialisable read-modify-write transaction on all other dialects.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id
|
str
|
ID of the tenant to update. |
required |
metadata
|
dict[str, Any]
|
Key-value pairs to shallow-merge into existing metadata. |
required |
Returns:
| Type | Description |
|---|---|
Tenant
|
The updated tenant. |
Raises:
| Type | Description |
|---|---|
TenantNotFoundError
|
When tenant_id does not exist. |
TenancyError
|
On unexpected storage failure. |
Source code in src/fastapi_tenancy/storage/database.py
get_by_ids
async
¶
get_by_ids(tenant_ids: Any) -> Sequence[Tenant]
Fetch multiple tenants in a single query using IN clause.
Overrides the base implementation to avoid N+1 queries.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_ids
|
Any
|
Iterable of opaque tenant IDs. |
required |
Returns:
| Type | Description |
|---|---|
Sequence[Tenant]
|
Found tenants (order is not guaranteed). |
Source code in src/fastapi_tenancy/storage/database.py
search
async
¶
search(
query: str, limit: int = 10, _scan_limit: int = 100
) -> Sequence[Tenant]
Search tenants by name or identifier using a database-level LIKE query.
Overrides the O(n) base implementation with a proper DB query.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str
|
Case-insensitive substring to search for. |
required |
limit
|
int
|
Maximum number of results to return. |
10
|
_scan_limit
|
int
|
Ignored (present for interface compatibility). |
100
|
Returns:
| Type | Description |
|---|---|
Sequence[Tenant]
|
Matching tenants, up to limit results. |
Source code in src/fastapi_tenancy/storage/database.py
bulk_update_status
async
¶
bulk_update_status(
tenant_ids: Any, status: TenantStatus
) -> Sequence[Tenant]
Update status for multiple tenants in a single UPDATE ... WHERE IN query.
Overrides the N+1 base implementation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_ids
|
Any
|
IDs of the tenants to update. |
required |
status
|
TenantStatus
|
New status applied to every matched tenant. |
required |
Returns:
| Type | Description |
|---|---|
Sequence[Tenant]
|
Updated tenants. |
Source code in src/fastapi_tenancy/storage/database.py
In-memory¶
InMemoryTenantStore ¶
Bases: TenantStore[Tenant]
In-memory tenant store for testing and local development.
All state lives in two Python dictionaries:
_tenants— mapstenant_id → Tenant_identifier_map— mapsidentifier → tenant_id
Both indices are kept in sync by every mutating method so that
get_by_id and get_by_identifier are always O(1).
Example — pytest fixture::
import pytest
from fastapi_tenancy.storage.memory import InMemoryTenantStore
@pytest.fixture
async def tenant_store():
store = InMemoryTenantStore()
yield store
store.clear() # reset between tests
Example — seeded store::
store = InMemoryTenantStore()
await store.create(Tenant(id="t1", identifier="acme", name="Acme"))
await store.create(Tenant(id="t2", identifier="globex", name="Globex"))
tenants = await store.list()
assert len(tenants) == 2
Initialise an empty in-memory store.
Source code in src/fastapi_tenancy/storage/memory.py
get_by_id
async
¶
get_by_id(tenant_id: str) -> Tenant
Return the tenant with tenant_id or raise TenantNotFoundError.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id
|
str
|
Opaque tenant primary key. |
required |
Returns:
| Type | Description |
|---|---|
Tenant
|
Matching |
Raises:
| Type | Description |
|---|---|
TenantNotFoundError
|
When tenant_id is not in the store. |
Source code in src/fastapi_tenancy/storage/memory.py
get_by_identifier
async
¶
get_by_identifier(identifier: str) -> Tenant
Return the tenant with identifier or raise TenantNotFoundError.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
identifier
|
str
|
Human-readable tenant slug. |
required |
Returns:
| Type | Description |
|---|---|
Tenant
|
Matching |
Raises:
| Type | Description |
|---|---|
TenantNotFoundError
|
When no tenant with identifier exists. |
Source code in src/fastapi_tenancy/storage/memory.py
list
async
¶
list(
skip: int = 0,
limit: int = 100,
status: TenantStatus | None = None,
) -> list[Tenant]
Return a page of tenants sorted by created_at descending.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skip
|
int
|
Number of records to skip. |
0
|
limit
|
int
|
Maximum number of records to return. |
100
|
status
|
TenantStatus | None
|
Optional status filter. |
None
|
Returns:
| Type | Description |
|---|---|
list[Tenant]
|
Filtered and paginated list of tenants. |
Source code in src/fastapi_tenancy/storage/memory.py
count
async
¶
count(status: TenantStatus | None = None) -> int
Return the number of stored tenants, optionally filtered by status.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
status
|
TenantStatus | None
|
Optional status filter. |
None
|
Returns:
| Type | Description |
|---|---|
int
|
Non-negative integer count. |
Source code in src/fastapi_tenancy/storage/memory.py
exists
async
¶
Return True when a tenant with tenant_id exists.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id
|
str
|
Opaque tenant primary key. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
Existence flag. |
Source code in src/fastapi_tenancy/storage/memory.py
get_by_ids
async
¶
get_by_ids(tenant_ids: Any) -> Sequence[Tenant]
Return all tenants whose IDs appear in tenant_ids.
Silently skips IDs with no matching tenant.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_ids
|
Any
|
Iterable of opaque tenant IDs. |
required |
Returns:
| Type | Description |
|---|---|
Sequence[Tenant]
|
Found tenants in the order their IDs appeared. |
Source code in src/fastapi_tenancy/storage/memory.py
create
async
¶
Persist a new tenant in memory.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant
|
Tenant
|
Fully-populated tenant object. |
required |
Returns:
| Type | Description |
|---|---|
Tenant
|
The stored tenant (identical to input; no server-side transforms). |
Raises:
| Type | Description |
|---|---|
ValueError
|
When |
Source code in src/fastapi_tenancy/storage/memory.py
update
async
¶
Replace all mutable fields of an existing tenant.
If the identifier changes, the old identifier key is removed from the index and the new one is added atomically.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant
|
Tenant
|
Updated tenant object. |
required |
Returns:
| Type | Description |
|---|---|
Tenant
|
Updated tenant with a refreshed |
Raises:
| Type | Description |
|---|---|
TenantNotFoundError
|
When |
Source code in src/fastapi_tenancy/storage/memory.py
delete
async
¶
Remove a tenant from both internal indices.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id
|
str
|
ID of the tenant to delete. |
required |
Raises:
| Type | Description |
|---|---|
TenantNotFoundError
|
When tenant_id is not in the store. |
Source code in src/fastapi_tenancy/storage/memory.py
set_status
async
¶
set_status(tenant_id: str, status: TenantStatus) -> Tenant
Update the lifecycle status of a tenant.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id
|
str
|
ID of the tenant to update. |
required |
status
|
TenantStatus
|
New status value. |
required |
Returns:
| Type | Description |
|---|---|
Tenant
|
Updated tenant with a refreshed |
Raises:
| Type | Description |
|---|---|
TenantNotFoundError
|
When tenant_id is not in the store. |
Source code in src/fastapi_tenancy/storage/memory.py
update_metadata
async
¶
update_metadata(
tenant_id: str, metadata: dict[str, Any]
) -> Tenant
Shallow-merge metadata into the tenant's metadata dictionary.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id
|
str
|
ID of the tenant to update. |
required |
metadata
|
dict[str, Any]
|
Key-value pairs to merge. |
required |
Returns:
| Type | Description |
|---|---|
Tenant
|
Updated tenant. |
Raises:
| Type | Description |
|---|---|
TenantNotFoundError
|
When tenant_id is not in the store. |
Source code in src/fastapi_tenancy/storage/memory.py
bulk_update_status
async
¶
bulk_update_status(
tenant_ids: Any, status: TenantStatus
) -> Sequence[Tenant]
Update the status of multiple tenants in one pass.
Uses a single shared timestamp so all updated_at values are
identical across the batch. IDs with no matching tenant are skipped.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_ids
|
Any
|
Iterable of opaque tenant IDs. |
required |
status
|
TenantStatus
|
New status applied to every matched tenant. |
required |
Returns:
| Type | Description |
|---|---|
Sequence[Tenant]
|
Updated tenants in the order their IDs appeared. |
Source code in src/fastapi_tenancy/storage/memory.py
search
async
¶
search(
query: str, limit: int = 10, _scan_limit: int = 100
) -> Sequence[Tenant]
Search tenants by name or identifier substring.
Results are ranked by relevance
- Exact identifier match.
- Identifier starts with query.
- Name contains query.
- Identifier contains query.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str
|
Case-insensitive search string. |
required |
limit
|
int
|
Maximum number of results to return. |
10
|
_scan_limit
|
int
|
Accepted for interface compatibility; all records scanned. |
100
|
Returns:
| Type | Description |
|---|---|
Sequence[Tenant]
|
Matching tenants sorted by relevance, up to limit results. |
Source code in src/fastapi_tenancy/storage/memory.py
clear ¶
Remove all tenants from both internal indices.
Use in test teardown / fixture cleanup to reset state between test cases::
@pytest.fixture
async def store():
store = InMemoryTenantStore()
yield store
store.clear()
Source code in src/fastapi_tenancy/storage/memory.py
get_all ¶
get_all() -> dict[str, Tenant]
statistics ¶
Return a summary of current store state for monitoring and debugging.
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Dictionary with:
- |
Source code in src/fastapi_tenancy/storage/memory.py
Redis¶
RedisTenantStore ¶
RedisTenantStore(
redis_url: str,
primary_store: TenantStore[Tenant],
ttl: int = 3600,
key_prefix: str = "tenant",
)
Bases: TenantStore[Tenant]
Redis write-through cache on top of a primary :class:TenantStore.
Reads are served from Redis when the key is warm. Every write goes to the primary store first and then populates (or invalidates) the cache — Redis is never the source of truth.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
redis_url
|
str
|
Redis connection URL (e.g. |
required |
primary_store
|
TenantStore[Tenant]
|
Backing :class: |
required |
ttl
|
int
|
Cache TTL in seconds (default 3600 = 1 hour). |
3600
|
key_prefix
|
str
|
Prefix applied to all Redis keys. Use a distinct prefix per application to avoid key collisions in a shared Redis instance. |
'tenant'
|
Example::
from fastapi_tenancy.storage.database import SQLAlchemyTenantStore
from fastapi_tenancy.storage.redis import RedisTenantStore
primary = SQLAlchemyTenantStore(
database_url="postgresql+asyncpg://user:pass@localhost/myapp"
)
await primary.initialize()
cache = RedisTenantStore(
redis_url="redis://localhost:6379/0",
primary_store=primary,
ttl=1800,
)
tenant = await cache.get_by_identifier("acme-corp")
await cache.close()
Source code in src/fastapi_tenancy/storage/redis.py
initialize
async
¶
Initialise the primary store if it supports it.
Delegates to the primary store's initialize() so callers do not
need to initialise both stores separately, satisfying LSP: a
RedisTenantStore can be used anywhere a TenantStore is expected
without additional lifecycle management by the caller.
Source code in src/fastapi_tenancy/storage/redis.py
close
async
¶
Close the Redis connection pool and the primary store.
Both resources are closed so callers only need to call close()
on the outermost store.
Source code in src/fastapi_tenancy/storage/redis.py
get_by_id
async
¶
get_by_id(tenant_id: str) -> Tenant
Return tenant from cache; fall back to primary on miss.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id
|
str
|
Opaque tenant primary key. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
Matching |
Tenant
|
class: |
Raises:
| Type | Description |
|---|---|
TenantNotFoundError
|
Propagated from the primary store. |
Source code in src/fastapi_tenancy/storage/redis.py
get_by_identifier
async
¶
get_by_identifier(identifier: str) -> Tenant
Return tenant from cache; fall back to primary on miss.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
identifier
|
str
|
Human-readable tenant slug. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
Matching |
Tenant
|
class: |
Raises:
| Type | Description |
|---|---|
TenantNotFoundError
|
Propagated from the primary store. |
Source code in src/fastapi_tenancy/storage/redis.py
list
async
¶
list(
skip: int = 0,
limit: int = 100,
status: TenantStatus | None = None,
) -> Sequence[Tenant]
Delegate list to primary — paginated results are not cached.
Caching paginated list results adds complex invalidation overhead for negligible benefit. Always reads from the primary store.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skip
|
int
|
Pagination offset. |
0
|
limit
|
int
|
Maximum number of results. |
100
|
status
|
TenantStatus | None
|
Optional status filter. |
None
|
Returns:
| Type | Description |
|---|---|
Sequence[Tenant]
|
Tenants from the primary store. |
Source code in src/fastapi_tenancy/storage/redis.py
count
async
¶
count(status: TenantStatus | None = None) -> int
Delegate count to primary store.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
status
|
TenantStatus | None
|
Optional status filter. |
None
|
Returns:
| Type | Description |
|---|---|
int
|
Total matching tenant count. |
Source code in src/fastapi_tenancy/storage/redis.py
exists
async
¶
Return True if the tenant exists in cache or primary.
Checks Redis first (O(1)); falls back to primary on a cache miss.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id
|
str
|
Opaque tenant primary key. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
Existence flag. |
Source code in src/fastapi_tenancy/storage/redis.py
create
async
¶
Create in primary, then populate cache.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant
|
Tenant
|
Fully-populated tenant object. |
required |
Returns:
| Type | Description |
|---|---|
Tenant
|
Stored tenant with server-generated timestamps. |
Raises:
| Type | Description |
|---|---|
ValueError
|
When |
Source code in src/fastapi_tenancy/storage/redis.py
update
async
¶
Update primary, invalidate old cache keys, repopulate with new values.
Cache-first optimisation: tries to read the old identifier from the cache before falling back to the primary store, avoiding an extra database round-trip on warm cache paths.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant
|
Tenant
|
Updated tenant object. |
required |
Returns:
| Type | Description |
|---|---|
Tenant
|
Updated tenant. |
Raises:
| Type | Description |
|---|---|
TenantNotFoundError
|
When |
Source code in src/fastapi_tenancy/storage/redis.py
delete
async
¶
Delete from primary and invalidate cache keys.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id
|
str
|
ID of the tenant to delete. |
required |
Raises:
| Type | Description |
|---|---|
TenantNotFoundError
|
When tenant_id does not exist. |
Source code in src/fastapi_tenancy/storage/redis.py
set_status
async
¶
set_status(tenant_id: str, status: TenantStatus) -> Tenant
Update status in primary and refresh cache.
Cache-first optimisation for reading the old identifier.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id
|
str
|
ID of the tenant to update. |
required |
status
|
TenantStatus
|
New lifecycle status. |
required |
Returns:
| Type | Description |
|---|---|
Tenant
|
Updated tenant. |
Raises:
| Type | Description |
|---|---|
TenantNotFoundError
|
When tenant_id does not exist. |
Source code in src/fastapi_tenancy/storage/redis.py
update_metadata
async
¶
update_metadata(
tenant_id: str, metadata: dict[str, Any]
) -> Tenant
Merge metadata in primary and refresh cache.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_id
|
str
|
ID of the tenant to update. |
required |
metadata
|
dict[str, Any]
|
Key-value pairs to shallow-merge. |
required |
Returns:
| Type | Description |
|---|---|
Tenant
|
Updated tenant. |
Raises:
| Type | Description |
|---|---|
TenantNotFoundError
|
When tenant_id does not exist. |
Source code in src/fastapi_tenancy/storage/redis.py
get_by_ids
async
¶
get_by_ids(tenant_ids: Iterable[str]) -> Sequence[Tenant]
Fetch multiple tenants — cache hits served first; misses delegated.
Uses a Redis pipeline to batch all cache lookups into one round-trip. The returned list preserves the same order as tenant_ids: IDs that are not found in the cache or primary store are silently omitted.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_ids
|
Iterable[str]
|
Iterable of opaque tenant IDs. |
required |
Returns:
| Type | Description |
|---|---|
Sequence[Tenant]
|
Found tenants in the same order as tenant_ids. |
Source code in src/fastapi_tenancy/storage/redis.py
bulk_update_status
async
¶
bulk_update_status(
tenant_ids: Iterable[str], status: TenantStatus
) -> Sequence[Tenant]
Update status for multiple tenants and invalidate their cache entries.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tenant_ids
|
Iterable[str]
|
IDs of tenants to update. |
required |
status
|
TenantStatus
|
New status applied to every matched tenant. |
required |
Returns:
| Type | Description |
|---|---|
Sequence[Tenant]
|
Updated tenants. |
Source code in src/fastapi_tenancy/storage/redis.py
invalidate_all
async
¶
Delete every cache key owned by this store.
Uses SCAN with a count hint rather than KEYS to avoid
blocking the Redis event loop on large keyspaces.
Returns:
| Type | Description |
|---|---|
int
|
Number of Redis keys deleted. |
Source code in src/fastapi_tenancy/storage/redis.py
cache_stats
async
¶
Return lightweight cache statistics.
Scans the keyspace via SCAN — does not block Redis.
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Dictionary with |