"""Validated write contracts for local cloud-resource governance.""" from __future__ import annotations import uuid from datetime import datetime from typing import Any, Literal from pydantic import BaseModel, Field, field_validator, model_validator GovernanceStatus = Literal["unreviewed", "in_review", "reviewed", "compliant", "exception_approved"] RiskLevel = Literal["low", "medium", "high", "critical"] Criticality = Literal["low", "medium", "high", "mission_critical"] DataClassification = Literal["public", "internal", "confidential", "restricted"] LifecycleStage = Literal["planned", "active", "deprecated", "retiring", "retired"] ComplianceStatus = Literal["unknown", "compliant", "non_compliant", "exception"] Severity = Literal["low", "medium", "high", "critical"] class GovernancePatch(BaseModel): version: int | None = Field(default=None, ge=1) business_owner_person_id: uuid.UUID | None = None technical_owner_person_id: uuid.UUID | None = None governance_status: GovernanceStatus | None = None criticality: Criticality | None = None data_classification: DataClassification | None = None risk_level: RiskLevel | None = None lifecycle_stage: LifecycleStage | None = None compliance_status: ComplianceStatus | None = None exception_reason: str | None = Field(default=None, max_length=4_000) exception_expires_at: datetime | None = None notes: str | None = Field(default=None, max_length=10_000) custom_fields: dict[str, Any] | None = None @field_validator("custom_fields") @classmethod def validate_custom_fields(cls, value: dict[str, Any] | None) -> dict[str, Any] | None: if value is not None and len(value) > 50: raise ValueError("custom_fields supports at most 50 keys") return value @field_validator("exception_expires_at") @classmethod def require_exception_timezone(cls, value: datetime | None) -> datetime | None: if value is not None and value.tzinfo is None: raise ValueError("exception_expires_at must include a timezone") return value @model_validator(mode="after") def validate_exception(self): if self.governance_status == "exception_approved" and not (self.exception_reason or "").strip(): raise ValueError("exception_reason is required for exception_approved") return self class GovernanceRuleCreate(BaseModel): rule_key: str = Field(pattern=r"^[a-z][a-z0-9_.-]{2,95}$") name: str = Field(min_length=2, max_length=192) description: str | None = Field(default=None, max_length=4_000) category: str = Field(min_length=2, max_length=64) severity: Severity evaluator: str = Field(min_length=2, max_length=64) applies_to: dict[str, list[str]] | None = None parameters: dict[str, Any] | None = None recommendation: str | None = Field(default=None, max_length=4_000) enabled: bool = True class GovernanceRulePatch(BaseModel): version: int = Field(ge=1) name: str | None = Field(default=None, min_length=2, max_length=192) description: str | None = Field(default=None, max_length=4_000) category: str | None = Field(default=None, min_length=2, max_length=64) severity: Severity | None = None applies_to: dict[str, list[str]] | None = None parameters: dict[str, Any] | None = None recommendation: str | None = Field(default=None, max_length=4_000) enabled: bool | None = None class FindingAction(BaseModel): action: Literal["acknowledge", "reopen", "suppress"] assigned_to_person_id: uuid.UUID | None = None due_at: datetime | None = None note: str | None = Field(default=None, max_length=4_000) @field_validator("due_at") @classmethod def require_due_timezone(cls, value: datetime | None) -> datetime | None: if value is not None and value.tzinfo is None: raise ValueError("due_at must include a timezone") return value