Source code for ferrosoft.apps.ferrobase.tenant.context
# Copyright (c) 2026 Ferrosoft GmbH. All rights reserved.
"""Context-variable storage for the currently active tenant.
Multi-tenancy is propagated through a single ``ContextVar`` so that both
threaded and asynchronous request handling pick up the correct tenant without
any explicit argument passing. ORM helpers in
``ferrosoft.apps.ferrobase.tenant.utils`` consult this variable to pick the
right database alias.
"""
from contextlib import contextmanager
from contextvars import ContextVar
from typing import Iterator
from ferrosoft.apps.ferrobase.models import Tenant
_tenant: ContextVar[Tenant | None] = ContextVar("tenant", default=None)
[docs]
def get_current_tenant() -> Tenant | None:
"""Return the tenant active in the current execution context, or ``None``."""
return _tenant.get()
[docs]
def set_current_tenant(tenant: Tenant | None):
"""Set the active tenant for the current execution context.
Performs no database side effects and does not restore the previous value
on exit. Prefer :func:`active_tenant` whenever the activation is scoped
to a block of code.
Args:
tenant: The tenant to activate, or ``None`` to clear the context.
"""
_tenant.set(tenant)
[docs]
@contextmanager
def active_tenant(tenant: Tenant | None) -> Iterator[None]:
"""Activate ``tenant`` for the duration of the ``with`` block.
On enter the tenant's database connection is activated (when ``tenant``
is not ``None``) and the context variable is set. On exit the previous
context value is restored via the token-based reset pattern, so nested
activations behave correctly.
Args:
tenant: Tenant to activate, or ``None`` to enter a no-tenant scope.
Yields:
Tenant | None: The tenant that was activated.
"""
if tenant is not None:
tenant.activate_connection()
token = _tenant.set(tenant)
try:
yield tenant
finally:
_tenant.reset(token)
__ALL__ = ["get_current_tenant", "tenant_context"]