Files
rar-autopass/app/db.py
Claude Agent f1b5f1f80f feat: schelet gateway FastAPI (API v1 + worker + dashboard + SQLite WAL)
Structura repo conform plan.md sect. 4, booteaza cu /healthz verde:
- app/main.py: FastAPI (lifespan init_db), /healthz (worker viu + last login + queue), /metrics
- app/api/v1: POST /v1/prezentari (enqueue + dedup idempotency UNIQUE), GET prezentari/{id}, nomenclator, mapari
- app/rar_client.py: client RAR real (login/JWT, nomenclator, postPrezentare, getFinalizate) cu User-Agent obligatoriu (fix WAF 403)
- app/worker: proces separat, claim atomic BEGIN IMMEDIATE, heartbeat, login+send (send dezactivat by default)
- app/web: dashboard Jinja2+HTMX (coada, banner alerta blocate, worker viu/mort, stari empty)
- app/db.py + schema.sql: SQLite WAL, tabele accounts/api_keys/operations_mapping/nomenclator_rar/submissions/worker_heartbeat
- app/idempotency.py + payload.py: hash continut canonic + builder payload (status FINALIZATA, fara tipPrestatie)
- Dockerfile + docker-compose.yml (api+worker, volum SQLite persistent, restart:always)
- tools/import_dbf.py: stub T5

Verificat live: login prin rar_client OK (token 259), nomenclator 18 coduri, worker heartbeat -> /healthz worker_alive=True.
Ramas: T3 validare Pydantic, T4 snapshot payload, T2 reconciliere/retry worker, T5 import DBF, auth API-key, middleware redactare creds, criptare PII.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 12:04:11 +00:00

63 lines
2.0 KiB
Python

"""Acces SQLite (WAL). Conexiune per-thread, schema idempotenta, heartbeat worker."""
from __future__ import annotations
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
from .config import get_settings
_SCHEMA = Path(__file__).resolve().parent / "schema.sql"
def _connect(db_path: Path) -> sqlite3.Connection:
db_path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(db_path, timeout=15.0, isolation_level=None) # autocommit; tranzactii explicite
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode = WAL")
conn.execute("PRAGMA foreign_keys = ON")
conn.execute("PRAGMA busy_timeout = 15000")
return conn
def get_connection() -> sqlite3.Connection:
"""Conexiune noua catre baza configurata. Apelantul o inchide."""
return _connect(get_settings().db_path)
def init_db() -> None:
"""Creeaza schema daca lipseste. Idempotent — sigur la fiecare boot."""
conn = get_connection()
try:
conn.executescript(_SCHEMA.read_text(encoding="utf-8"))
finally:
conn.close()
def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat(timespec="seconds")
def write_heartbeat(conn: sqlite3.Connection, *, rar_login_ok: bool = False, detail: str = "") -> None:
"""Worker bate la fiecare iteratie. last_rar_login_ok se actualizeaza doar la login reusit."""
if rar_login_ok:
conn.execute(
"UPDATE worker_heartbeat SET last_beat=?, last_rar_login_ok=?, detail=? WHERE id=1",
(_now_iso(), _now_iso(), detail),
)
else:
conn.execute(
"UPDATE worker_heartbeat SET last_beat=?, detail=? WHERE id=1",
(_now_iso(), detail),
)
def read_heartbeat(conn: sqlite3.Connection) -> sqlite3.Row | None:
return conn.execute("SELECT * FROM worker_heartbeat WHERE id=1").fetchone()
def queue_depth(conn: sqlite3.Connection) -> int:
row = conn.execute("SELECT COUNT(*) AS n FROM submissions WHERE status='queued'").fetchone()
return int(row["n"]) if row else 0