Files
govoplan-admin/src/govoplan_admin/backend/api/v1/schemas.py
2026-07-11 02:34:55 +02:00

523 lines
18 KiB
Python

from __future__ import annotations
from datetime import datetime
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator
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)
class MaintenanceModeItem(BaseModel):
model_config = ConfigDict(extra="forbid")
enabled: bool = False
message: str | None = Field(default=None, max_length=500)
class LanguagePackageItem(BaseModel):
model_config = ConfigDict(extra="forbid")
code: str = Field(min_length=1, max_length=20)
label: str = Field(min_length=1, max_length=100)
native_label: str | None = Field(default=None, max_length=100)
class AdminOverviewResponse(BaseModel):
active_tenant_id: str
active_tenant_name: str
tenant_count: int | None = None
system_account_count: int | None = None
system_group_template_count: int | None = None
system_role_template_count: int | None = None
user_count: int
active_user_count: int
group_count: int
role_count: int
active_api_key_count: int
capabilities: list[str] = Field(default_factory=list)
class SystemSettingsItem(BaseModel):
default_locale: str = "en"
allow_tenant_custom_groups: bool = True
allow_tenant_custom_roles: bool = True
allow_tenant_api_keys: bool = True
privacy_retention_policy: PrivacyRetentionPolicyItem = Field(default_factory=PrivacyRetentionPolicyItem)
maintenance_mode: MaintenanceModeItem = Field(default_factory=MaintenanceModeItem)
available_languages: list[LanguagePackageItem] = Field(default_factory=list)
enabled_language_codes: list[str] = Field(default_factory=list)
settings: dict[str, Any] = Field(default_factory=dict)
class SystemSettingsDeltaResponse(BaseModel):
item: SystemSettingsItem | None = None
sections: dict[str, Any] = Field(default_factory=dict)
changed_sections: list[str] = Field(default_factory=list)
deleted: list[DeltaDeletedItem] = Field(default_factory=list)
watermark: str | None = None
has_more: bool = False
full: bool = False
class SystemSettingsUpdateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
default_locale: str = Field(min_length=1, max_length=20)
allow_tenant_custom_groups: bool
allow_tenant_custom_roles: bool
allow_tenant_api_keys: bool
privacy_retention_policy: PrivacyRetentionPolicyItem | None = None
maintenance_mode: MaintenanceModeItem | None = None
available_languages: list[LanguagePackageItem] | None = None
enabled_language_codes: list[str] | None = None
change_request_id: str | None = None
class ModuleCatalogItem(BaseModel):
id: str
name: str
version: str
installed: bool = True
current_enabled: bool = False
desired_enabled: bool = False
protected: bool = False
restart_required: bool = False
dependencies: list[str] = Field(default_factory=list)
optional_dependencies: list[str] = Field(default_factory=list)
dependents: list[str] = Field(default_factory=list)
permissions_count: int = 0
role_templates_count: int = 0
route_contributed: bool = False
nav_count: int = 0
frontend_package: str | None = None
migration_module_id: str | None = None
migration_script_location: str | None = None
runtime_toggle_supported: bool = False
install_uninstall_supported: bool = False
class ModuleCatalogResponse(BaseModel):
modules: list[ModuleCatalogItem]
current_enabled: list[str]
desired_enabled: list[str]
configured_enabled: list[str]
protected_modules: list[str]
restart_required: bool = False
runtime_toggle_supported: bool = False
install_uninstall_supported: bool = False
install_plan_supported: bool = False
package_mutation_supported: bool = False
maintenance_mode: MaintenanceModeItem = Field(default_factory=MaintenanceModeItem)
notes: list[str] = Field(default_factory=list)
class ModuleStateUpdateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
enabled_modules: list[str] = Field(default_factory=list)
change_request_id: str | None = None
class ModuleInstallPlanItem(BaseModel):
model_config = ConfigDict(extra="forbid")
module_id: str = Field(min_length=1, max_length=120)
action: Literal["install", "update", "uninstall"]
source: Literal["manual", "catalog"] = "manual"
catalog: dict[str, Any] | None = None
python_package: str | None = Field(default=None, max_length=200)
python_ref: str | None = Field(default=None, max_length=1000)
webui_package: str | None = Field(default=None, max_length=200)
webui_ref: str | None = Field(default=None, max_length=1000)
data_safety_acknowledged: bool = False
destroy_data: bool = False
status: Literal["planned", "applied", "blocked"] = "planned"
notes: str | None = Field(default=None, max_length=1000)
class ModuleInstallPreflightIssue(BaseModel):
severity: Literal["blocker", "warning", "info"]
code: str
message: str
module_id: str | None = None
class ModuleInstallChecklistItem(BaseModel):
id: str
label: str
status: Literal["done", "pending", "blocked", "warning"]
detail: str | None = None
class ModuleInstallTargetItem(BaseModel):
module_id: str
action: Literal["install", "update", "uninstall"]
source: Literal["manual", "catalog"]
current_version: str | None = None
target_version: str | None = None
python_package: str | None = None
python_ref: str | None = None
webui_package: str | None = None
webui_ref: str | None = None
migration_safety: Literal["automatic", "requires_review", "forward_only", "destructive"] = "automatic"
migration_notes: str | None = None
current_version_min: str | None = None
current_version_max_exclusive: str | None = None
bridge_release: bool = False
bridge_notes: str | None = None
allow_downgrade: bool = False
allow_same_version: bool = False
recovery_tested: bool = False
recovery_notes: str | None = None
data_safety_acknowledged: bool = False
class ModuleMigrationPlanStep(BaseModel):
module_id: str
action: Literal["install", "update", "uninstall"]
phase: Literal["upgrade", "retirement"]
source: Literal["manifest", "catalog", "pending"]
has_migration_metadata: bool = False
metadata_pending: bool = False
migration_safety: Literal["automatic", "requires_review", "forward_only", "destructive"] = "automatic"
current_version: str | None = None
target_version: str | None = None
reason: str | None = None
class ModuleMigrationTaskPlanItem(BaseModel):
module_id: str
task_id: str
phase: Literal["pre_migration_check", "pre_migration_prepare", "post_migration_backfill", "post_migration_verify"]
summary: str
task_version: str = "1"
safety: Literal["automatic", "requires_review", "forward_only", "destructive"] = "automatic"
idempotent: bool = True
timeout_seconds: int | None = None
source: Literal["manifest", "catalog", "pending"] = "manifest"
has_executor: bool = False
metadata_pending: bool = False
class ModuleMigrationExecutionPlan(BaseModel):
enabled_modules: list[str] = Field(default_factory=list)
requires_database_migration: bool = False
steps: list[ModuleMigrationPlanStep] = Field(default_factory=list)
tasks: list[ModuleMigrationTaskPlanItem] = Field(default_factory=list)
class ModuleInstallPreflightResponse(BaseModel):
allowed: bool
maintenance_mode: bool
frontend_rebuild_required: bool
restart_required: bool
commands: list[str] = Field(default_factory=list)
rollback_commands: list[str] = Field(default_factory=list)
issues: list[ModuleInstallPreflightIssue] = Field(default_factory=list)
checklist: list[ModuleInstallChecklistItem] = Field(default_factory=list)
target_plan: list[ModuleInstallTargetItem] = Field(default_factory=list)
migration_plan: ModuleMigrationExecutionPlan = Field(default_factory=ModuleMigrationExecutionPlan)
class ModuleInstallPlanResponse(BaseModel):
items: list[ModuleInstallPlanItem] = Field(default_factory=list)
updated_at: str | None = None
commands: list[str] = Field(default_factory=list)
maintenance_mode: MaintenanceModeItem = Field(default_factory=MaintenanceModeItem)
preflight: ModuleInstallPreflightResponse | None = None
notes: list[str] = Field(default_factory=list)
class ModuleInstallerLockStatus(BaseModel):
model_config = ConfigDict(extra="allow")
locked: bool = False
path: str
class ModuleInstallerDaemonStatus(BaseModel):
model_config = ConfigDict(extra="allow")
running: bool = False
status: str = "unknown"
path: str
class ModuleInstallerRunSummary(BaseModel):
run_id: str
status: str
started_at: str | None = None
finished_at: str | None = None
request_id: str | None = None
requested_by: str | None = None
trace: dict[str, Any] | None = None
rollback_status: str | None = None
supervisor_status: str | None = None
error: str | None = None
record_path: str
commands_count: int = 0
planned_modules: list[str] = Field(default_factory=list)
class ModuleInstallerRunListResponse(BaseModel):
runs: list[ModuleInstallerRunSummary] = Field(default_factory=list)
lock: ModuleInstallerLockStatus
cursor: str | None = None
next_cursor: str | None = None
full: bool = False
class ModuleInstallerRequestOptions(BaseModel):
model_config = ConfigDict(extra="forbid")
build_webui: bool = False
migrate_database: bool = True
webui_root: str | None = Field(default=None, max_length=1000)
npm_bin: str | None = Field(default=None, max_length=500)
database_backup_command: str | None = Field(default=None, max_length=4000)
database_restore_command: str | None = Field(default=None, max_length=4000)
database_restore_check_command: str | None = Field(default=None, max_length=4000)
activate_installed_modules: bool = True
remove_uninstalled_modules_from_desired: bool = True
restart_commands: list[str] = Field(default_factory=list)
health_urls: list[str] = Field(default_factory=list)
health_timeout_seconds: float = Field(default=60.0, ge=1, le=3600)
health_interval_seconds: float = Field(default=2.0, ge=0.2, le=120)
class ModuleInstallerRequestItem(BaseModel):
model_config = ConfigDict(extra="allow")
request_id: str
status: str
created_at: str
requested_by: str | None = None
started_at: str | None = None
finished_at: str | None = None
cancelled_at: str | None = None
cancelled_by: str | None = None
retry_of: str | None = None
trace: dict[str, Any] | None = None
error: str | None = None
record_path: str | None = None
options: dict[str, Any] = Field(default_factory=dict)
class ModuleInstallerRequestListResponse(BaseModel):
requests: list[ModuleInstallerRequestItem] = Field(default_factory=list)
daemon: ModuleInstallerDaemonStatus
cursor: str | None = None
next_cursor: str | None = None
full: bool = False
class ModuleInstallerRequestCreateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
options: ModuleInstallerRequestOptions = Field(default_factory=ModuleInstallerRequestOptions)
class ModuleInterfaceProviderItem(BaseModel):
name: str
version: str
class ModuleInterfaceRequirementItem(BaseModel):
name: str
version_min: str | None = None
version_max_exclusive: str | None = None
optional: bool = False
class ModulePackageCatalogItem(BaseModel):
module_id: str
name: str
description: str | None = None
version: str | None = None
action: Literal["install", "update", "uninstall"] = "install"
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"
migration_notes: str | None = None
migration_after: list[str] = Field(default_factory=list)
migration_before: list[str] = Field(default_factory=list)
current_version_min: str | None = None
current_version_max_exclusive: str | None = None
bridge_release: bool = False
bridge_notes: str | None = None
allow_downgrade: bool = False
allow_same_version: bool = False
recovery_tested: bool = False
recovery_notes: str | None = None
python_package: str | None = None
python_ref: str | None = None
webui_package: str | None = None
webui_ref: str | None = None
provides_interfaces: list[ModuleInterfaceProviderItem] = Field(default_factory=list)
requires_interfaces: list[ModuleInterfaceRequirementItem] = Field(default_factory=list)
license_features: list[str] = Field(default_factory=list)
license_allowed: bool = True
license_enforced: bool = False
license_missing_features: list[str] = Field(default_factory=list)
license_reason: str | None = None
notes: str | None = None
tags: list[str] = Field(default_factory=list)
class ModuleLicenseDiagnostics(BaseModel):
configured: bool = False
valid: bool = True
path: str | None = None
license_id: str | None = None
subject: str | None = None
features: list[str] = Field(default_factory=list)
valid_from: str | None = None
valid_until: str | None = None
signed: bool = False
trusted: bool = False
key_id: str | None = None
enforced: bool = False
allowed: bool = True
required_features: list[str] = Field(default_factory=list)
missing_features: list[str] = Field(default_factory=list)
expires_in_days: int | None = None
reason: str | None = None
guidance: list[str] = Field(default_factory=list)
class ModulePackageCatalogResponse(BaseModel):
modules: list[ModulePackageCatalogItem] = Field(default_factory=list)
configured: bool = False
valid: bool = True
path: str | None = None
source: str | None = None
source_type: str | None = None
cache_used: bool = False
cache_path: str | None = None
channel: str | None = None
sequence: int | None = None
generated_at: str | None = None
not_before: str | None = None
expires_at: str | None = None
signed: bool = False
trusted: bool = False
key_id: str | None = None
license: ModuleLicenseDiagnostics = Field(default_factory=ModuleLicenseDiagnostics)
warnings: list[str] = Field(default_factory=list)
error: str | None = None
class ModuleInstallPlanUpdateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
items: list[ModuleInstallPlanItem] = Field(default_factory=list)
change_request_id: str | None = None
class GovernanceAssignment(BaseModel):
tenant_id: str
mode: Literal["available", "required"] = "available"
class GovernanceTemplateItem(BaseModel):
id: str
kind: Literal["group", "role"]
slug: str
name: str
description: str | None = None
permissions: list[str] = Field(default_factory=list)
effective_permission_count: int = 0
is_active: bool = True
assignments: list[GovernanceAssignment] = Field(default_factory=list)
created_at: datetime
updated_at: datetime
class GovernanceTemplateListResponse(BaseModel):
templates: list[GovernanceTemplateItem]
class GovernanceTemplateListDeltaResponse(BaseModel):
templates: list[GovernanceTemplateItem] = Field(default_factory=list)
deleted: list[DeltaDeletedItem] = Field(default_factory=list)
watermark: str | None = None
has_more: bool = False
full: bool = False
class GovernanceTemplateCreateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
kind: Literal["group", "role"]
slug: str
name: str = Field(min_length=1, max_length=255)
description: str | None = None
permissions: list[str] = Field(default_factory=list)
is_active: bool = True
assignments: list[GovernanceAssignment] = Field(default_factory=list)
change_request_id: str | None = None
class GovernanceTemplateUpdateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
name: str = Field(min_length=1, max_length=255)
description: str | None = None
permissions: list[str] = Field(default_factory=list)
is_active: bool = True
assignments: list[GovernanceAssignment] = Field(default_factory=list)
change_request_id: str | None = None