""" Pydantic models for The Scheduler API. """ from pydantic import BaseModel, Field from typing import Optional, Dict, Any from datetime import datetime from enum import Enum class SchedulePattern(str, Enum): """Common schedule patterns.""" EVERY_MINUTE = "every_minute" HOURLY = "hourly" DAILY = "daily" WEEKLY = "weekly" MONTHLY = "monthly" class TaskExecutor(str, Enum): """Available task executors.""" REST_API = "rest_api" SHELL = "shell" PYTHON = "python" class TaskPriorityLevel(str, Enum): """Task priority levels.""" EMERGENCY = "emergency" # 1-5: Critical system tasks SYSTEM = "system" # 5-10: System maintenance USER = "user" # 10-30: User-initiated tasks MAINTENANCE = "maintenance" # 40-70: Background maintenance LOW = "low" # 70+: Low priority cleanup class TaskBase(BaseModel): """Base task fields.""" task_name: str = Field( ..., description="Unique task identifier (e.g., 'librarian_consolidation')", example="librarian_consolidation" ) service: str = Field( ..., description="Service that owns this task (e.g., 'library-desk', 'core-api')", example="library-desk" ) executor: str = Field( ..., description="Executor type: 'rest_api', 'shell', or 'python'", example="rest_api" ) priority: int = Field( ..., ge=1, le=100, description="Priority level (1-5: emergency, 10-30: user, 40-70: maintenance, 70+: low)", example=25 ) description: Optional[str] = Field( None, description="Human-readable task description", example="Processes unprocessed search queries and consolidates knowledge into wiki pages" ) class TaskSchedule(BaseModel): """Cron-style schedule fields.""" minute: int = Field( -1, ge=-1, le=59, description="Minute to run (-1 = every minute, 0-59 = specific minute)", example=-1 ) hour: int = Field( -1, ge=-1, le=23, description="Hour to run (-1 = every hour, 0-23 = specific hour)", example=-1 ) day_of_month: int = Field( -1, ge=-1, le=31, description="Day of month to run (-1 = every day, 1-31 = specific day)", example=-1 ) month: int = Field( -1, ge=-1, le=12, description="Month to run (-1 = every month, 1-12 = specific month)", example=-1 ) day_of_week: int = Field( -1, ge=-1, le=6, description="Day of week to run (-1 = every day, 0-6 = Monday-Sunday)", example=-1 ) class TaskConfig(BaseModel): """Task execution configuration.""" enabled: bool = Field( True, description="Whether task is enabled", example=True ) max_retries: int = Field( 3, ge=0, le=10, description="Maximum retry attempts on failure", example=3 ) timeout_seconds: int = Field( 3600, ge=1, description="Execution timeout in seconds", example=3600 ) config: Optional[Dict[str, Any]] = Field( None, description="Executor-specific configuration (varies by executor type)", example={ "method": "POST", "url": "http://library-desk:8089/consolidation/run", "headers": { "Authorization": "Bearer ${LIBRARY_DESK_API_KEY}", "Content-Type": "application/json" }, "body": { "dry_run": False, "process_limit": 10, "lookback_days": 7, "min_web_results": 2 } } ) class TaskCreate(TaskBase, TaskSchedule, TaskConfig): """Request model for creating a new scheduled task.""" created_by: Optional[str] = Field( "api", description="User or system that created this task", example="api" ) class Config: json_schema_extra = { "example": { "task_name": "librarian_consolidation", "service": "library-desk", "executor": "rest_api", "priority": 25, "description": "Processes unprocessed search queries and consolidates knowledge", "minute": 0, "hour": -1, "day_of_month": -1, "month": -1, "day_of_week": -1, "enabled": True, "max_retries": 3, "timeout_seconds": 3600, "config": { "method": "POST", "url": "http://library-desk:8089/consolidation/run", "headers": { "Authorization": "Bearer ${LIBRARY_DESK_API_KEY}", "Content-Type": "application/json" }, "body": { "dry_run": False, "process_limit": 10, "lookback_days": 7, "min_web_results": 2 } }, "created_by": "api" } } class TaskUpdate(BaseModel): """Request model for updating a scheduled task.""" service: Optional[str] = None executor: Optional[str] = None priority: Optional[int] = Field(None, ge=1, le=100) minute: Optional[int] = Field(None, ge=-1, le=59) hour: Optional[int] = Field(None, ge=-1, le=23) day_of_month: Optional[int] = Field(None, ge=-1, le=31) month: Optional[int] = Field(None, ge=-1, le=12) day_of_week: Optional[int] = Field(None, ge=-1, le=6) enabled: Optional[bool] = None description: Optional[str] = None config: Optional[Dict[str, Any]] = None max_retries: Optional[int] = Field(None, ge=0, le=10) timeout_seconds: Optional[int] = Field(None, ge=1) class TaskResponse(TaskCreate): """Response model for task operations.""" # These are `timestamp` columns, so psycopg2 hands back datetime objects. # Declaring them as `str` made Pydantic reject every create response, which # 500'd the endpoint *after* the row had already been inserted and committed. # FastAPI serialises datetime to an ISO 8601 string, so the JSON on the wire # is unchanged — and now matches what GET /tasks/{name} already returned. created_at: datetime updated_at: Optional[datetime] = None class Config: from_attributes = True