Files
FitTrack2/FitnessSync/backend/src/api/scheduling.py
2026-01-09 09:59:36 -08:00

132 lines
4.8 KiB
Python

from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel
from sqlalchemy.orm import Session
from typing import List, Optional
from datetime import datetime, timedelta
import json
import logging
from ..models.scheduled_job import ScheduledJob
from ..services.postgresql_manager import PostgreSQLManager
from ..utils.config import config
from ..services.scheduler import scheduler
router = APIRouter()
logger = logging.getLogger(__name__)
def get_db():
db_manager = PostgreSQLManager(config.DATABASE_URL)
with db_manager.get_db_session() as session:
yield session
class ScheduledJobResponse(BaseModel):
id: int
job_type: str
name: str
interval_minutes: int
enabled: bool
last_run: Optional[datetime]
next_run: Optional[datetime]
params: Optional[str]
class Config:
from_attributes = True
class JobUpdateRequest(BaseModel):
interval_minutes: Optional[int] = None
enabled: Optional[bool] = None
params: Optional[dict] = None
@router.get("/scheduling/jobs", response_model=List[ScheduledJobResponse])
def list_scheduled_jobs(db: Session = Depends(get_db)):
"""List all scheduled jobs."""
jobs = db.query(ScheduledJob).order_by(ScheduledJob.id).all()
return jobs
@router.put("/scheduling/jobs/{job_id}", response_model=ScheduledJobResponse)
def update_scheduled_job(job_id: int, request: JobUpdateRequest, db: Session = Depends(get_db)):
"""Update a scheduled job's interval or enabled status."""
job = db.query(ScheduledJob).filter(ScheduledJob.id == job_id).first()
if not job:
raise HTTPException(status_code=404, detail="Job not found")
if request.interval_minutes is not None:
if request.interval_minutes < 1:
raise HTTPException(status_code=400, detail="Interval must be at least 1 minute")
job.interval_minutes = request.interval_minutes
# If enabled, update next_run based on new interval if it's far in future?
# Actually, standard behavior: next_run should be recalculated from last_run + new interval
# OR just leave it. If we shorten it, we might want it to run sooner.
# Let's recalculate next_run if it exists.
if job.last_run:
job.next_run = job.last_run + timedelta(minutes=job.interval_minutes)
else:
# If never run, next_run should be Now if enabled?
# Or keep existing next_run?
# If next_run is null and enabled, scheduler picks it up immediately.
pass
if request.enabled is not None:
job.enabled = request.enabled
if job.enabled and job.next_run is None:
# If re-enabling and no next run, set to now
job.next_run = datetime.now()
if request.params is not None:
job.params = json.dumps(request.params)
db.commit()
db.refresh(job)
return job
class JobCreateRequest(BaseModel):
job_type: str
name: str
interval_minutes: int
params: Optional[dict] = {}
enabled: Optional[bool] = True
@router.post("/scheduling/jobs", response_model=ScheduledJobResponse)
def create_scheduled_job(request: JobCreateRequest, db: Session = Depends(get_db)):
"""Create a new scheduled job."""
# Validate job_type
from ..services.scheduler import scheduler
if request.job_type not in scheduler.TASK_MAP:
raise HTTPException(status_code=400, detail=f"Invalid job_type. Must be one of: {list(scheduler.TASK_MAP.keys())}")
new_job = ScheduledJob(
job_type=request.job_type,
name=request.name,
interval_minutes=request.interval_minutes,
params=json.dumps(request.params) if request.params else "{}",
enabled=request.enabled,
next_run=datetime.now() if request.enabled else None
)
try:
db.add(new_job)
db.commit()
db.refresh(new_job)
return new_job
except Exception as e:
db.rollback()
logger.error(f"Failed to create job: {e}")
# Check for unique constraint on job_type if we enforced it?
# The model has job_type unique=True. This might be a problem if we want multiple of same type?
# User wants "new scheduled tasks" with "variables" -> implies multiple of same type (e.g. sync fitbit 10 days vs 30 days).
# We need to remove unique=True from ScheduledJob model if it exists!
raise HTTPException(status_code=400, detail=f"Failed to create job: {str(e)}")
@router.delete("/scheduling/jobs/{job_id}", status_code=204)
def delete_scheduled_job(job_id: int, db: Session = Depends(get_db)):
"""Delete a scheduled job."""
job = db.query(ScheduledJob).filter(ScheduledJob.id == job_id).first()
if not job:
raise HTTPException(status_code=404, detail="Job not found")
db.delete(job)
db.commit()
return None