"""Medii RAR per cont (PRD 5.20): disponibilitate + default efectiv. Sursa UNICA de adevar pentru REQ-DISP / REQ-DEFAULT: vizibilitatea selector/toggle in UI, validarea tintei in API si decizia worker-ului citesc TOATE de aici, ca sa decida identic. Un mediu ('test'|'prod') e *disponibil* pentru un cont daca e activat (bifa) SI are credentiale (slot per-mediu non-gol). Din disponibilitate decurge tot UX-ul: - 0 medii -> nicio tinta; trimiterea web e blocata, API cade pe ancora globala. - 1 mediu -> tinta implicita (acel mediu), fara selector. - 2 medii -> selector la import + toggle in statusbar + alegere in API. Functii PURE (fara DB) peste un rand de cont (sqlite3.Row sau dict). Helperele cu `conn` incarca randul si deleaga. """ from __future__ import annotations import sqlite3 from typing import Any VALID_ENVS: tuple[str, str] = ("test", "prod") def _field(account: Any, key: str, default: Any = None) -> Any: """Citire toleranta a unui camp de cont (dict sau sqlite3.Row, camp posibil absent).""" if account is None: return default if isinstance(account, dict): return account.get(key, default) try: return account[key] # sqlite3.Row except (IndexError, KeyError): return default def _are_creds(account: Any, env: str) -> bool: creds = _field(account, f"rar_creds_{env}_enc", None) return bool(creds and str(creds).strip()) def _enabled(account: Any, env: str) -> bool: return int(_field(account, f"rar_{env}_enabled", 0) or 0) == 1 def medii_disponibile(account: Any) -> list[str]: """Subset din ('test','prod') = activat AND creds prezente. Ordine stabila test str | None: """Mediul tinta implicit al contului (REQ-DEFAULT). Mereu unul din mediile disponibile: default-ul contului daca inca e disponibil, altfel singurul disponibil; daca 0 disponibile -> None (nicio tinta). """ disp = medii_disponibile(account) if not disp: return None default = _field(account, "rar_env_default", "prod") if default in disp: return default return disp[0] # --------------------------------------------------------------------------- # # Helpere cu conexiune # # --------------------------------------------------------------------------- # _ACCOUNT_ENV_COLS = ( "id, rar_test_enabled, rar_prod_enabled, " "rar_creds_test_enc, rar_creds_prod_enc, rar_env_default" ) def load_account_env(conn: sqlite3.Connection, account_id: int) -> sqlite3.Row | None: """Randul de cont cu exact coloanele de mediu (pentru medii_disponibile/rar_env_efectiv).""" from .mapping import account_or_default return conn.execute( f"SELECT {_ACCOUNT_ENV_COLS} FROM accounts WHERE id=?", (account_or_default(account_id),), ).fetchone() def medii_disponibile_cont(conn: sqlite3.Connection, account_id: int) -> list[str]: return medii_disponibile(load_account_env(conn, account_id)) def rar_env_efectiv_cont(conn: sqlite3.Connection, account_id: int) -> str | None: return rar_env_efectiv(load_account_env(conn, account_id)) # --------------------------------------------------------------------------- # # Exceptie si rezolvator de mediu tinta (US-004, dependent de US-002) # # --------------------------------------------------------------------------- # class MediuIndisponibil(Exception): """Mediul RAR cerut e valid dar nu e disponibil pentru contul dat. Atribute -------- env: mediul cerut (ex. 'test') disponibile: lista mediilor disponibile pentru cont in momentul erorii """ def __init__(self, env: str, disponibile: list[str]) -> None: self.env = env self.disponibile = disponibile super().__init__( f"mediu indisponibil: {env!r} (disponibile: {disponibile!r})" ) def rezolva_rar_env( conn: sqlite3.Connection, account_id: int, cerut: str | None = None, ) -> str: """Determina mediul RAR tinta pentru un submission la ingestie. Precedenta stricta (de la cea mai mare la cea mai mica): 1. `cerut` explicit si disponibil -> intoarce `cerut`. 2. `cerut` explicit dar indisponibil -> ridica MediuIndisponibil. 3. `cerut` invalid (nu in VALID_ENVS) -> ridica ValueError (fara fallback silentios). 4. `cerut` None -> incearca rar_env_efectiv_cont (default-ul contului). 5. Daca contul nu are niciun mediu disponibil (rar_env_efectiv_cont == None) -> cade pe ancora globala get_settings().rar_env, normalizata la VALID_ENVS. Acest fallback e intentionat (PRD 5.20 ยง2 Non-Goals): AUTOPASS_RAR_ENV ramane ancora de migrare si fallback pentru actiuni fara cont (keepalive, canal API cu creds efemere pe conturi nou-create fara medii configurate). Ridica ------ ValueError -- `cerut` nu e in VALID_ENVS MediuIndisponibil -- `cerut` e valid dar nu e disponibil pentru cont """ if cerut is not None: if cerut not in VALID_ENVS: raise ValueError(f"mediu invalid: {cerut!r}") disp = medii_disponibile_cont(conn, account_id) if cerut not in disp: raise MediuIndisponibil(cerut, disp) return cerut # cerut e None: incearca default-ul contului efectiv = rar_env_efectiv_cont(conn, account_id) if efectiv is not None: return efectiv # Ancora globala: 0 medii disponibile pe cont -> fallback la AUTOPASS_RAR_ENV. from .config import get_settings global_env = get_settings().rar_env if global_env in VALID_ENVS: return global_env return "test" # rar_env invalid in config -> cel mai sigur default