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
|
|
None
|
Returns:
| Type | Description |
|---|---|
DependencyNamespace exposing ``get_current_tenant``, which returns
|
the resolved |
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.