Files
Aetheel/scheduler/store.py
Tanmay Karande 41b2f9a593 latest updates
2026-02-15 15:02:58 -05:00

168 lines
5.1 KiB
Python

"""
Aetheel Scheduler — SQLite Job Store
=====================================
Persistent storage for scheduled jobs.
Schema:
jobs(id, cron_expr, prompt, channel_id, channel_type, created_at, next_run)
"""
import json
import logging
import os
import sqlite3
import uuid
from dataclasses import dataclass
from datetime import datetime, timezone
logger = logging.getLogger("aetheel.scheduler.store")
@dataclass
class ScheduledJob:
"""A persisted scheduled job."""
id: str
cron_expr: str | None # None for one-shot jobs
prompt: str
channel_id: str
channel_type: str # "slack", "telegram", etc.
created_at: str
next_run: str | None = None
thread_id: str | None = None # for threading context
user_name: str | None = None # who created the job
@property
def is_recurring(self) -> bool:
return self.cron_expr is not None
# ---------------------------------------------------------------------------
# SQLite Store
# ---------------------------------------------------------------------------
CREATE_TABLE_SQL = """
CREATE TABLE IF NOT EXISTS jobs (
id TEXT PRIMARY KEY,
cron_expr TEXT,
prompt TEXT NOT NULL,
channel_id TEXT NOT NULL,
channel_type TEXT NOT NULL DEFAULT 'slack',
thread_id TEXT,
user_name TEXT,
created_at TEXT NOT NULL,
next_run TEXT
);
"""
class JobStore:
"""SQLite-backed persistent job store."""
def __init__(self, db_path: str | None = None):
self._db_path = db_path or os.path.join(
os.path.expanduser("~/.aetheel"), "scheduler.db"
)
# Ensure directory exists
os.makedirs(os.path.dirname(self._db_path), exist_ok=True)
self._init_db()
def _init_db(self) -> None:
"""Initialize the database schema."""
with sqlite3.connect(self._db_path) as conn:
conn.execute(CREATE_TABLE_SQL)
conn.commit()
logger.info(f"Scheduler DB initialized: {self._db_path}")
def _conn(self) -> sqlite3.Connection:
conn = sqlite3.connect(self._db_path)
conn.row_factory = sqlite3.Row
return conn
def add(self, job: ScheduledJob) -> str:
"""Add a job to the store. Returns the job ID."""
with self._conn() as conn:
conn.execute(
"""
INSERT INTO jobs (id, cron_expr, prompt, channel_id, channel_type,
thread_id, user_name, created_at, next_run)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
job.id,
job.cron_expr,
job.prompt,
job.channel_id,
job.channel_type,
job.thread_id,
job.user_name,
job.created_at,
job.next_run,
),
)
conn.commit()
logger.info(f"Job added: {job.id} (cron={job.cron_expr})")
return job.id
def remove(self, job_id: str) -> bool:
"""Remove a job by ID. Returns True if found and removed."""
with self._conn() as conn:
cursor = conn.execute("DELETE FROM jobs WHERE id = ?", (job_id,))
conn.commit()
removed = cursor.rowcount > 0
if removed:
logger.info(f"Job removed: {job_id}")
return removed
def get(self, job_id: str) -> ScheduledJob | None:
"""Get a job by ID."""
with self._conn() as conn:
row = conn.execute(
"SELECT * FROM jobs WHERE id = ?", (job_id,)
).fetchone()
return self._row_to_job(row) if row else None
def list_all(self) -> list[ScheduledJob]:
"""List all jobs."""
with self._conn() as conn:
rows = conn.execute(
"SELECT * FROM jobs ORDER BY created_at"
).fetchall()
return [self._row_to_job(row) for row in rows]
def list_recurring(self) -> list[ScheduledJob]:
"""List only recurring (cron) jobs."""
with self._conn() as conn:
rows = conn.execute(
"SELECT * FROM jobs WHERE cron_expr IS NOT NULL ORDER BY created_at"
).fetchall()
return [self._row_to_job(row) for row in rows]
def clear_oneshot(self) -> int:
"""Remove all one-shot (non-cron) jobs. Returns count removed."""
with self._conn() as conn:
cursor = conn.execute(
"DELETE FROM jobs WHERE cron_expr IS NULL"
)
conn.commit()
return cursor.rowcount
@staticmethod
def _row_to_job(row: sqlite3.Row) -> ScheduledJob:
return ScheduledJob(
id=row["id"],
cron_expr=row["cron_expr"],
prompt=row["prompt"],
channel_id=row["channel_id"],
channel_type=row["channel_type"],
thread_id=row["thread_id"],
user_name=row["user_name"],
created_at=row["created_at"],
next_run=row["next_run"],
)
@staticmethod
def new_id() -> str:
"""Generate a short unique job ID."""
return uuid.uuid4().hex[:8]