perf(core): batch tenant summary providers

This commit is contained in:
2026-07-30 01:15:04 +02:00
parent 51d4032b86
commit af3e0a055d
6 changed files with 389 additions and 1 deletions
+5
View File
@@ -310,6 +310,10 @@ class ResourceAclProvider(Protocol):
TenantSummaryProvider = Callable[[object, str], Mapping[str, int]]
TenantSummaryBatchProvider = Callable[
[object, Sequence[str]],
Mapping[str, Mapping[str, int]],
]
@dataclass(frozen=True, slots=True)
@@ -357,6 +361,7 @@ class ModuleManifest:
frontend: FrontendModule | None = None
resource_acl_providers: tuple[ResourceAclProvider, ...] = ()
tenant_summary_providers: tuple[TenantSummaryProvider, ...] = ()
tenant_summary_batch_providers: tuple[TenantSummaryBatchProvider, ...] = ()
delete_veto_providers: Mapping[str, Sequence[DeleteVetoProvider]] = field(default_factory=dict)
uninstall_guard_providers: tuple[UninstallGuardProvider, ...] = ()
capability_factories: Mapping[str, CapabilityFactory] = field(default_factory=dict)
+19
View File
@@ -21,6 +21,7 @@ from govoplan_core.core.modules import (
RoleTemplate,
SUPPORTED_FRONTEND_ASSET_MANIFEST_CONTRACT_VERSION,
SUPPORTED_MANIFEST_CONTRACT_VERSION,
TenantSummaryBatchProvider,
TenantSummaryProvider,
user_workflow_scope_condition_issues,
)
@@ -62,6 +63,7 @@ class PlatformRegistry:
def __init__(self) -> None:
self._manifests: dict[str, ModuleManifest] = {}
self._tenant_summary_providers: dict[str, TenantSummaryProvider] = {}
self._tenant_summary_batch_providers: dict[str, TenantSummaryBatchProvider] = {}
self._delete_veto_providers: dict[str, list[DeleteVetoProviderRegistration]] = defaultdict(list)
self._capability_factories: dict[str, CapabilityFactory] = {}
self._capabilities: dict[str, object] = {}
@@ -79,6 +81,8 @@ class PlatformRegistry:
self._manifests[manifest.id] = manifest
for provider in manifest.tenant_summary_providers:
self.register_tenant_summary_provider(manifest.id, provider)
for provider in manifest.tenant_summary_batch_providers:
self.register_tenant_summary_batch_provider(manifest.id, provider)
for resource_type, providers in manifest.delete_veto_providers.items():
for provider in providers:
self.register_delete_veto(manifest.id, resource_type, provider)
@@ -110,6 +114,9 @@ class PlatformRegistry:
self._manifests = dict(replacement._manifests)
self._tenant_summary_providers = dict(replacement._tenant_summary_providers)
self._tenant_summary_batch_providers = dict(
replacement._tenant_summary_batch_providers
)
self._delete_veto_providers = defaultdict(list, {
resource_type: list(providers)
for resource_type, providers in replacement._delete_veto_providers.items()
@@ -273,6 +280,18 @@ class PlatformRegistry:
def tenant_summary_providers(self) -> Mapping[str, TenantSummaryProvider]:
return dict(self._tenant_summary_providers)
def register_tenant_summary_batch_provider(
self,
module_id: str,
provider: TenantSummaryBatchProvider,
) -> None:
self._tenant_summary_batch_providers[module_id] = provider
def tenant_summary_batch_providers(
self,
) -> Mapping[str, TenantSummaryBatchProvider]:
return dict(self._tenant_summary_batch_providers)
def register_delete_veto(self, module_id: str, resource_type: str, provider: DeleteVetoProvider) -> None:
self._delete_veto_providers[resource_type].append(DeleteVetoProviderRegistration(
module_id=module_id,
+3
View File
@@ -191,6 +191,9 @@ def _manifest_source_roots(manifest: ModuleManifest) -> tuple[Path, ...]:
for provider in manifest.tenant_summary_providers:
roots.extend(_source_roots_for_object(provider))
for provider in manifest.tenant_summary_batch_providers:
roots.extend(_source_roots_for_object(provider))
for providers in manifest.delete_veto_providers.values():
for provider in providers:
roots.extend(_source_roots_for_object(provider))
+115
View File
@@ -1,6 +1,8 @@
from __future__ import annotations
from dataclasses import dataclass
from collections.abc import Mapping, Sequence
from typing import Protocol, runtime_checkable
from sqlalchemy.orm import Session
@@ -18,6 +20,16 @@ class EffectiveTenantGovernance:
allow_api_keys: bool
@runtime_checkable
class _AccessTenantCountsBatchProvider(Protocol):
def tenant_counts_many(
self,
session: object,
tenant_ids: Sequence[str],
) -> Mapping[str, Mapping[str, int]]:
...
def _narrowing_bool(system_allows: bool, tenant_override: bool | None) -> bool:
if not system_allows:
return False
@@ -103,3 +115,106 @@ def tenant_counts(
"api_keys": int(access_counts.get("api_keys", 0)),
"active_api_keys": int(access_counts.get("active_api_keys", access_counts.get("api_keys", 0))),
}
def tenant_counts_many(
session: Session,
tenant_ids: Sequence[str],
*,
module_ids: tuple[str, ...] | None = None,
) -> dict[str, dict[str, int]]:
"""Collect list-page summaries while preserving legacy provider support."""
normalized_ids = tuple(
dict.fromkeys(
str(tenant_id).strip()
for tenant_id in tenant_ids
if str(tenant_id).strip()
)
)
counts_by_tenant: dict[str, dict[str, int]] = {
tenant_id: {} for tenant_id in normalized_ids
}
if not normalized_ids:
return counts_by_tenant
registry = get_registry()
if registry is not None and hasattr(registry, "tenant_summary_providers"):
providers = dict(registry.tenant_summary_providers())
if module_ids is not None:
providers = {
module_id: provider
for module_id, provider in providers.items()
if module_id in module_ids
}
batch_providers = (
dict(registry.tenant_summary_batch_providers())
if hasattr(registry, "tenant_summary_batch_providers")
else {}
)
for module_id, provider in providers.items():
batch_provider = batch_providers.get(module_id)
if batch_provider is None:
provided_by_tenant = {
tenant_id: provider(session, tenant_id)
for tenant_id in normalized_ids
}
else:
provided_by_tenant = batch_provider(session, normalized_ids)
_merge_tenant_counts(
counts_by_tenant,
provided_by_tenant,
tenant_ids=normalized_ids,
)
access_administration = _access_administration()
if isinstance(access_administration, _AccessTenantCountsBatchProvider):
access_counts = access_administration.tenant_counts_many(
session,
normalized_ids,
)
elif access_administration is not None:
access_counts = {
tenant_id: access_administration.tenant_counts(session, tenant_id)
for tenant_id in normalized_ids
}
else:
access_counts = {}
_merge_tenant_counts(
counts_by_tenant,
access_counts,
tenant_ids=normalized_ids,
)
return {
tenant_id: _normalized_tenant_counts(counts_by_tenant[tenant_id])
for tenant_id in normalized_ids
}
def _merge_tenant_counts(
target: dict[str, dict[str, int]],
provided_by_tenant: Mapping[str, Mapping[str, int]],
*,
tenant_ids: Sequence[str],
) -> None:
for tenant_id in tenant_ids:
provided = provided_by_tenant.get(tenant_id, {})
target[tenant_id].update(
{str(key): int(value) for key, value in provided.items()}
)
def _normalized_tenant_counts(counts: Mapping[str, int]) -> dict[str, int]:
return {
**{str(key): int(value) for key, value in counts.items()},
"users": int(counts.get("users", 0)),
"active_users": int(counts.get("active_users", 0)),
"groups": int(counts.get("groups", 0)),
"campaigns": int(counts.get("campaigns", 0)),
"files": int(counts.get("files", 0)),
"api_keys": int(counts.get("api_keys", 0)),
"active_api_keys": int(
counts.get("active_api_keys", counts.get("api_keys", 0))
),
}