Coverage for src / lexigram / contracts / tenancy / protocols.py: 0%
33 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-19 05:41 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-19 05:41 +0800
1"""Tenancy protocol definitions."""
3from __future__ import annotations
5from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
7if TYPE_CHECKING:
8 from lexigram.contracts.core.result import Result
9 from lexigram.contracts.tenancy.commands import (
10 CreateTenantCommand,
11 UpdateTenantCommand,
12 )
13 from lexigram.contracts.tenancy.errors import TenantError
14 from lexigram.contracts.tenancy.types import TenantInfo, TenantResolutionContext
17@runtime_checkable
18class TenantResolverProtocol(Protocol):
19 """Resolves tenant identity from request context.
21 Implementations are tried in priority order by
22 :class:`~lexigram.tenancy.resolution.chain.CompositeResolver`.
23 Lower ``priority`` value = tried first = higher trust level.
25 Attributes:
26 name: Unique resolver name (e.g. ``"header"``, ``"jwt_claim"``).
27 priority: Ordering weight; lower = tried first.
28 """
30 name: str
31 priority: int
33 async def resolve(self, context: TenantResolutionContext) -> str | None:
34 """Attempt to resolve the tenant identifier from the given context.
36 Args:
37 context: Immutable snapshot of request data.
39 Returns:
40 The resolved ``tenant_id`` string, or ``None`` if this resolver
41 cannot determine the tenant from the provided context.
42 """
43 ...
46@runtime_checkable
47class TenantProviderProtocol(Protocol):
48 """Storage-agnostic tenant CRUD operations.
50 Applications may supply their own implementation by binding a class to
51 this protocol in the DI container. The default implementations are
52 :class:`~lexigram.tenancy.stores.memory.InMemoryTenantProvider` (for
53 testing/dev) and ``SQLTenantProvider`` (when
54 ``lexigram-tenancy[sql]`` is installed).
55 """
57 async def get_tenant(self, tenant_id: str) -> TenantInfo | None:
58 """Retrieve a tenant by its unique identifier.
60 Args:
61 tenant_id: The unique tenant identifier.
63 Returns:
64 The :class:`~lexigram.contracts.tenancy.types.TenantInfo` record,
65 or ``None`` if no tenant with that ID exists.
66 """
67 ...
69 async def get_tenant_by_slug(self, slug: str) -> TenantInfo | None:
70 """Retrieve a tenant by its URL-safe slug.
72 Args:
73 slug: The tenant slug (e.g. ``acme-corp``).
75 Returns:
76 The matching :class:`~lexigram.contracts.tenancy.types.TenantInfo`,
77 or ``None`` if not found.
78 """
79 ...
81 async def list_tenants(self, *, active_only: bool = True) -> list[TenantInfo]:
82 """List tenants, optionally filtering to active ones only.
84 Args:
85 active_only: When ``True`` (default), return only tenants with
86 ``status == ACTIVE``.
88 Returns:
89 List of matching :class:`~lexigram.contracts.tenancy.types.TenantInfo`
90 records.
91 """
92 ...
94 async def create_tenant(
95 self, command: CreateTenantCommand
96 ) -> Result[TenantInfo, TenantError]:
97 """Persist a new tenant record.
99 Args:
100 command: :class:`~lexigram.contracts.tenancy.commands.CreateTenantCommand`
101 with the new tenant's attributes.
103 Returns:
104 ``Ok(TenantInfo)`` on success, ``Err(TenantError)`` on failure.
105 """
106 ...
108 async def update_tenant(
109 self,
110 tenant_id: str,
111 command: UpdateTenantCommand,
112 ) -> Result[TenantInfo, TenantError]:
113 """Update mutable fields on an existing tenant record.
115 Args:
116 tenant_id: Identifier of the tenant to update.
117 command: :class:`~lexigram.contracts.tenancy.commands.UpdateTenantCommand`
118 with the fields to apply.
120 Returns:
121 ``Ok(TenantInfo)`` with the updated record, or ``Err(TenantError)``.
122 """
123 ...
125 async def deactivate_tenant(self, tenant_id: str) -> Result[None, TenantError]:
126 """Mark a tenant as inactive.
128 Args:
129 tenant_id: Identifier of the tenant to deactivate.
131 Returns:
132 ``Ok(None)`` on success, ``Err(TenantError)`` on failure.
133 """
134 ...
136 async def activate_tenant(self, tenant_id: str) -> Result[None, TenantError]:
137 """Mark a tenant as active.
139 Args:
140 tenant_id: Identifier of the tenant to activate.
142 Returns:
143 ``Ok(None)`` on success, ``Err(TenantError)`` on failure.
144 """
145 ...
147 async def suspend_tenant(
148 self,
149 tenant_id: str,
150 reason: str | None = None,
151 ) -> Result[None, TenantError]:
152 """Mark a tenant as suspended.
154 Args:
155 tenant_id: Identifier of the tenant to suspend.
156 reason: Optional human-readable reason for the suspension.
158 Returns:
159 ``Ok(None)`` on success, ``Err(TenantError)`` on failure.
160 """
161 ...
164@runtime_checkable
165class TenantMembershipProtocol(Protocol):
166 """Verifies whether an authenticated caller belongs to a tenant.
168 Implemented by the application (e.g. over a ``tenant_memberships``
169 table, a ``users.tenant_id`` column, or an external identity service).
170 The framework does not ship an implementation; the app binds one in
171 the DI container. Membership caching is delegated to the implementer.
173 See Also:
174 ``docs/superpowers/specs/2026-08-16-security-tenancy-design.md`` §3.1
175 for the required sign-off and the deferred schema-provisioning option
176 (framework-managed ``tenant_memberships`` table).
177 """
179 async def user_belongs_to_tenant(self, user_id: str, tenant_id: str) -> bool:
180 """Return ``True`` when *user_id* may act under *tenant_id*."""
181 ...
184@runtime_checkable
185class TenantConfigProviderProtocol(Protocol):
186 """Per-tenant configuration key-value store.
188 Provides a low-level get/set interface. The higher-level
189 :class:`~lexigram.tenancy.config_overrides.service.TenantConfigService`
190 adds default fallback and event emission on top of this protocol.
191 """
193 async def get_config(self, tenant_id: str, key: str) -> Any | None:
194 """Retrieve a single configuration value for a tenant.
196 Args:
197 tenant_id: The tenant whose configuration is queried.
198 key: The configuration key.
200 Returns:
201 The stored value, or ``None`` if the key is not set for this tenant.
202 """
203 ...
205 async def get_all_config(self, tenant_id: str) -> dict[str, Any]:
206 """Retrieve all configuration entries for a tenant.
208 Args:
209 tenant_id: The tenant whose configuration is retrieved.
211 Returns:
212 A dictionary of all key-value pairs for the tenant.
213 Returns an empty dict if no overrides are set.
214 """
215 ...
217 async def set_config(self, tenant_id: str, key: str, value: Any) -> None:
218 """Set a configuration value for a tenant.
220 Args:
221 tenant_id: The tenant whose configuration is updated.
222 key: The configuration key.
223 value: The new value (must be JSON-serialisable).
224 """
225 ...
228@runtime_checkable
229class TenantIsolationStrategyProtocol(Protocol):
230 """Pluggable data isolation strategy.
232 Implementations provide the mechanics of isolating tenant data at the
233 database layer (row-level, schema-per-tenant, or database-per-tenant).
235 Attributes:
236 name: Strategy identifier used by the registry
237 (``"row_level"``, ``"schema"``, ``"database"``).
238 """
240 name: str
242 async def apply_isolation(self, tenant_id: str, context: dict[str, Any]) -> None:
243 """Apply tenant isolation to the given execution context.
245 For row-level isolation this is a no-op; for schema isolation this sets
246 the ``search_path`` in *context*.
248 Args:
249 tenant_id: The active tenant.
250 context: Mutable execution context dict to annotate.
251 """
252 ...
254 async def remove_isolation(self, tenant_id: str) -> None:
255 """Remove any active isolation for the tenant.
257 Args:
258 tenant_id: The tenant whose isolation context to tear down.
259 """
260 ...
262 async def provision_isolation(self, tenant_id: str) -> Result[None, TenantError]:
263 """Provision isolation resources for a newly created tenant.
265 For row-level isolation this is a no-op. For schema isolation this
266 creates the schema.
268 Args:
269 tenant_id: The newly created tenant.
271 Returns:
272 ``Ok(None)`` on success, ``Err(TenantError)`` on failure.
273 """
274 ...
276 async def deprovision_isolation(self, tenant_id: str) -> Result[None, TenantError]:
277 """Tear down isolation resources for a deactivated tenant.
279 Args:
280 tenant_id: The tenant being deactivated.
282 Returns:
283 ``Ok(None)`` on success, ``Err(TenantError)`` on failure.
284 """
285 ...
288__all__ = [
289 "TenantConfigProviderProtocol",
290 "TenantIsolationStrategyProtocol",
291 "TenantMembershipProtocol",
292 "TenantProviderProtocol",
293 "TenantResolverProtocol",
294]