Initial schedule service implementation
Cron-like daemon that wakes reason service on schedule: - FastAPI on port 8084 - CRUD API for managing schedules - Cron expression support via croniter - Calls POST /process on reason service - Tracks execution history per task - 30-second check interval Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
commit
27d0dce910
4 changed files with 441 additions and 0 deletions
187
main.py
Normal file
187
main.py
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
#!/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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue