Spaces:
Running
Running
| """ | |
| 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) | |