Files
rar-autopass/app/config.py
Claude Agent 77088daf29 feat(T2): reconciliere anti-duplicat + retry/backoff + recuperare orfane
Inchide bucla de trimitere (plan.md sect. 4 worker, failure registry).

- app/reconcile.py: match_finalizata pe vin+dataPrestatie+odometruFinal (int),
  alege id maxim la duplicate (RAR accepta duplicate, confirmat live)
- app/rar_client.get_finalizate: parseaza data.content (descoperit live ca
  ruta = GET /prezentari/getAllPrezentariFinalizate; filtrele nu merg pe test)
- app/worker rescris:
  - recuperare orfane (rand 'sending' peste lease = worker mort mid-POST)
  - pe eroare tranzitorie/timeout: reconciliere INAINTE de re-send (anti-duplicat);
    daca recordul exista la RAR -> sent fara re-POST
  - retry/backoff exponential; peste worker_max_retries -> error + banner
  - re-login la token expirat (JWT 30h)
- schema: coloana next_attempt_at (backoff) + migrare aditiva in init_db
- config: worker_sending_lease_s, worker_retry_base_s/max_s, worker_max_retries
- contract: documentata ruta+forma getAllPrezentariFinalizate (verificat live)

Verify: pytest 54 passed (15 noi T2) + validare live (reconciliere record 68514).

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

81 lines
3.1 KiB
Python

"""Configurare gateway. Env vars (prefix AUTOPASS_) + valori implicite.
NU stocheaza parole RAR. Credentialele RAR vin per-cerere de la ROAAUTO
(vezi plan.md sect. 5). Helper-ul `load_test_credentials` citeste blocul
<test> din settings.xml DOAR pentru dev local / probe pe mediul de test.
"""
from __future__ import annotations
import xml.etree.ElementTree as ET
from functools import lru_cache
from pathlib import Path
from pydantic_settings import BaseSettings, SettingsConfigDict
ROOT = Path(__file__).resolve().parent.parent
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="AUTOPASS_", env_file=".env", extra="ignore")
# --- Bază de date ---
db_path: Path = ROOT / "data" / "autopass.db"
# --- RAR ---
rar_env: str = "test" # "test" | "prod"
rar_base_url_test: str = "https://apps.rarom.ro/test-rar-autopass"
rar_base_url_prod: str = "https://apps.rarom.ro/rar-autopass"
# WAF-ul RAR da 403 fara User-Agent de browser (confirmat live, vezi
# docs/api-rar-contract.md). Toate apelurile httpx il trimit.
http_user_agent: str = "Mozilla/5.0"
http_timeout_s: float = 30.0
# --- Worker ---
worker_poll_interval_s: float = 5.0
worker_heartbeat_stale_s: int = 30 # /healthz considera worker-ul mort peste atat
# In schelet send-ul e DEZACTIVAT (nu trimite la RAR). Activeaza-l explicit
# pentru proba end-to-end. Reconcilierea/retry-ul complet = T2.
worker_send_enabled: bool = False
# Dev: foloseste creds <test> din settings.xml pt login worker. In productie
# creds vin per-cerere de la ROAAUTO (T2) — lasa False.
worker_use_test_creds: bool = False
# T2 — recuperare orfane + retry/backoff:
worker_sending_lease_s: int = 120 # rand 'sending' mai vechi de atat = orfan (worker mort mid-POST)
worker_retry_base_s: int = 5 # backoff = base * 2^retry (plafonat la max)
worker_retry_max_s: int = 300
worker_max_retries: int = 8 # peste atat -> error + banner (pana persistenta)
@property
def rar_base_url(self) -> str:
return self.rar_base_url_prod if self.rar_env == "prod" else self.rar_base_url_test
@lru_cache
def get_settings() -> Settings:
return Settings()
def load_test_credentials(settings_xml: Path | None = None) -> dict | None:
"""Citeste credentialele <test> din settings.xml (dev local / probe test).
Intoarce {"email", "password"} sau None daca fisierul lipseste / e template.
NU se foloseste in productie — acolo creds vin per-cerere de la ROAAUTO.
"""
path = settings_xml or (ROOT / "settings.xml")
if not path.exists():
return None
try:
root = ET.parse(path).getroot()
node = root.find("./test/credentials")
if node is None:
return None
email = (node.findtext("email") or "").strip()
password = (node.findtext("password") or "").strip()
if not email or not password or email.startswith("EMAIL_"):
return None
return {"email": email, "password": password}
except ET.ParseError:
return None