Files
Bjorn/db_utils/schedules.py
infinition b0584a1a8e feat: Add login page with dynamic RGB effects and password toggle functionality
feat: Implement package management utilities with JSON endpoints for listing and uninstalling packages

feat: Create plugin management utilities with endpoints for listing, configuring, and installing plugins

feat: Develop schedule and trigger management utilities with CRUD operations for schedules and triggers
2026-03-19 00:40:04 +01:00

245 lines
9.9 KiB
Python

"""schedules.py - Script scheduling and trigger operations."""
import json
import logging
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional
from logger import Logger
logger = Logger(name="db_utils.schedules", level=logging.DEBUG)
class ScheduleOps:
"""Script schedule and trigger management operations"""
def __init__(self, base):
self.base = base
def create_tables(self):
"""Create script_schedules and script_triggers tables"""
self.base.execute("""
CREATE TABLE IF NOT EXISTS script_schedules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
script_name TEXT NOT NULL,
schedule_type TEXT NOT NULL DEFAULT 'recurring',
interval_seconds INTEGER,
run_at TEXT,
args TEXT DEFAULT '',
conditions TEXT,
enabled INTEGER DEFAULT 1,
last_run_at TEXT,
next_run_at TEXT,
run_count INTEGER DEFAULT 0,
last_status TEXT,
last_error TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
""")
self.base.execute("""
CREATE INDEX IF NOT EXISTS idx_sched_next
ON script_schedules(next_run_at) WHERE enabled=1;
""")
self.base.execute("""
CREATE TABLE IF NOT EXISTS script_triggers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
script_name TEXT NOT NULL,
trigger_name TEXT NOT NULL,
conditions TEXT NOT NULL,
args TEXT DEFAULT '',
enabled INTEGER DEFAULT 1,
last_fired_at TEXT,
fire_count INTEGER DEFAULT 0,
cooldown_seconds INTEGER DEFAULT 60,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
""")
self.base.execute("""
CREATE INDEX IF NOT EXISTS idx_trig_enabled
ON script_triggers(enabled) WHERE enabled=1;
""")
logger.debug("Schedule and trigger tables created/verified")
# =========================================================================
# SCHEDULE OPERATIONS
# =========================================================================
def add_schedule(self, script_name: str, schedule_type: str,
interval_seconds: Optional[int] = None,
run_at: Optional[str] = None, args: str = '',
conditions: Optional[str] = None) -> int:
"""Insert a new schedule entry and return its id"""
next_run_at = None
if schedule_type == 'recurring' and interval_seconds:
next_run_at = (datetime.utcnow() + timedelta(seconds=interval_seconds)).strftime('%Y-%m-%d %H:%M:%S')
elif run_at:
next_run_at = run_at
self.base.execute("""
INSERT INTO script_schedules
(script_name, schedule_type, interval_seconds, run_at, args, conditions, next_run_at)
VALUES (?, ?, ?, ?, ?, ?, ?);
""", (script_name, schedule_type, interval_seconds, run_at, args, conditions, next_run_at))
rows = self.base.query("SELECT last_insert_rowid() AS id;")
return rows[0]['id'] if rows else 0
def update_schedule(self, id: int, **kwargs) -> None:
"""Update schedule fields; recompute next_run_at if interval changes"""
if not kwargs:
return
sets = []
params = []
for key, value in kwargs.items():
sets.append(f"{key}=?")
params.append(value)
sets.append("updated_at=datetime('now')")
params.append(id)
self.base.execute(
f"UPDATE script_schedules SET {', '.join(sets)} WHERE id=?;",
tuple(params)
)
# Recompute next_run_at if interval changed
if 'interval_seconds' in kwargs:
row = self.get_schedule(id)
if row and row['schedule_type'] == 'recurring' and kwargs['interval_seconds']:
next_run = (datetime.utcnow() + timedelta(seconds=kwargs['interval_seconds'])).strftime('%Y-%m-%d %H:%M:%S')
self.base.execute(
"UPDATE script_schedules SET next_run_at=?, updated_at=datetime('now') WHERE id=?;",
(next_run, id)
)
def delete_schedule(self, id: int) -> None:
"""Delete a schedule by id"""
self.base.execute("DELETE FROM script_schedules WHERE id=?;", (id,))
def list_schedules(self, enabled_only: bool = False) -> List[Dict[str, Any]]:
"""List all schedules, optionally filtered to enabled only"""
if enabled_only:
return self.base.query(
"SELECT * FROM script_schedules WHERE enabled=1 ORDER BY id;"
)
return self.base.query("SELECT * FROM script_schedules ORDER BY id;")
def get_schedule(self, id: int) -> Optional[Dict[str, Any]]:
"""Get a single schedule by id"""
return self.base.query_one(
"SELECT * FROM script_schedules WHERE id=?;", (id,)
)
def get_due_schedules(self) -> List[Dict[str, Any]]:
"""Get schedules that are due to run"""
return self.base.query("""
SELECT * FROM script_schedules
WHERE enabled=1
AND next_run_at <= datetime('now')
AND (last_status IS NULL OR last_status != 'running')
ORDER BY next_run_at;
""")
def mark_schedule_run(self, id: int, status: str, error: Optional[str] = None) -> None:
"""Mark a schedule as run, update counters, recompute next_run_at"""
row = self.get_schedule(id)
if not row:
return
now = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
if row['schedule_type'] == 'recurring' and row['interval_seconds']:
next_run = (datetime.utcnow() + timedelta(seconds=row['interval_seconds'])).strftime('%Y-%m-%d %H:%M:%S')
self.base.execute("""
UPDATE script_schedules
SET last_run_at=?, last_status=?, last_error=?,
run_count=run_count+1, next_run_at=?, updated_at=datetime('now')
WHERE id=?;
""", (now, status, error, next_run, id))
else:
# oneshot: disable after run
self.base.execute("""
UPDATE script_schedules
SET last_run_at=?, last_status=?, last_error=?,
run_count=run_count+1, enabled=0, updated_at=datetime('now')
WHERE id=?;
""", (now, status, error, id))
def toggle_schedule(self, id: int, enabled: bool) -> None:
"""Enable or disable a schedule"""
self.base.execute(
"UPDATE script_schedules SET enabled=?, updated_at=datetime('now') WHERE id=?;",
(1 if enabled else 0, id)
)
# =========================================================================
# TRIGGER OPERATIONS
# =========================================================================
def add_trigger(self, script_name: str, trigger_name: str, conditions: str,
args: str = '', cooldown_seconds: int = 60) -> int:
"""Insert a new trigger and return its id"""
self.base.execute("""
INSERT INTO script_triggers
(script_name, trigger_name, conditions, args, cooldown_seconds)
VALUES (?, ?, ?, ?, ?);
""", (script_name, trigger_name, conditions, args, cooldown_seconds))
rows = self.base.query("SELECT last_insert_rowid() AS id;")
return rows[0]['id'] if rows else 0
def update_trigger(self, id: int, **kwargs) -> None:
"""Update trigger fields"""
if not kwargs:
return
sets = []
params = []
for key, value in kwargs.items():
sets.append(f"{key}=?")
params.append(value)
params.append(id)
self.base.execute(
f"UPDATE script_triggers SET {', '.join(sets)} WHERE id=?;",
tuple(params)
)
def delete_trigger(self, id: int) -> None:
"""Delete a trigger by id"""
self.base.execute("DELETE FROM script_triggers WHERE id=?;", (id,))
def list_triggers(self, enabled_only: bool = False) -> List[Dict[str, Any]]:
"""List all triggers, optionally filtered to enabled only"""
if enabled_only:
return self.base.query(
"SELECT * FROM script_triggers WHERE enabled=1 ORDER BY id;"
)
return self.base.query("SELECT * FROM script_triggers ORDER BY id;")
def get_trigger(self, id: int) -> Optional[Dict[str, Any]]:
"""Get a single trigger by id"""
return self.base.query_one(
"SELECT * FROM script_triggers WHERE id=?;", (id,)
)
def get_active_triggers(self) -> List[Dict[str, Any]]:
"""Get all enabled triggers"""
return self.base.query(
"SELECT * FROM script_triggers WHERE enabled=1 ORDER BY id;"
)
def mark_trigger_fired(self, id: int) -> None:
"""Record that a trigger has fired"""
self.base.execute("""
UPDATE script_triggers
SET last_fired_at=datetime('now'), fire_count=fire_count+1
WHERE id=?;
""", (id,))
def is_trigger_on_cooldown(self, id: int) -> bool:
"""Check if a trigger is still within its cooldown period"""
row = self.base.query_one("""
SELECT 1 AS on_cooldown FROM script_triggers
WHERE id=?
AND last_fired_at IS NOT NULL
AND datetime(last_fired_at, '+' || cooldown_seconds || ' seconds') > datetime('now');
""", (id,))
return row is not None