feat: scaffold v2 with FastAPI, SQLite database, and project structure

Migrate from raw HTTPServer to FastAPI, add SQLite database with schema
for known objects, embeddings, events, and settings. Add centralized
config, Pydantic models, and 22 database integration tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 17:02:17 -06:00
parent 6086ed3cc0
commit 031b9eaaec
10 changed files with 721 additions and 82 deletions
+108
View File
@@ -0,0 +1,108 @@
"""SQLite database schema and connection management for frigate-notify v2."""
import logging
import sqlite3
import threading
logger = logging.getLogger(__name__)
# Thread-local storage for database connections
_local = threading.local()
_db_path: str = "/data/frigate-notify.db"
def set_db_path(path: str) -> None:
"""Set the database path. Must be called before any other database operations."""
global _db_path
_db_path = path
def get_connection() -> sqlite3.Connection:
"""Get a thread-local database connection with WAL mode and foreign keys enabled."""
if not hasattr(_local, "connection") or _local.connection is None:
conn = sqlite3.connect(_db_path)
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA foreign_keys=ON")
conn.row_factory = sqlite3.Row
_local.connection = conn
return _local.connection
def close_connection() -> None:
"""Close the thread-local database connection if one exists."""
if hasattr(_local, "connection") and _local.connection is not None:
_local.connection.close()
_local.connection = None
def init_db() -> None:
"""Create all tables and default settings."""
conn = get_connection()
cursor = conn.cursor()
cursor.executescript(
"""
CREATE TABLE IF NOT EXISTS known_objects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL CHECK(type IN ('person', 'vehicle', 'pet')),
action TEXT NOT NULL DEFAULT 'quiet' CHECK(action IN ('quiet', 'suppress', 'alert')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS embeddings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
known_object_id INTEGER NOT NULL,
embedding BLOB NOT NULL,
snapshot_url TEXT,
thumbnail BLOB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (known_object_id) REFERENCES known_objects(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
frigate_event_id TEXT UNIQUE NOT NULL,
camera TEXT NOT NULL,
label TEXT NOT NULL,
score REAL NOT NULL,
snapshot BLOB,
embedding BLOB,
match_status TEXT NOT NULL DEFAULT 'unreviewed'
CHECK(match_status IN ('known', 'unknown', 'ambiguous', 'unreviewed')),
matched_object_id INTEGER,
match_confidence REAL,
ai_description TEXT,
notified INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (matched_object_id) REFERENCES known_objects(id) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at);
CREATE INDEX IF NOT EXISTS idx_events_match_status ON events(match_status);
CREATE INDEX IF NOT EXISTS idx_events_camera ON events(camera);
CREATE INDEX IF NOT EXISTS idx_embeddings_known_object_id ON embeddings(known_object_id);
"""
)
# Insert default settings if they don't exist
defaults = {
"vision_backend": "haiku",
"recognition_enabled": "false",
"event_retention_days": "90",
}
for key, value in defaults.items():
cursor.execute(
"INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)",
(key, value),
)
conn.commit()
logger.info("Database initialized at %s", _db_path)