Finalize user-engine contracts and operability

This commit is contained in:
2026-05-22 21:45:30 +02:00
parent b81a9e05da
commit ce2d620f4e
12 changed files with 392 additions and 15 deletions

View File

@@ -1,13 +1,14 @@
"""Headless user-domain and profile engine."""
from user_engine.projections import ClaimsEnrichmentProjectionCache
from user_engine.projections import CacheStatus, ClaimsEnrichmentProjectionCache
from user_engine.service import PLATFORM_TENANT, UserEngineService
__all__ = [
"CacheStatus",
"ClaimsEnrichmentProjectionCache",
"PLATFORM_TENANT",
"UserEngineService",
"__version__",
]
__version__ = "0.0.0"
__version__ = "0.1.0"

View File

@@ -8,6 +8,14 @@ from user_engine.domain import Actor, ProjectionType
from user_engine.service import Projection, UserEngineService
@dataclass
class CacheStatus:
entries: int
tenants: tuple[str, ...]
applications: tuple[str, ...]
users: tuple[str, ...]
@dataclass
class ClaimsEnrichmentProjectionCache:
"""Cache claims-enrichment projections for external token adapters.
@@ -47,3 +55,11 @@ class ClaimsEnrichmentProjectionCache:
for key in tuple(self._cache):
if key[2] == user_id:
del self._cache[key]
def status(self) -> CacheStatus:
return CacheStatus(
entries=len(self._cache),
tenants=tuple(sorted({key[0] for key in self._cache})),
applications=tuple(sorted({key[1] for key in self._cache})),
users=tuple(sorted({key[2] for key in self._cache})),
)

View File

@@ -97,6 +97,21 @@ class TenantDiagnostics:
memberships: tuple[Membership, ...]
@dataclass(frozen=True)
class OutboxDiagnostics:
pending_count: int
event_types: Mapping[str, int]
oldest_correlation_id: str | None
@dataclass(frozen=True)
class OperabilitySnapshot:
ready: bool
checks: Mapping[str, bool]
metrics: Mapping[str, int]
issues: tuple[str, ...]
class UserEngineService:
"""Headless service API for isolated user and profile management."""
@@ -667,6 +682,61 @@ class UserEngineService:
def outbox_events(self) -> tuple[OutboxEvent, ...]:
return tuple(self.store.outbox_events)
def outbox_diagnostics(self) -> OutboxDiagnostics:
event_types: dict[str, int] = {}
for event in self.store.outbox_events:
event_types[event.event_type] = event_types.get(event.event_type, 0) + 1
oldest = self.store.outbox_events[0].correlation_id if self.store.outbox_events else None
return OutboxDiagnostics(
pending_count=len(self.store.outbox_events),
event_types=event_types,
oldest_correlation_id=oldest,
)
def operability_snapshot(self) -> OperabilitySnapshot:
audit_correlation_ok = all(
bool(record.correlation_id) for record in self.store.audit_records
)
checks = {
"ready": self.readiness().ready,
"audit_correlation": audit_correlation_ok,
"outbox_diagnostics": self.outbox_diagnostics().pending_count >= 0,
}
metrics = {
"users": len(self.store.users),
"accounts": len(self.store.accounts),
"tenant_accounts": len(self.store.tenant_accounts),
"memberships": len(self.store.memberships),
"applications": len(self.store.applications),
"catalogs": len(self.store.catalogs),
"profile_values": len(self.store.profile_values),
"audit_records": len(self.store.audit_records),
"pending_outbox_events": len(self.store.outbox_events),
}
issues = tuple(key for key, passed in checks.items() if not passed)
return OperabilitySnapshot(
ready=all(checks.values()),
checks=checks,
metrics=metrics,
issues=issues,
)
def structured_log_context(
self,
*,
correlation_id: str,
tenant: str,
actor: Actor | None = None,
) -> Mapping[str, str]:
context = {
"correlation_id": correlation_id,
"tenant": tenant,
}
if actor is not None:
context["actor_issuer"] = actor.issuer
context["actor_subject"] = actor.subject
return context
def _session(self, actor: Actor, user: User, account: Account) -> UserSession:
identities = tuple(
identity