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>
84 lines
2.5 KiB
Python
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"
|