#!/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)