intermittent commit

This commit is contained in:
2026-07-14 13:22:10 +02:00
parent 11ecf362a3
commit c9e1fb287f
6 changed files with 101 additions and 160 deletions

View File

@@ -59,6 +59,12 @@ from govoplan_core.core.module_installer import (
read_module_installer_request,
retry_module_installer_request,
)
from govoplan_core.core.module_installer_notifications import (
emit_module_installer_notification,
installer_notification_body,
installer_notification_priority,
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.maintenance import MAINTENANCE_ACCESS_SCOPE, MaintenanceMode, saved_maintenance_mode, save_maintenance_mode
@@ -147,6 +153,11 @@ def _request_registry(request: Request) -> PlatformRegistry:
return registry
def _optional_request_registry(request: Request) -> PlatformRegistry | None:
registry = getattr(request.app.state, "govoplan_registry", None)
return registry if isinstance(registry, PlatformRegistry) else None
def _request_lifecycle(request: Request) -> ModuleLifecycleManager:
lifecycle = getattr(request.app.state, "govoplan_lifecycle", None)
if not isinstance(lifecycle, ModuleLifecycleManager):
@@ -154,6 +165,29 @@ def _request_lifecycle(request: Request) -> ModuleLifecycleManager:
return lifecycle
def _emit_installer_request_notification(
*,
session: Session,
registry: PlatformRegistry | None,
tenant_id: str | None,
request: dict[str, object],
event_kind: str,
recipient_id: str | None = None,
) -> None:
status_value = str(request.get("status") or event_kind.rsplit(".", 1)[-1])
emit_module_installer_notification(
session=session,
registry=registry,
tenant_id=tenant_id,
request=request,
event_kind=event_kind,
subject=installer_notification_subject(event_kind, request),
body_text=installer_notification_body(event_kind, request),
recipient_id=recipient_id,
priority=installer_notification_priority(status_value),
)
def _http_admin_error(exc: Exception) -> HTTPException:
if isinstance(exc, AdminConflictError):
return HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc))
@@ -663,6 +697,7 @@ def read_module_install_request_detail(
@router.post("/system/modules/install-requests", response_model=ModuleInstallerRequestItem)
def create_module_install_request(
payload: ModuleInstallerRequestCreateRequest,
request_context: Request,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_scope("system:settings:write")),
):
@@ -675,8 +710,17 @@ def create_module_install_request(
request = queue_module_installer_request(
runtime_dir=runtime_dir,
requested_by=principal.user.id,
tenant_id=principal.tenant_id,
options=payload.options.model_dump(exclude_none=True),
)
_emit_installer_request_notification(
session=session,
registry=_optional_request_registry(request_context),
tenant_id=principal.tenant_id,
request=request,
event_kind="module_installer.request.queued",
recipient_id=principal.user.id,
)
audit_from_principal(
session,
principal,
@@ -698,6 +742,7 @@ def create_module_install_request(
@router.post("/system/modules/install-requests/{request_id}/cancel", response_model=ModuleInstallerRequestItem)
def cancel_module_install_request(
request_id: str,
request_context: Request,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_scope("system:settings:write")),
):
@@ -715,6 +760,14 @@ def cancel_module_install_request(
)
except ModuleInstallerError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
_emit_installer_request_notification(
session=session,
registry=_optional_request_registry(request_context),
tenant_id=str(request.get("tenant_id") or principal.tenant_id),
request=request,
event_kind="module_installer.request.cancelled",
recipient_id=str(request.get("requested_by") or principal.user.id),
)
audit_from_principal(
session,
principal,
@@ -736,6 +789,7 @@ def cancel_module_install_request(
@router.post("/system/modules/install-requests/{request_id}/retry", response_model=ModuleInstallerRequestItem)
def retry_module_install_request(
request_id: str,
request_context: Request,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_scope("system:settings:write")),
):
@@ -753,6 +807,14 @@ def retry_module_install_request(
)
except ModuleInstallerError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(exc)) from exc
_emit_installer_request_notification(
session=session,
registry=_optional_request_registry(request_context),
tenant_id=str(request.get("tenant_id") or principal.tenant_id),
request=request,
event_kind="module_installer.request.queued",
recipient_id=principal.user.id,
)
audit_from_principal(
session,
principal,

View File

@@ -3,61 +3,10 @@ from __future__ import annotations
from datetime import datetime
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator
from pydantic import BaseModel, ConfigDict, Field
from govoplan_core.api.v1.schemas import DeltaDeletedItem
RETENTION_DAY_KEYS = (
"raw_campaign_json_retention_days",
"generated_eml_retention_days",
"stored_report_detail_retention_days",
"mock_mailbox_retention_days",
"audit_detail_retention_days",
)
RETENTION_POLICY_FIELD_KEYS = (
"store_raw_campaign_json",
*RETENTION_DAY_KEYS,
"audit_detail_level",
)
def default_allow_lower_level_limits() -> dict[str, bool]:
return {key: True for key in RETENTION_POLICY_FIELD_KEYS}
def normalize_allow_lower_level_limits(value: Any, *, fill_defaults: bool) -> dict[str, bool] | None:
if value in (None, ""):
return default_allow_lower_level_limits() if fill_defaults else None
if not isinstance(value, dict):
raise ValueError("allow_lower_level_limits must be an object")
normalized = default_allow_lower_level_limits() if fill_defaults else {}
for key, allowed in value.items():
clean_key = str(key)
if clean_key not in RETENTION_POLICY_FIELD_KEYS:
raise ValueError(f"Unknown retention policy field: {clean_key}")
normalized[clean_key] = bool(allowed)
return normalized
class PrivacyRetentionPolicyItem(BaseModel):
model_config = ConfigDict(extra="forbid")
store_raw_campaign_json: bool = True
raw_campaign_json_retention_days: int | None = Field(default=None, ge=0)
generated_eml_retention_days: int | None = Field(default=None, ge=0)
stored_report_detail_retention_days: int | None = Field(default=None, ge=0)
mock_mailbox_retention_days: int | None = Field(default=None, ge=0)
audit_detail_retention_days: int | None = Field(default=None, ge=0)
audit_detail_level: Literal["full", "redacted", "minimal"] = "full"
allow_lower_level_limits: dict[str, bool] = Field(default_factory=default_allow_lower_level_limits)
@field_validator("allow_lower_level_limits", mode="before")
@classmethod
def _normalize_allow_lower_level_limits(cls, value: Any) -> Any:
return normalize_allow_lower_level_limits(value, fill_defaults=True)
from govoplan_core.privacy.schemas import PrivacyRetentionPolicyItem
class MaintenanceModeItem(BaseModel):