Release v0.1.6

This commit is contained in:
2026-07-07 16:00:38 +02:00
parent a2053518d1
commit 150b720f12
149 changed files with 14311 additions and 8005 deletions

View File

@@ -2,12 +2,13 @@ from __future__ import annotations
from fastapi import APIRouter
from govoplan_core.core.modules import ModuleContext
from govoplan_core.core.lifecycle import ModuleLifecycleManager
from govoplan_core.core.module_management import load_startup_enabled_modules, startup_candidate_module_ids
from govoplan_core.db.session import configure_database
from govoplan_core.server.config import GovoplanServerConfig, load_server_config
from govoplan_core.server.fastapi import create_govoplan_app
from govoplan_core.server.platform import create_platform_router
from govoplan_core.server.registry import build_platform_registry
from govoplan_core.server.registry import available_module_manifests, build_platform_registry
def create_app(config: GovoplanServerConfig | str | None = None):
@@ -20,17 +21,30 @@ def create_app(config: GovoplanServerConfig | str | None = None):
if database_url:
configure_database(str(database_url))
registry = build_platform_registry(server_config.enabled_modules, manifest_factories=server_config.manifest_factories)
raw_enabled_modules = load_startup_enabled_modules(server_config.enabled_modules)
candidate_modules = startup_candidate_module_ids(server_config.enabled_modules, raw_enabled_modules)
startup_available_modules = available_module_manifests(
server_config.manifest_factories,
enabled_modules=candidate_modules,
ignore_load_errors=True,
)
available_modules = available_module_manifests(server_config.manifest_factories, ignore_load_errors=True)
enabled_modules = load_startup_enabled_modules(server_config.enabled_modules, available=startup_available_modules)
registry = build_platform_registry(enabled_modules, manifest_factories=server_config.manifest_factories)
api_router = APIRouter(prefix=server_config.api_prefix)
for router in server_config.base_routers:
api_router.include_router(router)
module_context = ModuleContext(registry=registry, settings=server_config.settings, data=server_config.module_context_data)
registry.configure_capability_context(module_context)
for manifest in registry.manifests():
if manifest.route_factory is not None:
api_router.include_router(manifest.route_factory(module_context))
lifecycle = ModuleLifecycleManager(
registry=registry,
available_modules=available_modules,
settings=server_config.settings,
api_prefix=server_config.api_prefix,
manifest_factories=tuple(server_config.manifest_factories),
module_context_data=server_config.module_context_data,
)
lifecycle.configure_runtime()
api_router.include_router(create_platform_router(settings=server_config.settings))
@@ -49,6 +63,8 @@ def create_app(config: GovoplanServerConfig | str | None = None):
lifespan=server_config.lifespan,
cors_origins=server_config.cors_origins,
)
lifecycle.attach_app(app)
lifecycle.apply_enabled_modules(tuple(manifest.id for manifest in registry.manifests()), migrate=False)
for configurator in server_config.app_configurators:
configurator(app, registry, server_config.settings)
return app

View File

@@ -1,20 +1,23 @@
from __future__ import annotations
from contextlib import asynccontextmanager
from importlib import import_module
from fastapi import Depends, FastAPI
from govoplan_core.auth.dependencies import ApiPrincipal, require_scope
from govoplan_access.backend.auth.dependencies import ApiPrincipal, require_scope
from govoplan_core.core.registry import PlatformRegistry
from govoplan_core.db.bootstrap import bootstrap_dev_data, create_all_tables
from govoplan_core.db.session import get_database
from govoplan_core.server.config import GovoplanServerConfig, RouterContribution
from govoplan_core.server.config import GovoplanServerConfig
from govoplan_core.settings import Settings, settings
@asynccontextmanager
async def lifespan(app: FastAPI):
if settings.app_env.lower() == "dev" and settings.dev_auto_migrate_enabled:
from govoplan_core.db.migrations import migrate_database
migrate_database(database_url=settings.database_url)
if settings.app_env.lower() == "dev" and settings.dev_bootstrap_enabled:
create_all_tables()
with get_database().SessionLocal() as session:
@@ -30,22 +33,6 @@ def _cors_origins(value: str) -> list[str]:
return [item.strip() for item in value.split(",") if item.strip()]
def _settings_module_enabled(module_id: str) -> bool:
return module_id in {item.strip() for item in settings.enabled_modules.split(",") if item.strip()}
def _dev_mail_enabled(config_settings: object | None, registry: PlatformRegistry) -> bool:
active_settings = config_settings if isinstance(config_settings, Settings) else settings
return active_settings.app_env == "dev" and active_settings.dev_mailbox_api_enabled and registry.has_module("mail")
def _dev_mail_router_contributions() -> tuple[RouterContribution, ...]:
if not (settings.app_env == "dev" and settings.dev_mailbox_api_enabled and _settings_module_enabled("mail")):
return ()
module = import_module("govoplan_mail.backend.dev_router")
return (RouterContribution(module.router, required_module="mail", enabled=_dev_mail_enabled),)
def register_health_details(app: FastAPI, registry: PlatformRegistry, config_settings: object | None) -> None:
active_settings = config_settings if isinstance(config_settings, Settings) else settings
@@ -70,19 +57,12 @@ def register_health_details(app: FastAPI, registry: PlatformRegistry, config_set
def get_server_config() -> GovoplanServerConfig:
from govoplan_core.api.v1.admin import router as admin_router
from govoplan_core.api.v1.audit import router as audit_router
from govoplan_core.api.v1.auth import router as auth_router
from govoplan_core.api.v1.system import router as system_router
return GovoplanServerConfig(
title="GovOPlaN Server",
version="0.3.0",
settings=settings,
enabled_modules=settings.enabled_modules,
api_prefix="/api/v1",
base_routers=(auth_router, admin_router, audit_router, system_router),
extra_routers=_dev_mail_router_contributions(),
lifespan=lifespan,
cors_origins=_cors_origins(settings.cors_origins),
app_configurators=(register_health_details,),

View File

@@ -1,9 +1,12 @@
from __future__ import annotations
from fastapi import APIRouter, HTTPException, Request
from sqlalchemy.exc import SQLAlchemyError
from govoplan_core.core.maintenance import saved_maintenance_mode
from govoplan_core.core.modules import FrontendModule, FrontendRoute, NavItem
from govoplan_core.core.registry import PlatformRegistry
from govoplan_core.db.session import get_database
def _registry(request: Request) -> PlatformRegistry:
@@ -42,6 +45,10 @@ def _frontend_payload(frontend: FrontendModule | None) -> dict[str, object] | No
"module_id": frontend.module_id,
"package_name": frontend.package_name,
"asset_manifest": frontend.asset_manifest,
"asset_manifest_signature": frontend.asset_manifest_signature,
"asset_manifest_public_key_id": frontend.asset_manifest_public_key_id,
"asset_manifest_integrity": frontend.asset_manifest_integrity,
"asset_manifest_contract_version": frontend.asset_manifest_contract_version,
"routes": [_frontend_route_payload(route) for route in frontend.routes],
"nav": [_nav_item_payload(item) for item in frontend.nav_items],
"settings_routes": [_frontend_route_payload(route) for route in frontend.settings_routes],
@@ -60,6 +67,17 @@ def _runtime_ui_capabilities(manifest_id: str, settings: object | None, registry
def create_platform_router(settings: object | None = None) -> APIRouter:
router = APIRouter(prefix="/platform", tags=["platform"])
@router.get("/status")
def status():
try:
with get_database().session() as session:
maintenance_mode = saved_maintenance_mode(session)
except (RuntimeError, SQLAlchemyError):
maintenance_mode = None
return {
"maintenance_mode": maintenance_mode.as_dict() if maintenance_mode is not None else {"enabled": False, "message": None},
}
@router.get("/modules")
def modules(request: Request):
registry = _registry(request)

View File

@@ -2,10 +2,11 @@ from __future__ import annotations
from collections.abc import Callable, Iterable, Sequence
from govoplan_core.access.manifest import get_manifest as get_access_manifest
from govoplan_access.backend.manifest import get_manifest as get_access_manifest
from govoplan_core.core.discovery import discover_module_manifests
from govoplan_core.core.modules import ModuleManifest
from govoplan_core.core.registry import PlatformRegistry, RegistryError
from govoplan_tenancy.backend.manifest import get_manifest as get_tenancy_manifest
ManifestFactory = Callable[[], ModuleManifest]
@@ -18,8 +19,20 @@ def parse_enabled_modules(value: str | Iterable[str]) -> list[str]:
return [item for item in items if item]
def available_module_manifests(manifest_factories: Sequence[ManifestFactory] = (), *, enabled_modules: Iterable[str] | None = None) -> dict[str, ModuleManifest]:
manifests = {manifest.id: manifest for manifest in discover_module_manifests(enabled_modules=enabled_modules)}
def available_module_manifests(
manifest_factories: Sequence[ManifestFactory] = (),
*,
enabled_modules: Iterable[str] | None = None,
ignore_load_errors: bool = False,
) -> dict[str, ModuleManifest]:
manifests = {
manifest.id: manifest
for manifest in discover_module_manifests(
enabled_modules=enabled_modules,
ignore_load_errors=ignore_load_errors,
)
}
manifests.setdefault("tenancy", get_tenancy_manifest())
manifests.setdefault("access", get_access_manifest())
for factory in manifest_factories:
manifest = factory()
@@ -29,8 +42,15 @@ def available_module_manifests(manifest_factories: Sequence[ManifestFactory] = (
def build_platform_registry(enabled_modules: str | Iterable[str], *, manifest_factories: Sequence[ManifestFactory] = ()) -> PlatformRegistry:
requested = parse_enabled_modules(enabled_modules)
if "tenancy" not in requested:
requested.insert(0, "tenancy")
if "access" not in requested:
requested.insert(0, "access")
tenancy_index = requested.index("tenancy")
requested.insert(tenancy_index + 1, "access")
elif requested.index("access") < requested.index("tenancy"):
requested.remove("access")
tenancy_index = requested.index("tenancy")
requested.insert(tenancy_index + 1, "access")
available = available_module_manifests(manifest_factories, enabled_modules=requested)
registry = PlatformRegistry()