Files
openrun/db.py
2026-05-18 12:53:24 -04:00

163 lines
4.4 KiB
Python

"""SQLite schema + connection helpers."""
from __future__ import annotations
import sqlite3
from pathlib import Path
DB_PATH = Path(__file__).parent / "data" / "garmin.db"
SCHEMA = """
CREATE TABLE IF NOT EXISTS activities (
activity_id INTEGER PRIMARY KEY,
start_time_local TEXT,
start_time_gmt TEXT,
activity_type TEXT,
activity_name TEXT,
distance_m REAL,
duration_s REAL,
moving_duration_s REAL,
avg_speed_mps REAL,
max_speed_mps REAL,
avg_hr REAL,
max_hr REAL,
calories REAL,
elevation_gain_m REAL,
elevation_loss_m REAL,
training_load REAL,
aerobic_te REAL,
anaerobic_te REAL,
vo2_max REAL,
raw TEXT NOT NULL,
fetched_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_activities_start ON activities(start_time_local);
CREATE INDEX IF NOT EXISTS idx_activities_type ON activities(activity_type);
CREATE TABLE IF NOT EXISTS activity_splits (
activity_id INTEGER NOT NULL,
split_index INTEGER NOT NULL,
distance_m REAL,
duration_s REAL,
avg_hr REAL,
avg_speed_mps REAL,
elevation_gain_m REAL,
raw TEXT NOT NULL,
PRIMARY KEY (activity_id, split_index),
FOREIGN KEY (activity_id) REFERENCES activities(activity_id)
);
CREATE TABLE IF NOT EXISTS daily_steps (
calendar_date TEXT PRIMARY KEY,
total_steps INTEGER,
step_goal INTEGER,
distance_m REAL,
raw TEXT NOT NULL,
fetched_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS daily_sleep (
calendar_date TEXT PRIMARY KEY,
sleep_start_gmt TEXT,
sleep_end_gmt TEXT,
deep_s REAL,
light_s REAL,
rem_s REAL,
awake_s REAL,
sleep_score REAL,
raw TEXT NOT NULL,
fetched_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS daily_stress (
calendar_date TEXT PRIMARY KEY,
avg_stress REAL,
max_stress REAL,
raw TEXT NOT NULL,
fetched_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS daily_hrv (
calendar_date TEXT PRIMARY KEY,
weekly_avg REAL,
last_night_avg REAL,
last_night_5min REAL,
status TEXT,
raw TEXT NOT NULL,
fetched_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS daily_body_battery (
calendar_date TEXT PRIMARY KEY,
charged INTEGER,
drained INTEGER,
highest INTEGER,
lowest INTEGER,
raw TEXT NOT NULL,
fetched_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS daily_intensity_minutes (
calendar_date TEXT PRIMARY KEY,
moderate_minutes INTEGER,
vigorous_minutes INTEGER,
raw TEXT NOT NULL,
fetched_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS daily_resting_hr (
calendar_date TEXT PRIMARY KEY,
resting_hr INTEGER,
raw TEXT NOT NULL,
fetched_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS activity_fit_files (
activity_id INTEGER PRIMARY KEY,
fit_path TEXT NOT NULL,
indexed_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS activity_time_in_zone (
activity_id INTEGER PRIMARY KEY,
z1_s REAL,
z2_s REAL,
z3_s REAL,
z4_s REAL,
z5_s REAL,
total_s REAL,
source TEXT,
computed_at TEXT NOT NULL,
FOREIGN KEY (activity_id) REFERENCES activities(activity_id)
);
CREATE TABLE IF NOT EXISTS sync_state (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT NOT NULL
);
"""
def connect() -> sqlite3.Connection:
DB_PATH.parent.mkdir(exist_ok=True)
conn = sqlite3.connect(DB_PATH)
conn.execute("PRAGMA journal_mode = WAL")
conn.execute("PRAGMA foreign_keys = ON")
conn.row_factory = sqlite3.Row
conn.executescript(SCHEMA)
return conn
def set_state(conn: sqlite3.Connection, key: str, value: str) -> None:
conn.execute(
"INSERT INTO sync_state(key, value, updated_at) VALUES (?, ?, datetime('now')) "
"ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at",
(key, value),
)
def get_state(conn: sqlite3.Connection, key: str) -> str | None:
row = conn.execute("SELECT value FROM sync_state WHERE key = ?", (key,)).fetchone()
return row["value"] if row else None