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) 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 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) 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" 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