""" Klient SUDOP (System Udostępniania Danych o Pomocy Publicznej). API: https://api-sudop.uokik.gov.pl/swagger/ — spec: sudop-api.yml Przepływ asynchroniczny: wyszukanie → kolejka (303) → wynik (JSON). """ from __future__ import annotations import logging import os import time from typing import Any, Dict, List, Optional import requests logger = logging.getLogger(__name__) DEFAULT_BASE = "https://api-sudop.uokik.gov.pl/sudop-api/1.0.0" DE_MINIMIS_CODES = {"e1", "e2", "e3"} class SudopClient: def __init__( self, base_url: Optional[str] = None, poll_interval: float = 1.5, max_polls: int = 12, timeout: int = 15, ): self.base_url = (base_url or os.environ.get("SUDOP_API_BASE") or DEFAULT_BASE).rstrip("/") self.poll_interval = poll_interval self.max_polls = max_polls self.timeout = timeout def _abs_url(self, location: str) -> str: if location.startswith("http"): return location root = "https://api-sudop.uokik.gov.pl" if not location.startswith("/"): location = f"/{location}" return f"{root}{location}" def _poll_until_result(self, queue_path: str) -> Optional[Dict[str, Any]]: url = self._abs_url(queue_path) for attempt in range(self.max_polls): if attempt: time.sleep(self.poll_interval) try: resp = requests.get(url, timeout=self.timeout, allow_redirects=False) except Exception as e: logger.debug(f"[SUDOP] poll error: {e}") continue if resp.status_code == 303: result_loc = resp.headers.get("Location", "") if "/wynik/" in result_loc: return self._fetch_result(result_loc) elif resp.status_code == 200: try: return resp.json() except Exception: pass elif resp.status_code == 429: time.sleep(self.poll_interval * 2) return None def _fetch_result(self, result_path: str) -> Optional[Dict[str, Any]]: url = self._abs_url(result_path) try: resp = requests.get(url, timeout=self.timeout) if resp.status_code == 200: return resp.json() except Exception as e: logger.debug(f"[SUDOP] result fetch error: {e}") return None def search_aid_by_nip(self, nip: str, page: int = 1) -> Dict[str, Any]: """Wyszukuje przypadki pomocy publicznej dla beneficjenta (NIP).""" nip_clean = "".join(c for c in (nip or "") if c.isdigit()) if len(nip_clean) != 10: return { "configured": True, "nip": nip_clean, "error": "invalid_nip", "de_minimis_history": [], "total_aid_eur": None, } search_url = f"{self.base_url}/api/przypadki-pomocy" params = {"nip-beneficjenta": nip_clean, "strona": page} try: resp = requests.get( search_url, params=params, timeout=self.timeout, allow_redirects=False ) except Exception as e: logger.warning(f"[SUDOP] search failed: {e}") return { "configured": True, "nip": nip_clean, "error": str(e), "de_minimis_history": [], "total_aid_eur": None, "message": "SUDOP API niedostępne — weryfikacja ręczna.", } if resp.status_code == 303: queue_loc = resp.headers.get("Location", "") if not queue_loc: return self._empty_result(nip_clean, "missing_queue_location") raw = self._poll_until_result(queue_loc) return self._normalize_response(nip_clean, raw) if resp.status_code == 200: try: return self._normalize_response(nip_clean, resp.json()) except Exception: pass return self._empty_result(nip_clean, f"http_{resp.status_code}") def _empty_result(self, nip: str, reason: str) -> Dict[str, Any]: return { "configured": True, "nip": nip, "error": reason, "de_minimis_history": [], "total_aid_eur": 0.0, "total_aid_pln": 0.0, "aid_events_count": 0, "message": "Brak danych SUDOP lub błąd zapytania.", } def _normalize_response(self, nip: str, raw: Optional[Dict[str, Any]]) -> Dict[str, Any]: if not raw: return self._empty_result(nip, "timeout_or_empty") events: List[Dict[str, Any]] = raw.get("wyniki") or [] de_minimis: List[Dict[str, Any]] = [] total_eur = 0.0 total_pln = 0.0 for ev in events: przeznaczenie = (ev.get("przeznaczenie-pomocy-kod") or "").lower() is_de_minimis = przeznaczenie in DE_MINIMIS_CODES or "de minimis" in ( ev.get("przeznaczenie-pomocy-nazwa") or "" ).lower() eur_val = _parse_amount(ev.get("wartosc-brutto-eur")) pln_val = _parse_amount(ev.get("wartosc-brutto-pln")) total_eur += eur_val total_pln += pln_val entry = { "date": ev.get("dzien-udzielenia-pomocy"), "provider": ev.get("nazwa-udzielajacego-pomocy"), "purpose": ev.get("przeznaczenie-pomocy-nazwa"), "purpose_code": przeznaczenie, "amount_eur": eur_val, "amount_pln": pln_val, "is_de_minimis": is_de_minimis, } if is_de_minimis: de_minimis.append(entry) return { "configured": True, "nip": nip, "aid_events_count": raw.get("liczba-wynikow", len(events)), "de_minimis_history": de_minimis, "total_aid_eur": round(total_eur, 2), "total_aid_pln": round(total_pln, 2), "de_minimis_total_eur": round(sum(d["amount_eur"] for d in de_minimis), 2), "message": ( f"Znaleziono {len(events)} przypadków pomocy " f"({len(de_minimis)} de minimis)." if events else "Brak zarejestrowanej pomocy publicznej w SUDOP." ), } def _parse_amount(value: Any) -> float: if value is None: return 0.0 try: return float(str(value).replace(",", ".").replace(" ", "")) except (TypeError, ValueError): return 0.0 def fetch_sudop_for_nip(nip: str) -> Dict[str, Any]: """Funkcja pomocnicza dla registry_enrichment.""" if os.environ.get("SUDOP_DISABLED", "").lower() in ("1", "true", "yes"): return { "configured": False, "nip": nip, "de_minimis_history": [], "total_aid_eur": None, "message": "SUDOP wyłączone (SUDOP_DISABLED).", } return SudopClient().search_aid_by_nip(nip)