Skip to content

msflib.tenancy

Public modules of the msflib-tenancy package.

msflib.tenancy.actions

TenantAction

Bases: ModelAction[TenantType, TenantCreateType, TenantUpdateType], Generic[TenantType, TenantCreateType, TenantUpdateType]

ensure_default_tenant(session: Session, *, settings: TenancySettings, commit: bool = True, skip_lookup: bool = False) -> TenantType

Get-or-create the single tenant this app runs as while single-tenant.

Idempotent by slug, mirroring WorkspaceAction.ensure_default_workspace: safe to call on every resolution, not just once at startup.

skip_lookup is for a caller (resolve_default_tenant_id) that has already queried and confirmed the row's absence in this same transaction -- skips redoing that identical query.

Bypasses self.create's ensure_unique_slug renaming: the default tenant's slug must always be exactly settings.DEFAULT_TENANT_SLUG, never a suffixed variant. Without this, two callers racing to seed the row for the first time could both pass the initial lookup miss, then each see the other's just-flushed row in ensure_unique_slug's own existence check and rename their slug to e.g. default-2 instead of hitting the unique-slug conflict below -- silently creating a second "default" tenant. The insert runs in a SAVEPOINT so a genuine race (the loser hits the real unique constraint) unwinds only the failed insert and recovers the winner's canonical row, exactly like ConversationAction.get_or_create_direct.

msflib.tenancy.config

msflib.tenancy.deps

Request-scoped FastAPI dependency resolving the caller's Tenant row.

Mirrors msflib.auth.deps.get_current_workspace/get_current_account: resolved once, at the endpoint boundary, from a real DB lookup -- so ScopeEnvelope (and the domain-typed scopes compiled from it, e.g. KnowledgeScope/ConversationScope) carries the same validated tenant identity that gets stamped onto rows at write time, rather than an unresolved placeholder passed through from deep inside action/service code. Pass the result to msflib.scope.get_scope_dependencies's get_current_tenant parameter.

get_tenant_dependencies(*, session_dep: Callable, settings: TenancySettings | None = None) -> DependencyNamespace

Return a get_current_tenant FastAPI dependency bound to session_dep.

Single-tenant today: resolves the one seeded default tenant (seeded at app startup -- see TenantAction.ensure_default_tenant and testsite/app/db/init_db.py), so this never creates a row itself (create_if_missing=False): a missing tenant at request time means startup seeding didn't run, a deployment error rather than a per-request condition to paper over. Identity-based tenant resolution (e.g. from a request header or JWT claim, picking one of several tenants) is a later phase's concern -- this factory is the seam that phase extends.

Parameters:

Name Type Description Default
session_dep Callable

FastAPI-injectable callable that yields a DB session.

required
settings TenancySettings | None

TenancySettings instance to resolve against. Defaults to the shared process-wide instance (see resolver._get_default_settings) when omitted.

None

Returns:

Type Description
DependencyNamespace exposing ``get_current_tenant``, which returns

the resolved Tenant row or raises HTTPException (500) if the default tenant hasn't been seeded yet.

msflib.tenancy.models

msflib.tenancy.resolver

resolve_default_tenant_id(session: Session, *, settings: TenancySettings | None = None, tenant_action: TenantAction[Tenant, TenantCreate, TenantUpdate] | None = None, create_if_missing: bool = True) -> int | None

Resolve the real int id of the default tenant, creating it if missing.

Idempotent and cached per slug for the process lifetime. settings defaults to a shared process-wide TenancySettings() when omitted -- pass one explicitly only to resolve against a non-default slug/name.

Callers frequently invoke this from inside their own commit=False unit of work (e.g. an upload flow that must roll back atomically on failure), so the first-ever creation of the tenant row is deliberately commit=False here too -- only flushed, never committed. Committing here would prematurely commit the caller's whole transaction (every change flushed so far in the same session), silently defeating any rollback the caller still intends to be able to do.

Because that creation is only flushed, the process-wide cache is not populated until this session actually commits (registered via an after_commit listener below) -- caching the id immediately would let a concurrent session on another connection resolve to an id for a row that isn't visible to it yet (MVCC), and fail its own FK check. The resolved id is still returned to this caller immediately (it's what the row it's about to reference actually got), and a second call within the same still-uncommitted transaction re-queries and finds the same flushed row rather than creating a duplicate. If the transaction rolls back instead of committing, _cache_id_after_commit tears down the commit listener so a later, unrelated commit on the same Session can't cache an id from the rolled-back attempt.

create_if_missing=False is for read-only callers (a query filter, not a row being written) that must not have the side effect of an INSERT: if the tenant hasn't been seeded yet, there is by definition nothing scoped to it yet either, so returning None (for the caller to treat as "no possible match") is correct and avoids surprising a caller running against a read-only DB role/replica with an unexpected write.

Not safe against two worker processes racing to seed the row for the first time simultaneously -- neither is ensure_default_workspace, which this mirrors; the unique constraint on slug makes the loser of that race raise rather than duplicate, but it does not retry.

resolve_scoped_tenant_id(session: Session, dim_value: str | int | None, *, settings: TenancySettings | None = None, create_if_missing: bool = True) -> int | None

Resolve the real tenant id for a scope-carried tenant_id dimension.

This is the one place in the codebase that decides how a caller's scope/domain-typed-scope tenant_id (a string, per ScopeEnvelope's dimension convention -- see KnowledgeScope/ConversationScope) turns into the real int FK value: prefer dim_value when it's already a resolved tenant id (set once, at the endpoint boundary, by a get_current_tenant-backed dependency -- see msflib.tenancy.deps and msflib.scope.get_scope_dependencies) -- that's the validated value the caller was authorized for, and it must win over anything this function could look up on its own. Falls back to resolve_default_tenant_id (today's single-tenant resolution) only when the dimension is genuinely missing (None) -- e.g. a call path that hasn't wired get_current_tenant yet, or a background job with no request scope to inherit from. A malformed, non-None dim_value raises ValueError instead, since silently falling back would turn an invalid or untrusted scope value into access to the default tenant.

reset_default_tenant_id_cache() -> None

Clear the resolved-id cache.

Intended for test isolation between suites that each build their own engine/session and would otherwise see a stale id cached by an earlier test's default tenant.