Download app/db.py from drowzeys/keys-Auto-Receipts-Studio: direct link, hf CLI and curl.
- Browser
- Download file 12.7 kB
-
https://huggingface.co/drowzeys/keys-Auto-Receipts-Studio/resolve/main/app/db.py
- Command line
-
hf download hf://drowzeys/keys-Auto-Receipts-Studio/app/db.py
-
curl -L -o db.py https://huggingface.co/drowzeys/keys-Auto-Receipts-Studio/resolve/main/app/db.py
12.7 kB
| from __future__ import annotations | |
| import json | |
| import sqlite3 | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| from typing import Any, Iterator | |
| from app.config import Settings | |
| from app.schemas import MatchHit, ReceiptExtract, ReceiptStatus, to_cents | |
| try: | |
| import sqlite_vec | |
| from sqlite_vec import serialize_float32 | |
| except ImportError: # pragma: no cover | |
| sqlite_vec = None | |
| serialize_float32 = None # type: ignore[assignment] | |
| class VecLoadError(RuntimeError): | |
| pass | |
| class EmbedIndexError(RuntimeError): | |
| pass | |
| def _utc_now() -> str: | |
| return datetime.now(timezone.utc).replace(microsecond=0).isoformat() | |
| def connect(path: Path) -> sqlite3.Connection: | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| con = sqlite3.connect(str(path)) | |
| con.row_factory = sqlite3.Row | |
| con.execute("PRAGMA foreign_keys = ON") | |
| if sqlite_vec is None: | |
| raise VecLoadError("sqlite-vec is not installed") | |
| try: | |
| con.enable_load_extension(True) | |
| sqlite_vec.load(con) | |
| con.enable_load_extension(False) | |
| except Exception as exc: | |
| con.close() | |
| raise VecLoadError(f"sqlite-vec load failed: {exc}") from exc | |
| return con | |
| def _create_vec_table(con: sqlite3.Connection, name: str, pk: str, dim: int) -> None: | |
| ddl = ( | |
| f"CREATE VIRTUAL TABLE IF NOT EXISTS {name} USING vec0(" | |
| f"{pk} INTEGER PRIMARY KEY, embedding float[{dim}] distance_metric=cosine)" | |
| ) | |
| try: | |
| con.execute(ddl) | |
| except sqlite3.OperationalError: | |
| con.execute( | |
| f"CREATE VIRTUAL TABLE IF NOT EXISTS {name} USING vec0(" | |
| f"{pk} INTEGER PRIMARY KEY, embedding float[{dim}])" | |
| ) | |
| def init_schema(con: sqlite3.Connection, settings: Settings) -> None: | |
| con.executescript( | |
| """ | |
| CREATE TABLE IF NOT EXISTS meta ( | |
| key TEXT PRIMARY KEY, | |
| value TEXT NOT NULL | |
| ); | |
| CREATE TABLE IF NOT EXISTS receipts ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| source_path TEXT NOT NULL, | |
| sha256 TEXT NOT NULL UNIQUE, | |
| status TEXT NOT NULL, | |
| doc_kind TEXT, | |
| category TEXT, | |
| vendor TEXT, | |
| receipt_date TEXT, | |
| tax_cents INTEGER, | |
| total_cents INTEGER, | |
| currency TEXT, | |
| ocr_text TEXT, | |
| extract_json TEXT, | |
| error TEXT, | |
| created_at TEXT NOT NULL, | |
| updated_at TEXT NOT NULL | |
| ); | |
| CREATE INDEX IF NOT EXISTS idx_receipts_category ON receipts(category); | |
| CREATE INDEX IF NOT EXISTS idx_receipts_vendor ON receipts(vendor); | |
| CREATE INDEX IF NOT EXISTS idx_receipts_date ON receipts(receipt_date); | |
| CREATE TABLE IF NOT EXISTS line_items ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| receipt_id INTEGER NOT NULL REFERENCES receipts(id) ON DELETE CASCADE, | |
| description TEXT NOT NULL, | |
| qty REAL, | |
| unit_price_cents INTEGER, | |
| amount_cents INTEGER, | |
| sku TEXT, | |
| match_catalog_id INTEGER, | |
| match_score REAL, | |
| match_status TEXT | |
| ); | |
| CREATE TABLE IF NOT EXISTS catalog ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| sku TEXT, | |
| vendor TEXT, | |
| description TEXT NOT NULL, | |
| size TEXT, | |
| unit_price_cents INTEGER, | |
| metadata_json TEXT | |
| ); | |
| """ | |
| ) | |
| _create_vec_table(con, "receipt_vec", "receipt_id", settings.embed_dim) | |
| _create_vec_table(con, "catalog_vec", "catalog_id", settings.embed_dim) | |
| _check_or_set_meta(con, settings) | |
| con.commit() | |
| def _meta(con: sqlite3.Connection, key: str) -> str | None: | |
| row = con.execute("SELECT value FROM meta WHERE key = ?", (key,)).fetchone() | |
| return None if row is None else str(row["value"]) | |
| def _check_or_set_meta(con: sqlite3.Connection, settings: Settings) -> None: | |
| stored_model = _meta(con, "embed_model") | |
| stored_dim = _meta(con, "embed_dim") | |
| if stored_model is None: | |
| con.execute( | |
| "INSERT INTO meta(key, value) VALUES ('embed_model', ?), ('embed_dim', ?)", | |
| (settings.embed_model, str(settings.embed_dim)), | |
| ) | |
| return | |
| if stored_model != settings.embed_model or stored_dim != str(settings.embed_dim): | |
| raise EmbedIndexError( | |
| f"index is {stored_model} dim={stored_dim}; config is " | |
| f"{settings.embed_model} dim={settings.embed_dim}. Never mix embedding models." | |
| ) | |
| def open_db(settings: Settings) -> sqlite3.Connection: | |
| con = connect(settings.db_path) | |
| init_schema(con, settings) | |
| return con | |
| def get_by_sha(con: sqlite3.Connection, sha256: str) -> sqlite3.Row | None: | |
| return con.execute("SELECT * FROM receipts WHERE sha256 = ?", (sha256,)).fetchone() | |
| def insert_receipt( | |
| con: sqlite3.Connection, | |
| *, | |
| source_path: str, | |
| sha256: str, | |
| status: ReceiptStatus, | |
| extract: ReceiptExtract | None = None, | |
| ocr_text: str | None = None, | |
| error: str | None = None, | |
| ) -> int: | |
| now = _utc_now() | |
| cur = con.execute( | |
| """ | |
| INSERT INTO receipts ( | |
| source_path, sha256, status, doc_kind, category, vendor, receipt_date, | |
| tax_cents, total_cents, currency, ocr_text, extract_json, error, | |
| created_at, updated_at | |
| ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | |
| """, | |
| ( | |
| source_path, | |
| sha256, | |
| status.value, | |
| None if extract is None else extract.doc_kind, | |
| None if extract is None else extract.category, | |
| None if extract is None else extract.vendor, | |
| None if extract is None else (extract.date.isoformat() if extract.date else None), | |
| None if extract is None else to_cents(extract.tax), | |
| None if extract is None else to_cents(extract.total), | |
| None if extract is None else extract.currency, | |
| ocr_text, | |
| None if extract is None else extract.model_dump_json(), | |
| error, | |
| now, | |
| now, | |
| ), | |
| ) | |
| receipt_id = int(cur.lastrowid) | |
| if extract is not None: | |
| for item in extract.line_items: | |
| con.execute( | |
| """ | |
| INSERT INTO line_items ( | |
| receipt_id, description, qty, unit_price_cents, amount_cents, sku | |
| ) VALUES (?, ?, ?, ?, ?, ?) | |
| """, | |
| ( | |
| receipt_id, | |
| item.description, | |
| item.qty, | |
| to_cents(item.unit_price), | |
| to_cents(item.amount), | |
| item.sku, | |
| ), | |
| ) | |
| con.commit() | |
| return receipt_id | |
| def update_receipt_status( | |
| con: sqlite3.Connection, | |
| receipt_id: int, | |
| status: ReceiptStatus, | |
| *, | |
| error: str | None = None, | |
| ) -> None: | |
| con.execute( | |
| "UPDATE receipts SET status = ?, error = ?, updated_at = ? WHERE id = ?", | |
| (status.value, error, _utc_now(), receipt_id), | |
| ) | |
| con.commit() | |
| def update_extract( | |
| con: sqlite3.Connection, | |
| receipt_id: int, | |
| extract: ReceiptExtract, | |
| status: ReceiptStatus, | |
| ) -> None: | |
| con.execute("DELETE FROM line_items WHERE receipt_id = ?", (receipt_id,)) | |
| con.execute( | |
| """ | |
| UPDATE receipts SET | |
| status = ?, doc_kind = ?, category = ?, vendor = ?, receipt_date = ?, | |
| tax_cents = ?, total_cents = ?, currency = ?, extract_json = ?, | |
| error = NULL, updated_at = ? | |
| WHERE id = ? | |
| """, | |
| ( | |
| status.value, | |
| extract.doc_kind, | |
| extract.category, | |
| extract.vendor, | |
| extract.date.isoformat() if extract.date else None, | |
| to_cents(extract.tax), | |
| to_cents(extract.total), | |
| extract.currency, | |
| extract.model_dump_json(), | |
| _utc_now(), | |
| receipt_id, | |
| ), | |
| ) | |
| for item in extract.line_items: | |
| con.execute( | |
| """ | |
| INSERT INTO line_items ( | |
| receipt_id, description, qty, unit_price_cents, amount_cents, sku | |
| ) VALUES (?, ?, ?, ?, ?, ?) | |
| """, | |
| ( | |
| receipt_id, | |
| item.description, | |
| item.qty, | |
| to_cents(item.unit_price), | |
| to_cents(item.amount), | |
| item.sku, | |
| ), | |
| ) | |
| con.commit() | |
| def delete_receipt(con: sqlite3.Connection, receipt_id: int, *, unlink_file: bool = True) -> bool: | |
| row = con.execute( | |
| "SELECT source_path FROM receipts WHERE id = ?", (receipt_id,) | |
| ).fetchone() | |
| if row is None: | |
| return False | |
| try: | |
| con.execute("DELETE FROM receipt_vec WHERE receipt_id = ?", (receipt_id,)) | |
| except sqlite3.Error: | |
| pass | |
| con.execute("DELETE FROM receipts WHERE id = ?", (receipt_id,)) | |
| con.commit() | |
| if unlink_file: | |
| path = Path(row["source_path"] or "") | |
| if path.is_file(): | |
| try: | |
| path.unlink() | |
| except OSError: | |
| pass | |
| return True | |
| def set_line_match( | |
| con: sqlite3.Connection, | |
| line_id: int, | |
| hit: MatchHit, | |
| ) -> None: | |
| con.execute( | |
| """ | |
| UPDATE line_items SET match_catalog_id = ?, match_score = ?, match_status = ? | |
| WHERE id = ? | |
| """, | |
| (hit.catalog_id, hit.similarity, hit.band.value, line_id), | |
| ) | |
| con.commit() | |
| def list_receipts(con: sqlite3.Connection, *, status: str | None = None, limit: int = 50) -> list[sqlite3.Row]: | |
| if status: | |
| return list( | |
| con.execute( | |
| "SELECT * FROM receipts WHERE status = ? ORDER BY id DESC LIMIT ?", | |
| (status, limit), | |
| ) | |
| ) | |
| return list(con.execute("SELECT * FROM receipts ORDER BY id DESC LIMIT ?", (limit,))) | |
| def get_receipt(con: sqlite3.Connection, receipt_id: int) -> sqlite3.Row | None: | |
| return con.execute("SELECT * FROM receipts WHERE id = ?", (receipt_id,)).fetchone() | |
| def list_line_items(con: sqlite3.Connection, receipt_id: int) -> list[sqlite3.Row]: | |
| return list( | |
| con.execute("SELECT * FROM line_items WHERE receipt_id = ? ORDER BY id", (receipt_id,)) | |
| ) | |
| def add_catalog_item( | |
| con: sqlite3.Connection, | |
| *, | |
| description: str, | |
| sku: str | None = None, | |
| vendor: str | None = None, | |
| size: str | None = None, | |
| unit_price_cents: int | None = None, | |
| metadata: dict[str, Any] | None = None, | |
| ) -> int: | |
| cur = con.execute( | |
| """ | |
| INSERT INTO catalog (sku, vendor, description, size, unit_price_cents, metadata_json) | |
| VALUES (?, ?, ?, ?, ?, ?) | |
| """, | |
| ( | |
| sku, | |
| vendor, | |
| description, | |
| size, | |
| unit_price_cents, | |
| None if metadata is None else json.dumps(metadata), | |
| ), | |
| ) | |
| con.commit() | |
| return int(cur.lastrowid) | |
| def list_catalog(con: sqlite3.Connection, limit: int = 200) -> list[sqlite3.Row]: | |
| return list(con.execute("SELECT * FROM catalog ORDER BY id DESC LIMIT ?", (limit,))) | |
| def find_catalog_by_sku(con: sqlite3.Connection, sku: str) -> sqlite3.Row | None: | |
| return con.execute( | |
| "SELECT * FROM catalog WHERE sku = ? COLLATE NOCASE LIMIT 1", (sku,) | |
| ).fetchone() | |
| def upsert_vector(con: sqlite3.Connection, table: str, pk_col: str, pk: int, vec: list[float]) -> None: | |
| if serialize_float32 is None: | |
| raise VecLoadError("sqlite-vec missing") | |
| blob = serialize_float32(vec) | |
| con.execute(f"DELETE FROM {table} WHERE {pk_col} = ?", (pk,)) | |
| con.execute( | |
| f"INSERT INTO {table}({pk_col}, embedding) VALUES (?, ?)", | |
| (pk, blob), | |
| ) | |
| con.commit() | |
| def knn( | |
| con: sqlite3.Connection, | |
| table: str, | |
| pk_col: str, | |
| query: list[float], | |
| *, | |
| k: int = 5, | |
| ) -> list[tuple[int, float]]: | |
| if serialize_float32 is None: | |
| raise VecLoadError("sqlite-vec missing") | |
| blob = serialize_float32(query) | |
| rows = con.execute( | |
| f""" | |
| SELECT {pk_col} AS id, distance | |
| FROM {table} | |
| WHERE embedding MATCH ? | |
| AND k = ? | |
| """, | |
| (blob, k), | |
| ).fetchall() | |
| return [(int(row["id"]), float(row["distance"])) for row in rows] | |
| def receipt_to_extract(row: sqlite3.Row) -> ReceiptExtract | None: | |
| raw = row["extract_json"] | |
| if not raw: | |
| return None | |
| return ReceiptExtract.model_validate_json(raw) | |
| def iter_rows(rows: list[sqlite3.Row]) -> Iterator[dict[str, Any]]: | |
| for row in rows: | |
| yield dict(row) | |