"""Klient HTTP do warstwy bazodanowej. Jedyny punkt styku w dół. Gdyby warstwa bazodanowa zmieniła implementację (Excel→SQL), tutaj nie zmienia się NIC — kontrakt /search jest stały. """ from __future__ import annotations import os from typing import Any import httpx from app import link_crypto from app.config import settings def _auth_headers() -> dict[str, str]: """Token międzywarstwowy (LOG-32) — pusty, gdy ochrona wyłączona.""" token = os.getenv("INTERNAL_TOKEN", "") return {"X-Astrololo-Token": token} if token else {} def _link() -> link_crypto.Link | None: """Klucz łącza logika↔dane. Czytany przy każdym wywołaniu, bo konfiguracja może się zmienić bez restartu procesu (testy, podmiana sekretu).""" key = link_crypto.key_from_env(link_crypto.ENV_LOGIC_DATA) return link_crypto.Link(key) if key else None class DataClient: """Rozmówca warstwy danych, opcjonalnie ZWIĄZANY Z PULĄ KONTA (PRE-29). Pula siedzi w INSTANCJI, nie w sygnaturach metod. Gdyby jechała argumentem, trzeba by ją przeprowadzić przez protokół DataSource i przez build_report — czyli przez kod, który o kontach nie ma prawa nic wiedzieć. A przy okazji każde nowe wywołanie byłoby okazją, żeby o nią zapomnieć i sięgnąć nie tam. Puste = cały udział, czyli zachowanie produkcji.""" def __init__(self, base_url: str | None = None, tenant: str = "") -> None: self.base_url = (base_url or settings.data_url).rstrip("/") self.tenant = tenant or "" def search( self, key: str, value: str, exact: bool, limit: int, fields: list[str] | None = None, ) -> dict[str, Any]: payload = {"key": key, "value": value, "exact": exact, "limit": limit, "fields": fields, "tenant": self.tenant} with httpx.Client(timeout=max(settings.http_timeout, 30.0)) as client: return link_crypto.call_json(client, "POST", f"{self.base_url}/search", payload=payload, headers=_auth_headers(), link=_link()) def bases(self) -> dict[str, Any]: """Lista baz na udziale + metaopis (DAN-15) — same metadane, bez treści.""" with httpx.Client(timeout=settings.http_timeout) as client: return link_crypto.call_json(client, "GET", f"{self.base_url}/bases", headers=_auth_headers(), link=_link()) # ── zarządzanie plikami baz (DAN-27) ──────────────────────────────── # Jedna metoda na trasę, bez sprytnego generyka: te wywołania różnią się # skutkiem (odczyt / zapis / skasowanie), a ujednolicenie ich w jedno # `call(path, payload)` zaciera tę różnicę dokładnie tam, gdzie jest ważna. def files_list(self, for_admin: bool = False) -> dict[str, Any]: return self._files_post("/files", {"for_admin": for_admin, "tenant": self.tenant}) def files_status(self, path: str, status: str, by: str = "") -> dict[str, Any]: return self._files_post("/files/status", {"path": path, "status": status, "by": by, "tenant": self.tenant}) def files_upload(self, filename: str, content_b64: str, by: str = "") -> dict[str, Any]: return self._files_post("/files/upload", {"filename": filename, "content_b64": content_b64, "by": by, "tenant": self.tenant}) def files_delete(self, path: str) -> dict[str, Any]: return self._files_post("/files/delete", {"path": path, "tenant": self.tenant}) def files_rules(self, rules: dict) -> dict[str, Any]: return self._files_post("/files/rules", {"rules": rules, "tenant": self.tenant}) def _files_post(self, path: str, payload: dict) -> dict[str, Any]: with httpx.Client(timeout=settings.http_timeout) as client: return link_crypto.call_json(client, "POST", f"{self.base_url}{path}", payload=payload, headers=_auth_headers(), link=_link()) def health(self) -> dict[str, Any]: # /health celowo poza szyfrowaniem — pukają tu sondy k8s, które klucza # nie mają, a nie przechodzi tędy nic z baz. with httpx.Client(timeout=settings.http_timeout) as client: r = client.get(f"{self.base_url}/health", headers=_auth_headers()) r.raise_for_status() return r.json()