schedule/main.py

188 lines
5 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""
Egregore Schedule Service - Cron-like task scheduler
Provides HTTP API for managing scheduled tasks that wake the reason service.
Runs on port 8084.
"""
import asyncio
import os
import uuid
from contextlib import asynccontextmanager
from datetime import datetime
from typing import Optional
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from scheduler import Scheduler, ScheduledTask
# Lifespan for startup/shutdown
@asynccontextmanager
async def lifespan(app: FastAPI):
# Start the scheduler background task
scheduler_task = asyncio.create_task(scheduler.run())
yield
# Stop the scheduler
scheduler.stop()
scheduler_task.cancel()
try:
await scheduler_task
except asyncio.CancelledError:
pass
app = FastAPI(title="Egregore Schedule Service", docs_url="/docs", lifespan=lifespan)
# Initialize scheduler
DATA_DIR = "/home/admin/data/schedules"
scheduler = Scheduler(DATA_DIR)
# Request/Response models
class ScheduleCreate(BaseModel):
name: str
cron: str # Cron expression (e.g., "0 9 * * *" for 9am daily)
instruction: str # What to tell reason to do
enabled: bool = True
class ScheduleUpdate(BaseModel):
name: Optional[str] = None
cron: Optional[str] = None
instruction: Optional[str] = None
enabled: Optional[bool] = None
class ScheduleResponse(BaseModel):
id: str
name: str
cron: str
instruction: str
enabled: bool
created_at: str
last_run: Optional[str]
next_run: Optional[str]
run_count: int
# Endpoints
@app.get("/health")
async def health():
"""Health check endpoint"""
return {"status": "ok", "service": "schedule"}
@app.get("/schedules")
async def list_schedules():
"""List all scheduled tasks"""
tasks = scheduler.list_tasks()
return {"schedules": [task_to_response(t) for t in tasks]}
@app.post("/schedules")
async def create_schedule(req: ScheduleCreate):
"""Create a new scheduled task"""
task_id = str(uuid.uuid4())[:8]
task = ScheduledTask(
id=task_id,
name=req.name,
cron=req.cron,
instruction=req.instruction,
enabled=req.enabled,
created_at=datetime.utcnow().isoformat() + "Z",
last_run=None,
run_count=0
)
try:
scheduler.add_task(task)
return task_to_response(task)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@app.get("/schedules/{task_id}")
async def get_schedule(task_id: str):
"""Get a specific scheduled task"""
task = scheduler.get_task(task_id)
if not task:
raise HTTPException(status_code=404, detail="Schedule not found")
return task_to_response(task)
@app.patch("/schedules/{task_id}")
async def update_schedule(task_id: str, req: ScheduleUpdate):
"""Update a scheduled task"""
task = scheduler.get_task(task_id)
if not task:
raise HTTPException(status_code=404, detail="Schedule not found")
if req.name is not None:
task.name = req.name
if req.cron is not None:
task.cron = req.cron
if req.instruction is not None:
task.instruction = req.instruction
if req.enabled is not None:
task.enabled = req.enabled
try:
scheduler.update_task(task)
return task_to_response(task)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@app.delete("/schedules/{task_id}")
async def delete_schedule(task_id: str):
"""Delete a scheduled task"""
if not scheduler.delete_task(task_id):
raise HTTPException(status_code=404, detail="Schedule not found")
return {"status": "deleted", "id": task_id}
@app.post("/schedules/{task_id}/run")
async def run_schedule(task_id: str):
"""Manually trigger a scheduled task"""
task = scheduler.get_task(task_id)
if not task:
raise HTTPException(status_code=404, detail="Schedule not found")
result = await scheduler.execute_task(task)
return {"status": "executed", "id": task_id, "result": result}
@app.get("/schedules/{task_id}/history")
async def get_schedule_history(task_id: str, limit: int = 20):
"""Get execution history for a scheduled task"""
task = scheduler.get_task(task_id)
if not task:
raise HTTPException(status_code=404, detail="Schedule not found")
history = scheduler.get_history(task_id, limit)
return {"id": task_id, "history": history}
def task_to_response(task: ScheduledTask) -> dict:
"""Convert a task to API response format"""
next_run = scheduler.get_next_run(task) if task.enabled else None
return {
"id": task.id,
"name": task.name,
"cron": task.cron,
"instruction": task.instruction,
"enabled": task.enabled,
"created_at": task.created_at,
"last_run": task.last_run,
"next_run": next_run.isoformat() + "Z" if next_run else None,
"run_count": task.run_count
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=8084)