272 lines
9.3 KiB
Python
272 lines
9.3 KiB
Python
"""
|
|
Scheduled Jobs Management API
|
|
"""
|
|
import logging
|
|
import asyncio
|
|
from typing import List, Optional, Dict, Any
|
|
from datetime import datetime
|
|
from fastapi import APIRouter, HTTPException, Depends, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select
|
|
from pydantic import BaseModel
|
|
|
|
from app.database import get_db
|
|
from app.models.db.scheduled_job import ScheduledJob, JobType
|
|
from app.services.scheduler_service import scheduler_service
|
|
from app.services.code_validator import code_validator
|
|
from app.jobs.registry import task_registry
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/scheduled-jobs", tags=["scheduled-jobs"])
|
|
|
|
|
|
# Pydantic Models
|
|
class ScheduledJobBase(BaseModel):
|
|
name: str
|
|
cron_expression: str
|
|
description: Optional[str] = None
|
|
is_active: bool = True
|
|
|
|
|
|
class ScheduledJobCreatePredefined(ScheduledJobBase):
|
|
"""Create predefined task job"""
|
|
job_type: str = "predefined"
|
|
predefined_function: str
|
|
function_params: Optional[Dict[str, Any]] = {}
|
|
|
|
|
|
class ScheduledJobCreateCustomCode(ScheduledJobBase):
|
|
"""Create custom code job"""
|
|
job_type: str = "custom_code"
|
|
python_code: str
|
|
|
|
|
|
class ScheduledJobUpdate(BaseModel):
|
|
name: Optional[str] = None
|
|
cron_expression: Optional[str] = None
|
|
job_type: Optional[str] = None
|
|
predefined_function: Optional[str] = None
|
|
function_params: Optional[Dict[str, Any]] = None
|
|
python_code: Optional[str] = None
|
|
description: Optional[str] = None
|
|
is_active: Optional[bool] = None
|
|
|
|
|
|
class ScheduledJobResponse(BaseModel):
|
|
id: int
|
|
name: str
|
|
job_type: str
|
|
predefined_function: Optional[str] = None
|
|
function_params: Optional[Dict[str, Any]] = None
|
|
cron_expression: str
|
|
python_code: Optional[str] = None
|
|
is_active: bool
|
|
last_run_at: Optional[datetime] = None
|
|
last_run_status: Optional[str] = None
|
|
next_run_at: Optional[datetime] = None
|
|
description: Optional[str] = None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
@router.get("", response_model=List[ScheduledJobResponse])
|
|
async def get_scheduled_jobs(db: AsyncSession = Depends(get_db)):
|
|
"""Get all scheduled jobs"""
|
|
result = await db.execute(select(ScheduledJob).order_by(ScheduledJob.id))
|
|
return result.scalars().all()
|
|
|
|
|
|
@router.get("/available-tasks", response_model=List[Dict[str, Any]])
|
|
async def get_available_tasks():
|
|
"""Get list of all available predefined tasks"""
|
|
tasks = task_registry.list_tasks()
|
|
return tasks
|
|
|
|
|
|
@router.get("/{job_id}", response_model=ScheduledJobResponse)
|
|
async def get_scheduled_job(job_id: int, db: AsyncSession = Depends(get_db)):
|
|
"""Get a specific scheduled job"""
|
|
result = await db.execute(select(ScheduledJob).where(ScheduledJob.id == job_id))
|
|
job = result.scalar_one_or_none()
|
|
if not job:
|
|
raise HTTPException(status_code=404, detail="Job not found")
|
|
return job
|
|
|
|
|
|
@router.post("", response_model=ScheduledJobResponse, status_code=status.HTTP_201_CREATED)
|
|
async def create_scheduled_job(
|
|
job_data: Dict[str, Any],
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Create a new scheduled job (predefined or custom code)"""
|
|
|
|
job_type = job_data.get("job_type", "predefined")
|
|
|
|
# Validate job type
|
|
if job_type not in ["predefined", "custom_code"]:
|
|
raise HTTPException(status_code=400, detail="job_type must be 'predefined' or 'custom_code'")
|
|
|
|
# Validate based on job type
|
|
if job_type == "predefined":
|
|
# Validate predefined function exists
|
|
predefined_function = job_data.get("predefined_function")
|
|
if not predefined_function:
|
|
raise HTTPException(status_code=400, detail="predefined_function is required for predefined jobs")
|
|
|
|
task_def = task_registry.get_task(predefined_function)
|
|
if not task_def:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Predefined task '{predefined_function}' not found. Use /scheduled-jobs/available-tasks to list available tasks."
|
|
)
|
|
|
|
# Create job
|
|
new_job = ScheduledJob(
|
|
name=job_data["name"],
|
|
job_type=JobType.PREDEFINED,
|
|
predefined_function=predefined_function,
|
|
function_params=job_data.get("function_params", {}),
|
|
cron_expression=job_data["cron_expression"],
|
|
description=job_data.get("description"),
|
|
is_active=job_data.get("is_active", True)
|
|
)
|
|
|
|
else: # custom_code
|
|
# Validate python code
|
|
python_code = job_data.get("python_code")
|
|
if not python_code:
|
|
raise HTTPException(status_code=400, detail="python_code is required for custom_code jobs")
|
|
|
|
validation_result = code_validator.validate_code(python_code)
|
|
if not validation_result["valid"]:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={
|
|
"message": "代码验证失败",
|
|
"errors": validation_result["errors"],
|
|
"warnings": validation_result["warnings"]
|
|
}
|
|
)
|
|
# Log warnings if any
|
|
if validation_result["warnings"]:
|
|
logger.warning(f"Code validation warnings: {validation_result['warnings']}")
|
|
|
|
# Create job
|
|
new_job = ScheduledJob(
|
|
name=job_data["name"],
|
|
job_type=JobType.CUSTOM_CODE,
|
|
python_code=python_code,
|
|
cron_expression=job_data["cron_expression"],
|
|
description=job_data.get("description"),
|
|
is_active=job_data.get("is_active", True)
|
|
)
|
|
|
|
db.add(new_job)
|
|
await db.commit()
|
|
await db.refresh(new_job)
|
|
|
|
# Schedule it
|
|
if new_job.is_active:
|
|
scheduler_service.add_job_to_scheduler(new_job)
|
|
|
|
return new_job
|
|
|
|
|
|
@router.put("/{job_id}", response_model=ScheduledJobResponse)
|
|
async def update_scheduled_job(
|
|
job_id: int,
|
|
job_data: ScheduledJobUpdate,
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Update a scheduled job"""
|
|
result = await db.execute(select(ScheduledJob).where(ScheduledJob.id == job_id))
|
|
job = result.scalar_one_or_none()
|
|
if not job:
|
|
raise HTTPException(status_code=404, detail="Job not found")
|
|
|
|
# Validate if changing job_type
|
|
if job_data.job_type is not None and job_data.job_type != job.job_type.value:
|
|
if job_data.job_type == "predefined":
|
|
if not job_data.predefined_function:
|
|
raise HTTPException(status_code=400, detail="predefined_function is required when changing to predefined type")
|
|
task_def = task_registry.get_task(job_data.predefined_function)
|
|
if not task_def:
|
|
raise HTTPException(status_code=400, detail=f"Task '{job_data.predefined_function}' not found")
|
|
elif job_data.job_type == "custom_code":
|
|
if not job_data.python_code:
|
|
raise HTTPException(status_code=400, detail="python_code is required when changing to custom_code type")
|
|
|
|
# Validate python code if being updated
|
|
if job_data.python_code is not None:
|
|
validation_result = code_validator.validate_code(job_data.python_code)
|
|
if not validation_result["valid"]:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={
|
|
"message": "代码验证失败",
|
|
"errors": validation_result["errors"],
|
|
"warnings": validation_result["warnings"]
|
|
}
|
|
)
|
|
if validation_result["warnings"]:
|
|
logger.warning(f"Code validation warnings: {validation_result['warnings']}")
|
|
|
|
# Validate predefined function if being updated
|
|
if job_data.predefined_function is not None:
|
|
task_def = task_registry.get_task(job_data.predefined_function)
|
|
if not task_def:
|
|
raise HTTPException(status_code=400, detail=f"Task '{job_data.predefined_function}' not found")
|
|
|
|
# Update fields
|
|
update_dict = job_data.dict(exclude_unset=True)
|
|
for key, value in update_dict.items():
|
|
if key == "job_type":
|
|
setattr(job, key, JobType(value))
|
|
else:
|
|
setattr(job, key, value)
|
|
|
|
job.updated_at = datetime.utcnow()
|
|
await db.commit()
|
|
await db.refresh(job)
|
|
|
|
# Update scheduler
|
|
await scheduler_service.reload_job(job.id)
|
|
|
|
return job
|
|
|
|
|
|
@router.delete("/{job_id}")
|
|
async def delete_scheduled_job(job_id: int, db: AsyncSession = Depends(get_db)):
|
|
"""Delete a scheduled job"""
|
|
result = await db.execute(select(ScheduledJob).where(ScheduledJob.id == job_id))
|
|
job = result.scalar_one_or_none()
|
|
if not job:
|
|
raise HTTPException(status_code=404, detail="Job not found")
|
|
|
|
# Remove from scheduler
|
|
scheduler_service.remove_job(job_id)
|
|
|
|
await db.delete(job)
|
|
await db.commit()
|
|
|
|
return {"message": "Job deleted successfully"}
|
|
|
|
|
|
@router.post("/{job_id}/run")
|
|
async def run_scheduled_job(job_id: int, db: AsyncSession = Depends(get_db)):
|
|
"""Manually trigger a scheduled job immediately"""
|
|
result = await db.execute(select(ScheduledJob).where(ScheduledJob.id == job_id))
|
|
job = result.scalar_one_or_none()
|
|
if not job:
|
|
raise HTTPException(status_code=404, detail="Job not found")
|
|
|
|
# Trigger async execution
|
|
# We use create_task to run it in background so API returns immediately
|
|
asyncio.create_task(scheduler_service.run_job_now(job_id))
|
|
|
|
return {"message": f"Job '{job.name}' triggered successfully"}
|