from __future__ import annotations import os import sqlite3 from contextlib import contextmanager from pathlib import Path from typing import Iterator from urllib.parse import quote DEFAULT_DB_PATH = Path.home() / ".local" / "share" / "jarca" / "ledger.sqlite3" class DatabaseNotInitializedError(RuntimeError): """Raised when a read is requested before the ledger exists.""" def resolve_db_path(db_path: str | None = None) -> Path: raw = db_path or os.getenv("JARCA_LEDGER_DB_PATH") or str(DEFAULT_DB_PATH) return Path(raw).expanduser().resolve() def connect(db_path: str | None = None) -> sqlite3.Connection: path = resolve_db_path(db_path) path.parent.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(path) conn.row_factory = sqlite3.Row conn.execute("PRAGMA foreign_keys=ON") return conn def connect_readonly(db_path: str | None = None) -> sqlite3.Connection: """Open an existing ledger in SQLite URI read-only mode.""" path = resolve_db_path(db_path) if not path.is_file(): raise DatabaseNotInitializedError("ledger database is not initialized") uri = f"file:{quote(str(path), safe='/')}?mode=ro" try: conn = sqlite3.connect(uri, uri=True) except sqlite3.Error as exc: raise DatabaseNotInitializedError("ledger database is not initialized") from exc conn.row_factory = sqlite3.Row conn.execute("PRAGMA foreign_keys=ON") return conn @contextmanager def read_db(db_path: str | None = None) -> Iterator[sqlite3.Connection]: """Open an existing ledger without filesystem or transaction writes.""" conn = connect_readonly(db_path) try: yield conn finally: conn.close() @contextmanager def db(db_path: str | None = None) -> Iterator[sqlite3.Connection]: from .migrations import init_db init_db(db_path) conn = connect(db_path) try: yield conn conn.commit() except Exception: conn.rollback() raise finally: conn.close()