Expose public module catalog discovery

This commit is contained in:
2026-08-06 21:13:13 +02:00
parent 218f94fa23
commit 1e55a80d3c
7 changed files with 79 additions and 12 deletions
+43 -5
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
from collections.abc import Mapping
import os
from pathlib import Path
from typing import Any
@@ -74,7 +75,11 @@ from govoplan_core.core.module_installer_notifications import (
installer_notification_subject,
)
from govoplan_core.core.module_license import module_license_decision, module_license_diagnostics
from govoplan_core.core.module_package_catalog import record_module_package_catalog_acceptance, validate_module_package_catalog
from govoplan_core.core.module_package_catalog import (
record_module_package_catalog_acceptance,
validate_module_package_catalog,
validate_official_module_package_catalog,
)
from govoplan_core.core.maintenance import MAINTENANCE_ACCESS_SCOPE, MaintenanceMode, saved_maintenance_mode, save_maintenance_mode
from govoplan_core.core.lifecycle import ModuleLifecycleManager
from govoplan_core.core.registry import PlatformRegistry
@@ -589,7 +594,7 @@ def _catalog_plan_item(
*,
validation: dict[str, object] | None = None,
) -> tuple[ModuleInstallPlanItem, dict[str, object]]:
result = validation or validate_module_package_catalog()
result = validation or _module_package_catalog_validation()
_require_valid_module_catalog(result)
raw_item = _catalog_install_or_update_item(result, module_id)
action = _catalog_plan_action(raw_item, module_id, available_module_ids)
@@ -604,6 +609,7 @@ def _catalog_plan_item(
python_ref=_catalog_string(raw_item, "python_ref"),
webui_package=_catalog_string(raw_item, "webui_package"),
webui_ref=_catalog_string(raw_item, "webui_ref"),
artifact_integrity=_catalog_mapping(raw_item, "artifact_integrity"),
notes=_catalog_plan_notes(raw_item, license_decision),
), result
@@ -616,6 +622,13 @@ def _require_valid_module_catalog(result: dict[str, object]) -> None:
)
def _module_package_catalog_validation() -> dict[str, object]:
configured = validate_module_package_catalog()
if configured.get("configured") or configured.get("error"):
return configured
return validate_official_module_package_catalog()
def _catalog_install_or_update_item(result: dict[str, object], module_id: str) -> dict[str, object]:
for raw_item in result.get("modules", []):
if not isinstance(raw_item, dict):
@@ -659,6 +672,11 @@ def _catalog_string(raw_item: dict[str, object], field: str) -> str | None:
return value if isinstance(value, str) else None
def _catalog_mapping(raw_item: dict[str, object], field: str) -> dict[str, object] | None:
value = raw_item.get(field)
return dict(value) if isinstance(value, Mapping) else None
def _catalog_plan_metadata(validation: dict[str, object]) -> dict[str, object]:
metadata: dict[str, object] = {
"source": validation.get("source") or validation.get("path"),
@@ -688,8 +706,21 @@ def _catalog_license_features(raw_item: dict[str, object]) -> list[str]:
return [str(item).strip() for item in value if str(item).strip()]
def _module_package_catalog_item(raw_item: dict[str, object]) -> ModulePackageCatalogItem:
def _module_package_catalog_item(
raw_item: dict[str, object],
available: Mapping[str, Any],
) -> ModulePackageCatalogItem:
payload = dict(raw_item)
manifest = available.get(str(raw_item.get("module_id") or ""))
installed_version = str(getattr(manifest, "version", "") or "") or None
target_version = raw_item.get("version")
payload["installed"] = manifest is not None
payload["installed_version"] = installed_version
payload["update_available"] = bool(
installed_version
and isinstance(target_version, str)
and target_version != installed_version
)
decision = module_license_decision(_catalog_license_features(raw_item))
payload["license_allowed"] = bool(decision.get("allowed"))
payload["license_enforced"] = bool(decision.get("enforced"))
@@ -1218,13 +1249,20 @@ def retry_module_install_request(
@router.get("/system/modules/package-catalog", response_model=ModulePackageCatalogResponse)
def read_module_package_catalog(
request: Request,
principal: ApiPrincipal = Depends(require_scope("system:settings:read")),
):
del principal
result = validate_module_package_catalog()
result = _module_package_catalog_validation()
lifecycle = getattr(request.app.state, "govoplan_lifecycle", None)
available = (
dict(lifecycle.available_modules)
if isinstance(lifecycle, ModuleLifecycleManager)
else available_module_manifests(ignore_load_errors=True)
)
license_diagnostics = module_license_diagnostics(required_features=_catalog_required_license_features(result))
return ModulePackageCatalogResponse(
modules=[_module_package_catalog_item(item) for item in result["modules"] if isinstance(item, dict)],
modules=[_module_package_catalog_item(item, available) for item in result["modules"] if isinstance(item, dict)],
configured=bool(result["configured"]),
valid=bool(result["valid"]),
path=result["path"] if isinstance(result["path"], str) else None,
@@ -393,6 +393,9 @@ class ModulePackageCatalogItem(BaseModel):
description: str | None = None
version: str | None = None
action: Literal["install", "update", "uninstall"] = "install"
installed: bool = False
installed_version: str | None = None
update_available: bool = False
dependencies: list[str] = Field(default_factory=list)
optional_dependencies: list[str] = Field(default_factory=list)
migration_safety: Literal["automatic", "requires_review", "forward_only", "destructive"] = "automatic"