#!/usr/bin/env python3 """Delta report over db/ollama_usage.sqlite collected by ollama_usage_poll.py. Request counts are the exact axis; `limits.*.usage` has a resolution of 0.1 %, so per-sample percentage deltas are mostly quantization noise and are shown only as a running level, plus one aggregate for the whole window. """ from __future__ import annotations import argparse import itertools import json import sqlite3 import sys from datetime import UTC, datetime, timedelta from pathlib import Path WORKSPACE = Path(__file__).resolve().parents[3] DB_PATH = WORKSPACE / "db" / "ollama_usage.sqlite" DEFAULT_WINDOW = timedelta(hours=24) GAP_MINUTES = 15 def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--since", help="ISO 8601 UTC start (default: 24 h ago)") parser.add_argument("--until", help="ISO 8601 UTC end (default: now)") return parser.parse_args() def resolve_window(args: argparse.Namespace) -> tuple[str, str]: now = datetime.now(UTC).replace(microsecond=0) since = args.since or (now - DEFAULT_WINDOW).isoformat() until = args.until or now.isoformat() return since, until def load_samples(conn: sqlite3.Connection, since: str, until: str) -> list[tuple]: return conn.execute( "SELECT ts, session_usage, weekly_usage, session_models, weekly_models " "FROM samples WHERE ts >= ? AND ts <= ? ORDER BY ts", (since, until), ).fetchall() def count_deltas(previous: dict[str, int], current: dict[str, int]) -> dict[str, int]: names = set(previous) | set(current) deltas = {n: current.get(n, 0) - previous.get(n, 0) for n in names} return {n: d for n, d in sorted(deltas.items()) if d} def format_deltas(deltas: dict[str, int]) -> str: return ", ".join(f"{name} +{count}" for name, count in deltas.items()) or "-" def minutes_between(earlier: str, later: str) -> float: delta = datetime.fromisoformat(later) - datetime.fromisoformat(earlier) return delta.total_seconds() / 60 def is_window_reset(previous: tuple, current: tuple) -> bool: """Session window rolled over: its usage or its request total went down.""" previous_total = sum(json.loads(previous[3]).values()) current_total = sum(json.loads(current[3]).values()) return current[1] < previous[1] or current_total < previous_total def print_rows(samples: list[tuple]) -> dict[str, int]: total: dict[str, int] = {} for previous, current in itertools.pairwise(samples): gap = minutes_between(previous[0], current[0]) if gap > GAP_MINUTES: print(f" … {gap:.0f} min with no recorded change (idle or poller down)") if is_window_reset(previous, current): print(" ── session window reset ──") deltas = count_deltas(json.loads(previous[4]), json.loads(current[4])) for name, count in deltas.items(): total[name] = total.get(name, 0) + count print( f"{current[0]} session {current[1] * 100:5.1f} % " f"weekly {current[2] * 100:5.1f} % {format_deltas(deltas)}" ) return total def print_summary(samples: list[tuple], total: dict[str, int]) -> None: first, last = samples[0], samples[-1] print() print(f"Window: {first[0]} → {last[0]} ({len(samples)} samples)") print(f"Weekly usage: {first[2] * 100:.1f} % → {last[2] * 100:.1f} %") print(f"Requests: {format_deltas(total)}") def print_poll_status(conn: sqlite3.Connection) -> None: row = conn.execute("SELECT last_ts, last_status FROM meta WHERE id = 1").fetchone() if row: print(f"Last poll: {row[0]} ({row[1]})") def main() -> None: if not DB_PATH.is_file(): sys.exit(f"no samples yet: {DB_PATH} does not exist") args = parse_args() since, until = resolve_window(args) conn = sqlite3.connect(DB_PATH) samples = load_samples(conn, since, until) if len(samples) < 2: print(f"Not enough samples between {since} and {until}.") print_poll_status(conn) return total = print_rows(samples) print_summary(samples, total) print_poll_status(conn) if __name__ == "__main__": main()