49c22c9385
Clean re-implementation of the full product (not a port of the old M3 tree): - P0 foundation: 13-table schema + per-call sqlite factory (thread-safe), FastAPI app (health, JSON logging + secret redaction, CORS, startup guard). - P1 M1 Capture: mask/sheetKeep, geometry/rect (min-area rect), measure, png crop/inventory/overlay, EXIF decodeOrientedBitmap. - P2 M2 Photos: stateful capture session (batch, pick-cal, apply mask/crop to batch) + photos-ready lineup. - P3 M3 Create: real backend (crypto, woo client, taxonomy sync, pricing, inference Call1/Call2, review, publish, settings, slabs, photos) + real FE review/publish/settings screens consuming the API (no hardcoded Woo ids). - P4 gate exit: check_requirements + check_coverage both exit 0 (137/155 IDs, 0 gaps); backend 130/130, frontend 150/150, build green. Cite: docs/REQUIREMENTS.md (155 rows), docs/MILESTONES.md M1-M3.
55 lines
2.3 KiB
Python
55 lines
2.3 KiB
Python
"""Clean db factory (stdlib sqlite3 only).
|
|
|
|
get_connection opens a FRESH connection on every call (row_factory + pragmas
|
|
applied). The P0.1 cached-singleton was NOT safe: FastAPI runs sync endpoints
|
|
in anyio worker threads and async endpoints on the portal loop, and a cached
|
|
connection bound on the main thread got silently re-opened against the default
|
|
path once the app's event loop ran — tests passed only because a stale
|
|
`slab_demo.db` in the repo masked the misdirection. A per-call connection makes
|
|
the path binding explicit and thread-local by construction. Callers own the
|
|
connection's lifetime (close when done; endpoints close via context).
|
|
|
|
init_db applies schema.sql + upserts schema_version (v=1).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sqlite3
|
|
from pathlib import Path
|
|
|
|
|
|
def get_connection(db_path: str | None = None) -> sqlite3.Connection:
|
|
if db_path is None:
|
|
db_path = os.environ.get("SLAB_DB_PATH") or "slab_demo.db"
|
|
db_path = str(Path(db_path).resolve())
|
|
# isolation_level=None -> autocommit mode. The P0.1 default (deferred
|
|
# transactions) left a read-transaction open for the whole life of an
|
|
# endpoint connection, which (a) blocked other writers with "database is
|
|
# locked" and (b) made conn.commit() inside an open txn a no-op for
|
|
# multi-statement units. Autocommit + explicit BEGIN for the few
|
|
# multi-statement units (taxonomy sync) is the honest model for a
|
|
# per-call-connection factory.
|
|
conn = sqlite3.connect(db_path, check_same_thread=False, isolation_level=None)
|
|
conn.row_factory = sqlite3.Row
|
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
conn.execute("PRAGMA foreign_keys=ON")
|
|
conn.execute("PRAGMA busy_timeout=5000")
|
|
conn.execute("PRAGMA synchronous=NORMAL")
|
|
return conn
|
|
|
|
|
|
def init_db(db_path: str | None = None) -> None:
|
|
"""Apply schema.sql (CREATE IF NOT EXISTS) and ensure schema_version row=1."""
|
|
schema_path = Path(__file__).parent / "schema.sql"
|
|
if not schema_path.exists():
|
|
raise FileNotFoundError(f"Schema not found: {schema_path}")
|
|
conn = get_connection(db_path)
|
|
try:
|
|
with open(schema_path, "r", encoding="utf-8") as f:
|
|
schema = f.read()
|
|
conn.executescript(schema)
|
|
conn.execute("INSERT OR REPLACE INTO schema_version (version) VALUES (1)")
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|