from __future__ import annotations from collections.abc import Iterable from dataclasses import dataclass, field from typing import Any from .base import ModuleMenuSeed, ModulePermission ModulePhase = str @dataclass class ModuleRegistry: """Registry for in-process modules.""" modules: list[Any] = field(default_factory=list) permissions: dict[str, ModulePermission] = field(default_factory=dict) menu_seeds: dict[str, ModuleMenuSeed] = field(default_factory=dict) def register(self, module: Any) -> None: name = getattr(module, "name", None) if not name: raise ValueError("module must define a non-empty name") if self.get(name) is not None: raise ValueError(f"module already registered: {name}") self.modules.append(module) def extend(self, modules: Iterable[Any] | None) -> None: for module in modules or []: self.register(module) def get(self, name: str) -> Any | None: for module in self.modules: if getattr(module, "name", None) == name: return module return None def run_phase(self, phase: ModulePhase, app) -> None: for module in self.modules: callback = getattr(module, phase, None) if callback is not None: callback(app) def register_permission(self, permission: ModulePermission) -> ModulePermission: if not permission.code: raise ValueError("permission code is required") if permission.code in self.permissions: raise ValueError(f"permission already registered: {permission.code}") self.permissions[permission.code] = permission return permission def register_menu_seed(self, menu_seed: ModuleMenuSeed) -> ModuleMenuSeed: if not menu_seed.id: raise ValueError("menu seed id is required") if not menu_seed.name: raise ValueError("menu seed name is required") if menu_seed.id in self.menu_seeds: raise ValueError(f"menu seed already registered: {menu_seed.id}") self.menu_seeds[menu_seed.id] = menu_seed return menu_seed def list_permissions(self) -> list[ModulePermission]: return list(self.permissions.values()) def list_menu_seeds(self) -> list[ModuleMenuSeed]: return sorted( self.menu_seeds.values(), key=lambda menu_seed: (menu_seed.sort, menu_seed.name), ) def get_module_registry(app) -> ModuleRegistry: registry = app.extensions.get("iti_modules") if registry is None: registry = ModuleRegistry() app.extensions["iti_modules"] = registry return registry def init_modules(app, modules: Iterable[Any] | None = None) -> ModuleRegistry: registry = get_module_registry(app) registry.extend(modules) registry.run_phase("init_app", app) registry.run_phase("register_commands", app) return registry