migrace /remind na sqlite
This commit is contained in:
108
skills/remind/scripts/db.py
Normal file
108
skills/remind/scripts/db.py
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
"""SQLite storage layer for /remind skill.
|
||||
|
||||
Schema, connection factory, and audit logging.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
SCHEMA = """
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS reminders (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
text TEXT NOT NULL CHECK(text <> ''),
|
||||
enabled INTEGER NOT NULL DEFAULT 1 CHECK(enabled IN (0, 1)),
|
||||
timezone TEXT NOT NULL DEFAULT 'Europe/Prague',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
deleted_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS schedule_at (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
reminder_id INTEGER NOT NULL REFERENCES reminders(id) ON DELETE CASCADE,
|
||||
at_datetime TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS schedule_cron (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
reminder_id INTEGER NOT NULL REFERENCES reminders(id) ON DELETE CASCADE,
|
||||
cron_expr TEXT NOT NULL CHECK(cron_expr <> '')
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS schedule_random (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
reminder_id INTEGER NOT NULL REFERENCES reminders(id) ON DELETE CASCADE,
|
||||
times_per_day INTEGER NOT NULL CHECK(times_per_day >= 1),
|
||||
window_start INTEGER NOT NULL CHECK(window_start >= 0 AND window_start < 1440),
|
||||
window_end INTEGER NOT NULL CHECK(window_end > 0 AND window_end <= 1440),
|
||||
days_filter TEXT,
|
||||
from_date TEXT,
|
||||
until_date TEXT,
|
||||
CHECK(window_start < window_end)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS reminder_fires (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
reminder_id INTEGER NOT NULL REFERENCES reminders(id) ON DELETE CASCADE,
|
||||
schedule_id INTEGER NOT NULL,
|
||||
schedule_type TEXT NOT NULL CHECK(schedule_type IN ('at', 'cron', 'random')),
|
||||
fire_time TEXT NOT NULL,
|
||||
delivered_at TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'delivered', 'failed')),
|
||||
error_message TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_reminders_text ON reminders(text);
|
||||
CREATE INDEX IF NOT EXISTS idx_at_datetime ON schedule_at(reminder_id, at_datetime);
|
||||
CREATE INDEX IF NOT EXISTS idx_cron_expr ON schedule_cron(reminder_id, cron_expr);
|
||||
CREATE INDEX IF NOT EXISTS idx_random_dates ON schedule_random(from_date, until_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_fire_lookup ON reminder_fires(
|
||||
reminder_id, schedule_type, schedule_id, fire_time, status
|
||||
);
|
||||
"""
|
||||
|
||||
|
||||
def get_db(path: Path) -> sqlite3.Connection:
|
||||
"""Return a connection with WAL mode and foreign keys enabled."""
|
||||
conn = sqlite3.connect(path, isolation_level=None)
|
||||
conn.execute("PRAGMA journal_mode = WAL")
|
||||
conn.execute("PRAGMA foreign_keys = ON")
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
|
||||
def init_db(path: Path) -> None:
|
||||
"""Create tables and indexes if they don't exist."""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
conn = get_db(path)
|
||||
try:
|
||||
conn.executescript(SCHEMA)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def log_operation(operation: str, reminder_id: int | None, details: str) -> None:
|
||||
"""Append an audit line to workspace/log/reminder.log."""
|
||||
workspace = Path(__file__).resolve().parent.parent.parent.parent
|
||||
log_dir = workspace / "log"
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
log_file = log_dir / "reminder.log"
|
||||
ts = datetime.now(timezone.utc).isoformat(timespec="seconds")
|
||||
line = f"{ts} [{operation}]"
|
||||
if reminder_id is not None:
|
||||
line += f" id={reminder_id}"
|
||||
if details:
|
||||
line += f" {details}"
|
||||
with log_file.open("a", encoding="utf-8") as f:
|
||||
f.write(line + "\n")
|
||||
Reference in New Issue
Block a user