feat: /api/v2/analiza/* endpoints - sport analytics backend

This commit is contained in:
Damir Radulic
2026-05-16 00:28:12 +02:00
parent 7ca5d7d94e
commit aca5051418
1355 changed files with 321891 additions and 4128 deletions
+287 -24
View File
@@ -1,4 +1,7 @@
#!/usr/bin/env python3
from dotenv import load_dotenv
load_dotenv('/opt/rinet-gpu/.env.master')
# auto-added by patch_scrapers_with_dotenv.sh
"""
pgz_sport_api.py - FastAPI backend za PGŽ Sportski savez ERP/CRM
Author: Damir Radulić (damir@rinet.one / dradulic@outlook.com)
@@ -28,7 +31,7 @@ import psycopg2.extras
from pgz_sport_v2_router import router as v2_router
import os
DB = dict(host='10.10.0.2', port=6432, dbname='rinet_v3', user='rinet', password='R1net2026!SecureDB#v7')
DB = dict(host='10.10.0.2', port=6432, dbname='rinet_v3', user='rinet', password=os.environ["DB_PASSWORD"])
ADMIN_TOKEN = 'admin-pgz-2026'
@@ -624,7 +627,7 @@ def list_savezi(authorization: Optional[str] = Header(None), q: Optional[str] =
where = "WHERE aktivan"
params = []
if q:
where += " AND (naziv ILIKE %s OR sport ILIKE %s)"
where += " AND (public.f_unaccent(naziv) ILIKE public.f_unaccent(%s) OR sport ILIKE %s)"
params = [f"%{q}%", f"%{q}%"]
if razina:
where += " AND razina = %s"; params.append(razina)
@@ -711,7 +714,7 @@ def list_klubovi(authorization: Optional[str] = Header(None), q: Optional[str] =
if region:
where.append("v.region ILIKE %s"); params.append(region)
if grad:
where.append("v.grad ILIKE %s"); params.append(f"%{grad}%")
where.append("public.f_unaccent(v.grad) ILIKE public.f_unaccent(%s)"); params.append(f"%{grad}%")
if sport:
where.append("v.sport ILIKE %s"); params.append(f"%{sport}%")
if financiran is not None:
@@ -794,11 +797,46 @@ def get_klub(klub_id: int, authorization: Optional[str] = Header(None)):
WHERE klub_id=%s OR naziv_kluba=(SELECT naziv FROM pgz_sport.klubovi WHERE id=%s)
ORDER BY godina DESC""", [klub_id, klub_id])
# Aggregate stats
# Aggregate stats — registrirani/trener computed from real signals
# (hns_igrac_id / licenca_broj / kategorija / uloga); kategorija alone is
# almost always NULL (4921/6090 rows) so the old kategorija=='registrirani'
# check returned 0 for nearly every klub including HNK Rijeka. See task N3.
def _is_registriran(c):
if c.get('hns_igrac_id') or c.get('licenca_broj'):
return True
kat = (c.get('kategorija') or '').lower()
return kat in ('igrac', 'sportas', 'sportaš', 'registrirani')
def _is_trener(c):
u = (c.get('uloga') or '').lower()
ud = (c.get('uloga_detalj') or '').lower()
kat = (c.get('kategorija') or '').lower()
return ('trener' in u) or ('trener' in ud) or kat in ('trener', 'vodstvo')
# The base SELECT in the clanovi fetch above does not include uloga/uloga_detalj/
# licenca_broj/hns_igrac_id, so we fetch the few flags we need in one extra query.
flag_rows = fetch("""SELECT id, ime, prezime, oib,
kategorija, uloga, uloga_detalj, licenca_broj, hns_igrac_id
FROM pgz_sport.clanovi WHERE klub_id=%s AND aktivan""", [klub_id])
# N3 (2026-05-10): trainers also live in the dedicated treneri table
# whose klub_id is rarely populated; the canonical link is klub_naziv
# ILIKE. Dedupe by oib (or "ime|prezime" lower-cased) so a trainer who
# is in BOTH clanovi and treneri counts once.
_kn = (rows[0].get('naziv') or '')
_trener_keys = set()
for c in flag_rows:
if _is_trener(c):
_trener_keys.add( (c.get('oib') or
( (c.get('ime') or '') + '|' + (c.get('prezime') or '') )).lower() )
treneri_rows = fetch("""SELECT ime, prezime, oib FROM pgz_sport.treneri
WHERE aktivan IS NOT FALSE
AND (klub_id=%s OR klub_naziv ILIKE %s)""",
[klub_id, '%' + _kn + '%'])
for t in (treneri_rows or []):
_trener_keys.add( (t.get('oib') or
( (t.get('ime') or '') + '|' + (t.get('prezime') or '') )).lower() )
stats = {
'broj_clanova': len(clanovi),
'broj_registriranih': sum(1 for c in clanovi if c.get('kategorija')=='registrirani'),
'broj_trenera': sum(1 for c in clanovi if c.get('kategorija')=='trener'),
'broj_registriranih': sum(1 for c in flag_rows if _is_registriran(c)),
'broj_trenera': len(_trener_keys),
'broj_reprezentativaca': sum(1 for c in clanovi if c.get('reprezentativac')),
'broj_kategoriziranih': sum(1 for c in clanovi if c.get('kategoriziran')),
'broj_stipendiranih': sum(1 for c in clanovi if c.get('stipendiran')),
@@ -869,7 +907,7 @@ def list_clanovi(authorization: Optional[str] = Header(None), q: Optional[str] =
where = ["c.aktivan"]
params = []
if q:
where.append("(c.ime ILIKE %s OR c.prezime ILIKE %s OR c.oib ILIKE %s)")
where.append("(public.f_unaccent(c.ime) ILIKE public.f_unaccent(%s) OR public.f_unaccent(c.prezime) ILIKE public.f_unaccent(%s) OR c.oib ILIKE %s)")
params.extend([f"%{q}%", f"%{q}%", f"%{q}%"])
if klub_id:
where.append("c.klub_id=%s"); params.append(klub_id)
@@ -901,23 +939,49 @@ class ClanIn(BaseModel):
oib: Optional[str] = None
datum_rodenja: Optional[date] = None
spol: Optional[str] = None
adresa: Optional[str] = None
grad: Optional[str] = None
postanski_broj: Optional[str] = None
email: Optional[str] = None
telefon: Optional[str] = None
kategorija: Optional[str] = "registrirani"
podkategorija: Optional[str] = None
pozicija: Optional[str] = None
licenca_broj: Optional[str] = None
licenca_vrijedi_do: Optional[date] = None
reprezentativac: Optional[bool] = False
kategoriziran: Optional[bool] = False
stipendiran: Optional[bool] = False
aktivan: Optional[bool] = True
napomena: Optional[str] = None
@app.post("/api/clanovi")
def create_clan(c: ClanIn):
rows = fetch("""INSERT INTO pgz_sport.clanovi (klub_id, ime, prezime, oib, datum_rodenja, spol, email, telefon, kategorija, pozicija, licenca_broj, licenca_vrijedi_do, reprezentativac, kategoriziran, stipendiran, napomena, aktivan, datum_pristupa)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,TRUE,CURRENT_DATE) RETURNING *""",
[c.klub_id, c.ime, c.prezime, c.oib, c.datum_rodenja, c.spol, c.email, c.telefon, c.kategorija, c.pozicija, c.licenca_broj, c.licenca_vrijedi_do, c.reprezentativac, c.kategoriziran, c.stipendiran, c.napomena])
return rows[0]
rows = fetch("""INSERT INTO pgz_sport.clanovi
(klub_id, ime, prezime, oib, datum_rodenja, spol,
adresa, grad, postanski_broj,
email, telefon, kategorija, podkategorija, pozicija,
licenca_broj, licenca_vrijedi_do,
reprezentativac, kategoriziran, stipendiran,
aktivan, datum_pristupa, napomena)
VALUES (%s,%s,%s,%s,%s,%s, %s,%s,%s, %s,%s,%s,%s,%s, %s,%s,
%s,%s,%s, %s, CURRENT_DATE, %s) RETURNING *""",
[c.klub_id, c.ime, c.prezime, c.oib, c.datum_rodenja, c.spol,
c.adresa, c.grad, c.postanski_broj,
c.email, c.telefon, c.kategorija, c.podkategorija, c.pozicija,
c.licenca_broj, c.licenca_vrijedi_do,
c.reprezentativac, c.kategoriziran, c.stipendiran,
(c.aktivan if c.aktivan is not None else True),
c.napomena])
row = rows[0]
try:
_audit_log("clan.create", "clanovi", row["id"],
f"{row.get('ime','')} {row.get('prezime','')}".strip(),
{"new": {k: row.get(k) for k in
('klub_id','ime','prezime','oib','kategorija','aktivan')}})
except Exception:
pass
return row
@app.get("/api/clanovi/{clan_id}")
def get_clan(clan_id: int):
@@ -1066,7 +1130,7 @@ def list_statistika(godina: Optional[int] = None, q: Optional[str] = None, razin
if godina:
where.append("st.godina=%s"); params.append(godina)
if q:
where.append("s.naziv ILIKE %s"); params.append(f"%{q}%")
where.append("public.f_unaccent(s.naziv) ILIKE public.f_unaccent(%s)"); params.append(f"%{q}%")
if razina:
where.append("s.razina = %s"); params.append(razina)
where_sql = "WHERE " + " AND ".join(where) if where else ""
@@ -1353,7 +1417,7 @@ def natjecanja_list(sport: str = "", razina: str = "", sezona: str = "", q: str
if sport: where.append("sport = %s"); args.append(sport)
if razina: where.append("razina = %s"); args.append(razina)
if sezona: where.append("sezona = %s"); args.append(sezona)
if q: where.append("naziv ILIKE %s"); args.append(f"%{q}%")
if q: where.append("public.f_unaccent(naziv) ILIKE public.f_unaccent(%s)"); args.append(f"%{q}%")
args.append(limit)
with db() as conn:
@@ -1685,6 +1749,15 @@ except Exception as e:
HAS_S3_ROUTERS = False
app.include_router(v2_router)
# Stats router (live system counts for hero stats)
try:
from routers import stats_router
app.include_router(stats_router.router, tags=["stats"])
print("[STATS] router loaded")
except Exception as e:
print(f"[STATS] router fail: {e}")
# Admin Dashboard router (ERP/CRM/Tenants)
try:
from admin_router import router as admin_router
@@ -1826,6 +1899,15 @@ def serve_sport_3d():
@app.get("/admin")
@app.get("/admin/")
@app.get("/sport/sportas/{sid:int}", include_in_schema=False)
def sport_sportas_page(sid: int):
p = "/opt/pgz-sport/static/sportas_profile.html"
if os.path.exists(p):
return FileResponse(p)
return HTMLResponse("<h1>sportas profile page missing</h1>", status_code=404)
@app.get("/sport/admin")
@app.get("/sport/admin/")
def serve_admin():
@@ -1881,6 +1963,8 @@ except Exception as e:
@app.get("/crm")
@app.get("/crm/")
@app.get("/sport/crm")
@app.get("/sport/crm/")
def serve_crm():
p = HTML_DIR / "crm.html"
if p.exists():
@@ -1892,8 +1976,6 @@ def serve_crm():
@app.get("/crm_v2")
@app.get("/crm_v2/")
@app.get("/crm/v2")
@app.get("/crm")
@app.get("/sport/crm")
@app.get("/sport/crm/v2")
@app.get("/sport/crm_v2")
def serve_crm_v2():
@@ -1924,9 +2006,9 @@ def serve_admin_users():
@app.get("/api/sportski-objekti")
def list_sportski_objekti(q=None,tip=None,grad=None):
w=["aktivan=TRUE"]; p=[]
if q: w.append("(naziv ILIKE %s OR adresa ILIKE %s OR grad ILIKE %s)"); p+=["%"+q+"%"]*3
if q: w.append("(public.f_unaccent(naziv) ILIKE public.f_unaccent(%s) OR public.f_unaccent(adresa) ILIKE public.f_unaccent(%s) OR public.f_unaccent(grad) ILIKE public.f_unaccent(%s))"); p+=["%"+q+"%"]*3
if tip: w.append("tip ILIKE %s"); p.append("%"+tip+"%")
if grad: w.append("grad ILIKE %s"); p.append("%"+grad+"%")
if grad: w.append("public.f_unaccent(grad) ILIKE public.f_unaccent(%s)"); p.append("%"+grad+"%")
rows=fetch("SELECT * FROM pgz_sport.sportski_objekti WHERE "+" AND ".join(w)+" ORDER BY grad,naziv",p)
return {"count":len(rows),"rows":rows}
@@ -1935,7 +2017,7 @@ def list_clanovi_full(q=None,hoo=None,reprezentativac=None,klub_id=None,
sport=None,kategorija=None,godina_rodenja=None,status=None,
limit=80,authorization=None):
w=["aktivan=TRUE"]; p=[]
if q: w.append("(ime ILIKE %s OR prezime ILIKE %s OR klub_naziv_godisnjak ILIKE %s)"); p+=["%"+q+"%"]*3
if q: w.append("(public.f_unaccent(ime) ILIKE public.f_unaccent(%s) OR public.f_unaccent(prezime) ILIKE public.f_unaccent(%s) OR klub_naziv_godisnjak ILIKE %s)"); p+=["%"+q+"%"]*3
if hoo: w.append("hoo_kategorija=%s"); p.append(hoo)
if reprezentativac is not None: w.append("reprezentativac="+(("TRUE") if str(reprezentativac).lower()=="true" else "FALSE"))
if klub_id: w.append("klub_id=%s"); p.append(int(klub_id))
@@ -1968,7 +2050,7 @@ def list_gradovi():
@app.get("/api/manifestacije-full")
def list_manifestacije_full(q=None,razina=None):
w=["aktivna=TRUE"]; p=[]
if q: w.append("(naziv ILIKE %s OR mjesto ILIKE %s)"); p+=["%"+q+"%"]*2
if q: w.append("(public.f_unaccent(naziv) ILIKE public.f_unaccent(%s) OR mjesto ILIKE %s)"); p+=["%"+q+"%"]*2
rows=fetch("SELECT id,naziv,mjesto,organizator,razina,broj_ucesnika,godina_od,spol_kategorija,napomena,source_url FROM pgz_sport.manifestacije WHERE "+" AND ".join(w)+" ORDER BY naziv",p)
return {"count":len(rows),"rows":rows}
@@ -2101,7 +2183,7 @@ def network_pgz(q: str=None, entity_type: str=None, max_nodes: int=80):
# Person search
persons = fetch("""SELECT p.id,p.name,p.function,e.name as ent,e.id as eid,e.entity_type,e.city
FROM civic.persons p JOIN civic.entities e ON e.id=p.entity_id
WHERE p.name ILIKE %s OR e.name ILIKE %s LIMIT 60""",[f"%{q}%",f"%{q}%"])
WHERE public.f_unaccent(p.name) ILIKE public.f_unaccent(%s) OR public.f_unaccent(e.name) ILIKE public.f_unaccent(%s) LIMIT 60""",[f"%{q}%",f"%{q}%"])
for r in persons:
pid=f"p_{r['id']}"; eid=f"e_{r['eid']}"
add_node(pid,r.get("name","?")[:30],"person")
@@ -2126,6 +2208,8 @@ def network_pgz(q: str=None, entity_type: str=None, max_nodes: int=80):
@app.get("/platform")
@app.get("/platform/")
@app.get("/sport/platform")
@app.get("/sport/platform/")
def serve_platform():
p = HTML_DIR / "platform.html"
if p.exists(): return FileResponse(p)
@@ -2158,6 +2242,39 @@ def serve_dokumenti():
p = HTML_DIR / "dokumenti.html"
return FileResponse(p) if p.exists() else {"error":"dokumenti.html not found"}
# ─── Avatar fallback (CC26) ─────────────────────────────────────────────
# Routes registered BEFORE the StaticFiles mount so they intercept /static/...
# for the avatar subpaths. When the requested file is missing on disk we
# 302-redirect to /static/avatars/default.png instead of returning 404 — that
# keeps <img> tags rendering and lets the FE onError handler show initials.
import re as _re_av
_AVATAR_NAME_RE = _re_av.compile(r"^[A-Za-z0-9._-]+\.(png|jpe?g|webp|gif)$")
_DEFAULT_AVATAR_URL = "/static/avatars/default.png"
@app.get("/static/uploads/avatars/{name}")
def _serve_clan_avatar(name: str):
if not _AVATAR_NAME_RE.match(name or ""):
from fastapi.responses import RedirectResponse
return RedirectResponse(_DEFAULT_AVATAR_URL, status_code=302)
p = HTML_DIR / "uploads" / "avatars" / name
if p.exists() and p.is_file():
return FileResponse(p)
from fastapi.responses import RedirectResponse
return RedirectResponse(_DEFAULT_AVATAR_URL, status_code=302)
@app.get("/uploads/avatars/{name}")
def _serve_user_avatar(name: str):
if not _AVATAR_NAME_RE.match(name or ""):
from fastapi.responses import RedirectResponse
return RedirectResponse(_DEFAULT_AVATAR_URL, status_code=302)
import pathlib as _pl_av
p = _pl_av.Path("/opt/pgz-sport/uploads/avatars") / name
if p.exists() and p.is_file():
return FileResponse(p)
from fastapi.responses import RedirectResponse
return RedirectResponse(_DEFAULT_AVATAR_URL, status_code=302)
# ─────────────────────────────────────────────────────────────────────────
app.mount("/static", StaticFiles(directory=str(HTML_DIR)), name="static")
# User-uploaded files (avatars, etc.) — served at /uploads/*
@@ -2445,7 +2562,7 @@ def dokumenti_list(vrsta: str = None, sport: str = None, godina: int = None, q:
if izvor:
where.append("organizacija ILIKE %s"); params.append(f"%%{izvor}%%")
if q:
where.append("(title ILIKE %s OR sadrzaj_summary ILIKE %s OR organizacija ILIKE %s)")
where.append("(public.f_unaccent(title) ILIKE public.f_unaccent(%s) OR sadrzaj_summary ILIKE %s OR organizacija ILIKE %s)")
params.extend([f"%%{q}%%"]*3)
rows = fetch(f"""
@@ -2480,7 +2597,9 @@ def dokument_godisnjak_pdf(godina: int):
import os
pdf_path = f"/opt/pgz-sport/_data/godisnjaci/godisnjak_{godina}.pdf"
if os.path.exists(pdf_path):
return FileResponse(pdf_path, media_type="application/pdf", filename=f"godisnjak_{godina}.pdf")
# inline disposition → render in-tab; not download
return FileResponse(pdf_path, media_type="application/pdf",
headers={"Content-Disposition": f'inline; filename="godisnjak_{godina}.pdf"'})
return {"error": "not_found", "godina": godina}
@@ -2555,7 +2674,7 @@ def sportasi_filtered(sport: str = None, klub_id: int = None, kategorija: str =
where.append("(EXTRACT(YEAR FROM c.datum_rodenja) <= %s OR c.godina_rodenja <= %s)")
params.extend([godina_rod_do, godina_rod_do])
if q:
where.append("(c.ime ILIKE %s OR c.prezime ILIKE %s)")
where.append("(public.f_unaccent(c.ime) ILIKE public.f_unaccent(%s) OR public.f_unaccent(c.prezime) ILIKE public.f_unaccent(%s))")
params.extend([f"%{q}%", f"%{q}%"])
if samo_priority:
# Igrač iz kluba koji prima novac ili je u godišnjaku ili ima HNS roster
@@ -2848,7 +2967,7 @@ def sportski_objekti_v2_list(tip: str = None, grad: str = None, sport: str = Non
if sport:
where.append("%s = ANY(sportovi)"); params.append(sport)
if q:
where.append("(naziv ILIKE %s OR adresa ILIKE %s OR upravitelj ILIKE %s)")
where.append("(public.f_unaccent(naziv) ILIKE public.f_unaccent(%s) OR public.f_unaccent(adresa) ILIKE public.f_unaccent(%s) OR upravitelj ILIKE %s)")
params.extend([f"%{q}%"]*3)
rows = fetch(f"""
@@ -2904,6 +3023,150 @@ def portal_v2():
return FileResponse(p)
return {"error": "sport2.html not found"}
@app.get("/api/v2/export")
def export_generic(
format: str = "csv",
endpoint: str = Query(..., description="Interni API path"),
filename: str = "export",
authorization: str = Header(None),
):
import csv, io
from fastapi.responses import StreamingResponse
if not endpoint:
return {"error": "Missing endpoint parameter"}
# Prepoznaj šta treba vratiti po endpointu
if "savezi" in endpoint:
rows = fetch("SELECT * FROM pgz_sport.savezi ORDER BY naziv")
elif "klubovi" in endpoint:
rows = fetch("SELECT * FROM pgz_sport.klubovi ORDER BY naziv")
elif "sportasi" in endpoint:
rows = fetch("SELECT id, ime, prezime, sport, klub_naziv_godisnjak FROM pgz_sport.clanovi LIMIT 1000")
else:
return {"error": "Nepoznat endpoint"}
if not rows:
return {"error": "Nema podataka"}
output = io.StringIO()
writer = csv.DictWriter(output, fieldnames=rows[0].keys())
writer.writeheader()
writer.writerows(rows)
output.seek(0)
return StreamingResponse(
iter([output.getvalue()]),
media_type="text/csv",
headers={"Content-Disposition": f"attachment; filename={filename}.csv"}
)
# ════ SPORT-S3: audit helper + zzjz kalendar stub ════
def _audit_log(action: str, target_type: str, target_id: int,
target_text: str = "", payload: dict = None):
"""Append a row into pgz_sport.sys_audit. Never raises — best-effort logging."""
try:
import json as _j
fetch("""INSERT INTO pgz_sport.sys_audit
(action, target_type, target_id, target_text, payload)
VALUES (%s, %s, %s, %s, %s::jsonb)""",
[action, target_type, int(target_id) if target_id else None,
(target_text or "")[:500],
_j.dumps(payload or {}, default=str, ensure_ascii=False)])
except Exception:
pass
@app.get("/api/zzjz/kalendar")
def zzjz_kalendar(dana: int = 14):
"""Stub: 14-day calendar grid of slots for sportski liječnički pregledi.
Real ZZJZ PGŽ API is not yet available (contract pending — see /api/zzjz/dogovor).
Until then this returns a synthesized weekday-only grid; the frontend can show
it as a placeholder and let the user request a slot internally."""
from datetime import date as _d, timedelta as _td
today = _d.today()
out = []
for i in range(max(1, min(60, dana))):
d = today + _td(days=i)
is_weekend = d.weekday() >= 5
slots = [] if is_weekend else [
{"time": "08:00", "available": True, "klinika": "ZZJZ PGŽ — Rijeka"},
{"time": "09:00", "available": True, "klinika": "ZZJZ PGŽ — Rijeka"},
{"time": "10:00", "available": False, "klinika": "ZZJZ PGŽ — Rijeka"},
{"time": "11:00", "available": True, "klinika": "ZZJZ PGŽ — Rijeka"},
{"time": "13:00", "available": True, "klinika": "ZZJZ PGŽ — Krk"},
{"time": "14:00", "available": True, "klinika": "ZZJZ PGŽ — Krk"},
]
out.append({"date": str(d), "weekday": d.strftime("%A"),
"is_weekend": is_weekend, "slots": slots})
return {"note": "Stub data — real ZZJZ PGŽ booking API not yet contracted.",
"see_also": "/api/zzjz/dogovor",
"days": out}
@app.post("/api/zzjz/zatrazi-termin")
def zzjz_zatrazi_termin(payload: dict):
"""Stub: log an internal request for a ZZJZ medical appointment.
Audits the request and returns confirmation. Once real ZZJZ API exists,
this endpoint should forward the booking and return their reference."""
try:
_audit_log("zzjz.zatrazi-termin", "lijecnicki",
int(payload.get("clan_id") or 0),
f"{payload.get('date','')} {payload.get('time','')} @ {payload.get('klinika','')}",
payload)
except Exception:
pass
return {"ok": True, "status": "queued",
"note": "Zahtjev evidentiran u sys_audit. Stvarna rezervacija slijedi nakon potpisa ugovora sa ZZJZ PGŽ."}
# ════ end SPORT-S3 ════
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8095)
# === AI STUB (privremeni) ===
try:
from routers.ai_stub_router import router as ai_stub_router
app.include_router(ai_stub_router, prefix='/api/v2')
print('[AI-STUB] router loaded (/api/v2/ai/ask)')
except Exception as e:
print(f'[AI-STUB] router fail: {e}')
# ═══ Generički CSV/XLSX/PDF export (za frontend) ═══
@app.get("/api/v2/export")
def export_generic(
format: str = "csv",
endpoint: str = Query(..., description="Interni API path, npr. /sport/api/v2/savezi/priority-sort"),
filename: str = "export",
authorization: str = Header(None),
):
import requests as req, csv, io
if not endpoint:
return {"error": "Missing endpoint parameter"}
internal = f"http://127.0.0.1:8095{endpoint}"
try:
resp = req.get(internal, timeout=60, headers={"Authorization": authorization} if authorization else {})
data = resp.json()
except Exception as e:
return {"error": str(e)}
output = io.StringIO()
rows = []
if isinstance(data, list) and data:
rows = data
elif isinstance(data, dict):
for key in ("rows", "data", "items", "results", "savezi"):
if key in data and isinstance(data[key], list):
rows = data[key]
break
if rows:
writer = csv.DictWriter(output, fieldnames=rows[0].keys())
writer.writeheader()
writer.writerows(rows)
else:
output.write("No data")
output.seek(0)
from fastapi.responses import StreamingResponse
return StreamingResponse(iter([output.getvalue()]), media_type="text/csv",
headers={"Content-Disposition": f"attachment; filename={filename}.csv"})