Release v0.1.2

This commit is contained in:
2026-06-25 19:58:20 +02:00
parent 15794e920e
commit 02564047e9
73 changed files with 4073 additions and 478 deletions

View File

@@ -35,6 +35,8 @@ def discover_module_manifests(
enabled = set(enabled_modules) if enabled_modules is not None else None
manifests: list[ModuleManifest] = []
for entry_point in iter_module_entry_points(group):
if enabled is not None and entry_point.name not in enabled:
continue
manifest = _load_manifest(entry_point)
if enabled is not None and manifest.id not in enabled:
continue

View File

@@ -104,6 +104,7 @@ class ResourceAclProvider(Protocol):
TenantSummaryProvider = Callable[[object, str], Mapping[str, int]]
DeleteVetoProvider = Callable[[object, str, str], None]
RouteFactory = Callable[[ModuleContext], "APIRouter"]
CapabilityFactory = Callable[[ModuleContext], object]
@dataclass(frozen=True, slots=True)
@@ -122,4 +123,5 @@ class ModuleManifest:
resource_acl_providers: tuple[ResourceAclProvider, ...] = ()
tenant_summary_providers: tuple[TenantSummaryProvider, ...] = ()
delete_veto_providers: Mapping[str, Sequence[DeleteVetoProvider]] = field(default_factory=dict)
capability_factories: Mapping[str, CapabilityFactory] = field(default_factory=dict)

View File

@@ -0,0 +1,13 @@
from __future__ import annotations
def module_not_found_for(exc: ModuleNotFoundError, package: str) -> bool:
"""Return true when a ModuleNotFoundError belongs to an optional package."""
missing = exc.name or ""
return missing == package or missing.startswith(f"{package}.")
def reraise_unless_missing_package(exc: ModuleNotFoundError, package: str) -> None:
if not module_not_found_for(exc, package):
raise exc

View File

@@ -6,7 +6,9 @@ from collections.abc import Iterable, Mapping
from dataclasses import dataclass
from govoplan_core.core.modules import (
CapabilityFactory,
DeleteVetoProvider,
ModuleContext,
ModuleManifest,
NavItem,
PermissionDefinition,
@@ -36,6 +38,9 @@ class PlatformRegistry:
self._manifests: dict[str, ModuleManifest] = {}
self._tenant_summary_providers: dict[str, TenantSummaryProvider] = {}
self._delete_veto_providers: dict[str, list[DeleteVetoProvider]] = defaultdict(list)
self._capability_factories: dict[str, CapabilityFactory] = {}
self._capabilities: dict[str, object] = {}
self._capability_context: ModuleContext | None = None
def register(self, manifest: ModuleManifest) -> ModuleManifest:
if manifest.id in self._manifests:
@@ -46,6 +51,8 @@ class PlatformRegistry:
for resource_type, providers in manifest.delete_veto_providers.items():
for provider in providers:
self.register_delete_veto(resource_type, provider)
for name, factory in manifest.capability_factories.items():
self.register_capability_factory(manifest.id, name, factory)
return manifest
def get(self, module_id: str) -> ModuleManifest | None:
@@ -79,6 +86,36 @@ class PlatformRegistry:
def resource_acl_providers(self) -> tuple[ResourceAclProvider, ...]:
return tuple(provider for manifest in self.manifests() for provider in manifest.resource_acl_providers)
def configure_capability_context(self, context: ModuleContext) -> None:
self._capability_context = context
self._capabilities.clear()
def register_capability_factory(self, module_id: str, name: str, factory: CapabilityFactory) -> None:
if name in self._capability_factories:
raise RegistryError(f"Duplicate capability: {name}")
self._capability_factories[name] = factory
def has_capability(self, name: str) -> bool:
return name in self._capability_factories
def capability(self, name: str) -> object | None:
if name in self._capabilities:
return self._capabilities[name]
factory = self._capability_factories.get(name)
if factory is None:
return None
if self._capability_context is None:
raise RegistryError(f"Capability context is not configured: {name}")
capability = factory(self._capability_context)
self._capabilities[name] = capability
return capability
def require_capability(self, name: str) -> object:
capability = self.capability(name)
if capability is None:
raise RegistryError(f"Required capability is not available: {name}")
return capability
def register_tenant_summary_provider(self, module_id: str, provider: TenantSummaryProvider) -> None:
self._tenant_summary_providers[module_id] = provider