grantforge-api / backend /integrations /sudop_client.py
GrantForge Bot
Deploy sha-9a5957fcdef15b7e2623f8b147cda6026475aee0 — source build (no GHCR)
3a3734f
Raw
History Blame Contribute Delete
7.17 kB
"""
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)