116 lines
4.3 KiB
Python
116 lines
4.3 KiB
Python
"""Maintenance schedule CRUD endpoints."""
|
|
from typing import Annotated
|
|
from uuid import UUID
|
|
from datetime import datetime, timezone
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from croniter import croniter
|
|
import pytz
|
|
|
|
from app.api.deps import get_current_active_user, get_db
|
|
from app.models.maintenance_schedule import MaintenanceSchedule
|
|
from app.models.user import User
|
|
from app.schemas.maintenance_schedule import (
|
|
MaintenanceScheduleCreate,
|
|
MaintenanceScheduleUpdate,
|
|
MaintenanceScheduleResponse,
|
|
)
|
|
|
|
router = APIRouter(prefix="/maintenance-schedules", tags=["maintenance-schedules"])
|
|
|
|
|
|
def _compute_next_run(cron_expression: str, tz_name: str) -> datetime:
|
|
"""Compute next run time from cron expression, returned as UTC."""
|
|
tz = pytz.timezone(tz_name)
|
|
now = datetime.now(tz)
|
|
cron = croniter(cron_expression, now)
|
|
return cron.get_next(datetime).astimezone(timezone.utc)
|
|
|
|
|
|
@router.post("", response_model=MaintenanceScheduleResponse, status_code=201)
|
|
async def create_schedule(
|
|
data: MaintenanceScheduleCreate,
|
|
current_user: Annotated[User, Depends(get_current_active_user)],
|
|
db: Annotated[AsyncSession, Depends(get_db)],
|
|
):
|
|
"""Create a cron schedule for a maintenance flow. One per flow."""
|
|
# Check no existing schedule for this tree
|
|
existing = await db.execute(
|
|
select(MaintenanceSchedule).where(MaintenanceSchedule.tree_id == data.tree_id)
|
|
)
|
|
if existing.scalar_one_or_none():
|
|
raise HTTPException(status_code=409, detail="Schedule already exists for this tree")
|
|
|
|
try:
|
|
next_run = _compute_next_run(data.cron_expression, data.timezone)
|
|
except (ValueError, KeyError) as e:
|
|
raise HTTPException(status_code=422, detail=f"Invalid cron expression or timezone: {e}")
|
|
|
|
schedule = MaintenanceSchedule(
|
|
tree_id=data.tree_id,
|
|
created_by=current_user.id,
|
|
cron_expression=data.cron_expression,
|
|
timezone=data.timezone,
|
|
target_list_id=data.target_list_id,
|
|
is_active=True,
|
|
next_run_at=next_run,
|
|
)
|
|
db.add(schedule)
|
|
await db.commit()
|
|
await db.refresh(schedule)
|
|
return schedule
|
|
|
|
|
|
@router.get("/tree/{tree_id}", response_model=MaintenanceScheduleResponse)
|
|
async def get_schedule_for_tree(
|
|
tree_id: UUID,
|
|
current_user: Annotated[User, Depends(get_current_active_user)],
|
|
db: Annotated[AsyncSession, Depends(get_db)],
|
|
):
|
|
"""Get the schedule for a specific maintenance flow."""
|
|
result = await db.execute(
|
|
select(MaintenanceSchedule).where(MaintenanceSchedule.tree_id == tree_id)
|
|
)
|
|
schedule = result.scalar_one_or_none()
|
|
if not schedule:
|
|
raise HTTPException(status_code=404, detail="No schedule found for this tree")
|
|
return schedule
|
|
|
|
|
|
@router.patch("/{schedule_id}", response_model=MaintenanceScheduleResponse)
|
|
async def update_schedule(
|
|
schedule_id: UUID,
|
|
data: MaintenanceScheduleUpdate,
|
|
current_user: Annotated[User, Depends(get_current_active_user)],
|
|
db: Annotated[AsyncSession, Depends(get_db)],
|
|
):
|
|
"""Update a schedule (disable, change cron, change timezone, change target list)."""
|
|
result = await db.execute(
|
|
select(MaintenanceSchedule).where(MaintenanceSchedule.id == schedule_id)
|
|
)
|
|
schedule = result.scalar_one_or_none()
|
|
if not schedule:
|
|
raise HTTPException(status_code=404, detail="Schedule not found")
|
|
|
|
update_fields = data.model_fields_set
|
|
if "cron_expression" in update_fields and data.cron_expression is not None:
|
|
schedule.cron_expression = data.cron_expression
|
|
if "timezone" in update_fields and data.timezone is not None:
|
|
schedule.timezone = data.timezone
|
|
if "target_list_id" in update_fields:
|
|
schedule.target_list_id = data.target_list_id
|
|
if "is_active" in update_fields and data.is_active is not None:
|
|
schedule.is_active = data.is_active
|
|
|
|
# Recompute next_run_at if schedule timing changed
|
|
if "cron_expression" in update_fields or "timezone" in update_fields:
|
|
try:
|
|
schedule.next_run_at = _compute_next_run(schedule.cron_expression, schedule.timezone)
|
|
except (ValueError, KeyError) as e:
|
|
raise HTTPException(status_code=422, detail=f"Invalid cron expression or timezone: {e}")
|
|
|
|
await db.commit()
|
|
await db.refresh(schedule)
|
|
return schedule
|