Files
rar-autopass/app/main.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

84 lines
2.5 KiB
Python

"""Aplicatia FastAPI: API v1 + dashboard web + /healthz + /metrics.
Worker-ul ruleaza ca PROCES SEPARAT (python -m app.worker), NU ca task aici
(plan.md sect. 4: un worker mort nu trebuie sa lase containerul "sanatos").
Pornire dev: uvicorn app.main:app --reload
"""
from __future__ import annotations
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from fastapi import FastAPI
from fastapi.responses import PlainTextResponse
from . import __version__
from .api.v1.router import router as api_v1_router
from .config import get_settings
from .db import get_connection, init_db, queue_depth, read_heartbeat
from .web.routes import router as web_router
@asynccontextmanager
async def lifespan(app: FastAPI):
init_db()
yield
app = FastAPI(title="Gateway RAR AUTOPASS", version=__version__, lifespan=lifespan)
app.include_router(api_v1_router)
app.include_router(web_router)
@app.get("/healthz")
def healthz() -> dict:
"""Sanatate: worker viu + ultimul login RAR reusit + adancime coada.
Pica (200 cu ok=False / sau folosit de orchestrator) cand worker-ul e mort
-> semnal de restart (plan.md sect. 8). Intoarce 200 mereu cu detalii;
orchestratorul decide pe campul `worker_alive`.
"""
settings = get_settings()
conn = get_connection()
try:
hb = read_heartbeat(conn)
depth = queue_depth(conn)
finally:
conn.close()
worker_alive = False
last_beat = hb["last_beat"] if hb else None
if last_beat:
try:
age = (datetime.now(timezone.utc) - datetime.fromisoformat(last_beat)).total_seconds()
worker_alive = age <= settings.worker_heartbeat_stale_s
except ValueError:
worker_alive = False
return {
"ok": True,
"version": __version__,
"rar_env": settings.rar_env,
"worker_alive": worker_alive,
"last_beat": last_beat,
"last_rar_login_ok": hb["last_rar_login_ok"] if hb else None,
"queue_depth": depth,
}
@app.get("/metrics", response_class=PlainTextResponse)
def metrics() -> str:
"""Metrici text simplu (submissions pe status + backlog). Format Prometheus-lite."""
conn = get_connection()
try:
rows = conn.execute("SELECT status, COUNT(*) AS n FROM submissions GROUP BY status").fetchall()
finally:
conn.close()
lines = ["# submissions pe status"]
for r in rows:
lines.append(f'autopass_submissions{{status="{r["status"]}"}} {r["n"]}')
return "\n".join(lines) + "\n"