from datetime import datetime from typing import Literal, Optional from pydantic import BaseModel, ConfigDict, Field, field_validator from app.schemas.storage import StorageOut class MySQLSourceConfig(BaseModel): host: str = Field(..., min_length=1) port: int = Field(3306, ge=1, le=65535) user: str = Field(..., min_length=1) password: str = Field("", description="可空(trust auth)") database: str = Field(..., min_length=1, description="要备份的库名") extra_args: str = Field("", description="额外 mysqldump 参数,如 '--skip-lock-tables'") class DirectorySourceConfig(BaseModel): path: str = Field(..., min_length=1, description="要备份的目录绝对路径") exclude_patterns: list[str] = Field( default_factory=lambda: [".DS_Store", "__pycache__", "node_modules", ".git"] ) class JobBase(BaseModel): name: str = Field(..., min_length=1, max_length=64) type: Literal["mysql", "directory"] source_config: dict storage_id: int cron_expression: Optional[str] = Field(None, description="标准 5 段 cron 表达式,空表示手动") enabled: bool = True retention_count: int = Field(7, ge=1, le=365) retention_days: Optional[int] = Field(None, ge=1, le=3650) description: Optional[str] = Field(None, max_length=255) @field_validator("cron_expression") @classmethod def _check_cron(cls, v: Optional[str]) -> Optional[str]: if v is None or v.strip() == "": return None v = v.strip() parts = v.split() if len(parts) != 5: raise ValueError("cron 必须是 5 段:分 时 日 月 周") return v class JobCreate(JobBase): pass class JobUpdate(BaseModel): name: Optional[str] = Field(None, min_length=1, max_length=64) source_config: Optional[dict] = None storage_id: Optional[int] = None cron_expression: Optional[str] = None enabled: Optional[bool] = None retention_count: Optional[int] = Field(None, ge=1, le=365) retention_days: Optional[int] = Field(None, ge=1, le=3650) description: Optional[str] = None class JobOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int name: str type: str cron_expression: Optional[str] enabled: bool retention_count: int retention_days: Optional[int] description: Optional[str] storage: StorageOut created_at: datetime updated_at: datetime class RunSummary(BaseModel): model_config = ConfigDict(from_attributes=True) id: int status: str trigger: str started_at: Optional[datetime] finished_at: Optional[datetime] duration_seconds: Optional[int] artifact_size: Optional[int] error_message: Optional[str] class JobWithLastRun(JobOut): last_run: Optional[RunSummary] = None class JobRunResponse(BaseModel): run_id: int status: str