Skip to content

symfonic.kernel.contracts.tenancy

tenancy

SCOPE-6 / SCOPE-11 — the subject scope, and the one way it becomes a key.

Two rules meet in this module and neither is negotiable.

One serialization contract. SCOPE-11 admits exactly one way a scope becomes a storage key or a thread key. The formula was not chosen here — it was chosen by the engine that minted every pause token currently in flight, and symfonic.capabilities.human.binding.hash_scope reproduces it character for character including the _ placeholder. :attr:SubjectScope.scope_key is that same string, so hashing a SubjectScope and hashing whatever the legacy path passed produce the same sixteen hex digits. A second formula "for the new path" is how two halves of one deployment stop seeing the same rows.

Exact ancestor-prefix membership. SEC-TEN-1 forbids LIKE 'prefix%', $regex and startswith in every backend, because acme-evil is a string prefix of nothing anyone intended. :meth:SubjectScope.narrows compares segments, so a lookalike tenant is a different scope no matter how its name was spelled.

This module lives in kernel.contracts because everybody needs the value and nobody may import platform to get it (SEC-TEN-5, LAY-ADR C6): the platform derives a scope, the backends enforce it, and neither borrows the other's job. Standard library only, by the package rule.

SubjectScope dataclass

SubjectScope(tenant_id: str, sub_tenant_id: str | None = None, namespace: str | None = None)

The immutable scope a derivation produces and nothing downstream edits.

sub_tenant_id and namespace are two names for one segment — the shipped FrameworkTenantScope allows either, requires them to agree when both are given, and that behaviour is preserved here rather than reinterpreted.

child_segment property

child_segment: str | None

The single second-level segment, whichever name it arrived under.

scope_key property

scope_key: str

The one serialization (SCOPE-11). Exactly what hash_scope hashes.

segments property

segments: tuple[str, ...]

The scope as an ordered path — the unit membership is tested on.

from_key classmethod

from_key(key: str) -> SubjectScope

Rebuild a scope from :attr:scope_key. Round-trips exactly.

Source code in src/symfonic/kernel/contracts/tenancy.py
@classmethod
def from_key(cls, key: str) -> SubjectScope:
    """Rebuild a scope from :attr:`scope_key`. Round-trips exactly."""
    parts = key.split(_SEPARATOR)
    if len(parts) != 3:
        raise SubjectScopeError(
            f"{key!r} is not a scope key; the contract is "
            f"tenant{_SEPARATOR}sub{_SEPARATOR}namespace with "
            f"{ABSENT_SEGMENT!r} for an absent segment"
        )
    tenant, sub, namespace = parts
    return cls(
        tenant_id=tenant,
        sub_tenant_id=None if sub == ABSENT_SEGMENT else sub,
        namespace=None if namespace == ABSENT_SEGMENT else namespace,
    )

narrows

narrows(parent: SubjectScope) -> bool

Is this scope parent itself, or strictly inside it?

Segment-wise, never string-wise: acme-evil does not narrow acme.

Source code in src/symfonic/kernel/contracts/tenancy.py
def narrows(self, parent: SubjectScope) -> bool:
    """Is this scope ``parent`` itself, or strictly inside it?

    Segment-wise, never string-wise: ``acme-evil`` does not narrow ``acme``.
    """
    mine, theirs = self.segments, parent.segments
    if len(mine) < len(theirs):
        return False
    return mine[: len(theirs)] == theirs

visible_to

visible_to(viewer: SubjectScope) -> bool

SEC-TEN-1 read predicate: exact ancestor-prefix set membership.

Source code in src/symfonic/kernel/contracts/tenancy.py
def visible_to(self, viewer: SubjectScope) -> bool:
    """SEC-TEN-1 read predicate: exact ancestor-prefix set membership."""
    return self.narrows(viewer)

SubjectScopeError

Bases: ValueError

A scope that cannot name its subject, or names two of them.

Deliberately a ValueError subclass: a malformed scope is bad input, and the transport maps it to 400 (EMAP-4). It is a distinct type so a caller can tell "this scope is unusable" from "this tenant is not yours", which is the distinction T3.4.5 asserts three separate refusals for.