TaskResponse declared created_at and updated_at as str, but both are
timestamp columns and psycopg2 returns datetime objects. Pydantic
rejected every response, so the endpoint raised ResponseValidationError
after the INSERT had already committed.
Every task creation therefore looked like a failure, and the natural
retry failed again with a genuine duplicate-key violation, making it
appear the first attempt had done nothing.
Declaring them as datetime leaves the JSON on the wire unchanged
(FastAPI serialises to ISO 8601) and matches what GET /tasks/{task_name}
already returned.
Co-Authored-By: Claude <noreply@anthropic.com>
217 lines
6.5 KiB
Python
217 lines
6.5 KiB
Python
"""
|
|
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
|