Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """HTTP smoke test katalogu (HF test env lub lokalny). Bez maskowania closed=0.""" | |
| from __future__ import annotations | |
| import json | |
| import os | |
| import sys | |
| import urllib.error | |
| import urllib.parse | |
| import urllib.request | |
| from pathlib import Path | |
| BASE = os.environ.get("CATALOG_API_BASE", "https://bogdan555-grantforge-api.hf.space") | |
| TIMEOUT = int(os.environ.get("CATALOG_HTTP_TIMEOUT", "45")) | |
| SCRATCH = Path( | |
| os.environ.get( | |
| "CATALOG_SCRATCH", | |
| "/tmp/grok-goal-226f39df5eac/implementer", | |
| ) | |
| ) | |
| SCRATCH.mkdir(parents=True, exist_ok=True) | |
| LOG_PATH = SCRATCH / "http_verify.log" | |
| class _Tee: | |
| def __init__(self, *streams): | |
| self._streams = streams | |
| def write(self, data: str) -> None: | |
| for s in self._streams: | |
| s.write(data) | |
| s.flush() | |
| def flush(self) -> None: | |
| for s in self._streams: | |
| s.flush() | |
| def _post(path: str, payload: dict) -> tuple[int, dict | str]: | |
| url = f"{BASE.rstrip('/')}{path}" | |
| data = json.dumps(payload).encode("utf-8") | |
| req = urllib.request.Request( | |
| url, data=data, headers={"Accept": "application/json", "Content-Type": "application/json"} | |
| ) | |
| try: | |
| with urllib.request.urlopen(req, timeout=TIMEOUT) as resp: | |
| body = resp.read().decode("utf-8", errors="replace") | |
| try: | |
| return resp.status, json.loads(body) | |
| except json.JSONDecodeError: | |
| return resp.status, body[:2000] | |
| except urllib.error.HTTPError as e: | |
| body = e.read().decode("utf-8", errors="replace")[:2000] | |
| return e.code, body | |
| def _get(path: str) -> tuple[int, dict | str]: | |
| url = f"{BASE.rstrip('/')}{path}" | |
| req = urllib.request.Request(url, headers={"Accept": "application/json"}) | |
| try: | |
| with urllib.request.urlopen(req, timeout=TIMEOUT) as resp: | |
| body = resp.read().decode("utf-8", errors="replace") | |
| try: | |
| return resp.status, json.loads(body) | |
| except json.JSONDecodeError: | |
| return resp.status, body[:2000] | |
| except urllib.error.HTTPError as e: | |
| body = e.read().decode("utf-8", errors="replace")[:2000] | |
| return e.code, body | |
| def main() -> int: | |
| log_file = LOG_PATH.open("w", encoding="utf-8") | |
| orig_stdout = sys.stdout | |
| sys.stdout = _Tee(orig_stdout, log_file) | |
| try: | |
| return _run_checks() | |
| finally: | |
| sys.stdout = orig_stdout | |
| log_file.close() | |
| def _run_checks() -> int: | |
| print(f"HTTP_VERIFY base={BASE}") | |
| checks: list[str] = [] | |
| code, stats = _get("/api/grants/catalog/stats") | |
| print(f"GET /api/grants/catalog/stats -> HTTP {code}") | |
| print(json.dumps(stats, ensure_ascii=False, indent=2) if isinstance(stats, dict) else stats) | |
| closed_in_db = 0 | |
| if code != 200: | |
| checks.append(f"stats HTTP {code}") | |
| elif isinstance(stats, dict): | |
| if not stats.get("has_data"): | |
| checks.append("has_data false") | |
| if stats.get("visible_in_catalog", 0) < 100: | |
| checks.append(f"visible_in_catalog={stats.get('visible_in_catalog')}") | |
| closed_in_db = int(stats.get("closed") or 0) | |
| search_body = { | |
| "query": "PARP", | |
| "filters": {"status": "active", "region": "mazowieckie"}, | |
| "limit": 5, | |
| } | |
| code2, search = _post("/api/grants/catalog/search", search_body) | |
| print(f"\nPOST /api/grants/catalog/search -> HTTP {code2}") | |
| print(json.dumps(search_body, ensure_ascii=False)) | |
| if isinstance(search, dict): | |
| grants = search.get("grants") or [] | |
| print( | |
| json.dumps( | |
| { | |
| "count": search.get("count"), | |
| "sample": [ | |
| { | |
| "name": (g.get("name") or "")[:50], | |
| "status": g.get("status"), | |
| "regions": g.get("eligible_regions"), | |
| } | |
| for g in grants[:3] | |
| ], | |
| }, | |
| ensure_ascii=False, | |
| indent=2, | |
| ) | |
| ) | |
| if code2 != 200: | |
| checks.append(f"catalog/search HTTP {code2}") | |
| elif search.get("count", 0) < 1: | |
| checks.append("catalog/search count=0") | |
| else: | |
| for g in grants: | |
| if g.get("status") != "active": | |
| checks.append(f"search returned non-active: {g.get('name')}") | |
| break | |
| else: | |
| print(search) | |
| checks.append("catalog/search non-json") | |
| closed_body = { | |
| "query": "", | |
| "filters": {"status": "closed", "region": "mazowieckie"}, | |
| "limit": 5, | |
| } | |
| code3, closed = _post("/api/grants/catalog/search", closed_body) | |
| print(f"\nPOST /api/grants/catalog/search (closed) -> HTTP {code3}") | |
| if isinstance(closed, dict): | |
| grants_c = closed.get("grants") or [] | |
| closed_count = closed.get("count", 0) | |
| print( | |
| json.dumps( | |
| { | |
| "count": closed_count, | |
| "sample_status": [g.get("status") for g in grants_c[:3]], | |
| "sample_names": [(g.get("name") or "")[:50] for g in grants_c[:3]], | |
| }, | |
| ensure_ascii=False, | |
| indent=2, | |
| ) | |
| ) | |
| if code3 != 200: | |
| checks.append(f"closed search HTTP {code3}") | |
| elif closed_count < 1: | |
| if closed_in_db > 0: | |
| checks.append( | |
| f"closed+mazowieckie count=0 but stats.closed={closed_in_db} " | |
| "(wymaga FORCE_WYSZUKIWARKA_REIMPORT=true + redeploy)" | |
| ) | |
| else: | |
| checks.append("closed+mazowieckie count=0 and stats.closed=0") | |
| else: | |
| bad = [g for g in grants_c if g.get("status") != "closed"] | |
| if bad: | |
| checks.append(f"closed search has non-closed: {len(bad)}") | |
| else: | |
| checks.append("closed search failed") | |
| nabory_qs = urllib.parse.urlencode( | |
| {"status": "active", "region": "mazowieckie", "q": "PARP", "limit": "5"} | |
| ) | |
| nabory_path = f"/api/grants/nabory?{nabory_qs}" | |
| code4, nabory = _get(nabory_path) | |
| print(f"\nGET {nabory_path} -> HTTP {code4}") | |
| if isinstance(nabory, dict): | |
| nabory_list = nabory.get("nabory") or [] | |
| has_catalog_stats = "catalog_stats" in nabory | |
| print( | |
| json.dumps( | |
| { | |
| "count": nabory.get("count"), | |
| "has_catalog_stats": has_catalog_stats, | |
| "catalog_visible": (nabory.get("catalog_stats") or {}).get( | |
| "visible_in_catalog" | |
| ), | |
| "sample": [ | |
| { | |
| "name": (n.get("name") or "")[:50], | |
| "status": n.get("status"), | |
| "program_year": n.get("program_year"), | |
| } | |
| for n in nabory_list[:5] | |
| ], | |
| }, | |
| ensure_ascii=False, | |
| indent=2, | |
| ) | |
| ) | |
| if code4 != 200: | |
| checks.append(f"nabory HTTP {code4}") | |
| elif not has_catalog_stats: | |
| checks.append("nabory missing catalog_stats (stary endpoint — wymaga redeploy)") | |
| elif nabory.get("count", 0) < 1: | |
| checks.append("nabory count=0 for active+PARP+mazowieckie") | |
| else: | |
| stale = [ | |
| n | |
| for n in nabory_list | |
| if (n.get("program_year") or 2024) < 2023 and n.get("status") == "active" | |
| ] | |
| if stale: | |
| checks.append(f"nabory returned stale active: {len(stale)}") | |
| else: | |
| print(nabory) | |
| checks.append("nabory non-json") | |
| nabory_closed_qs = urllib.parse.urlencode( | |
| {"status": "closed", "region": "mazowieckie", "limit": "5"} | |
| ) | |
| nabory_closed_path = f"/api/grants/nabory?{nabory_closed_qs}" | |
| code5, nabory_closed = _get(nabory_closed_path) | |
| print(f"\nGET {nabory_closed_path} -> HTTP {code5}") | |
| if isinstance(nabory_closed, dict): | |
| print( | |
| json.dumps( | |
| { | |
| "count": nabory_closed.get("count"), | |
| "has_catalog_stats": "catalog_stats" in nabory_closed, | |
| "sample_status": [ | |
| n.get("status") for n in (nabory_closed.get("nabory") or [])[:3] | |
| ], | |
| }, | |
| ensure_ascii=False, | |
| indent=2, | |
| ) | |
| ) | |
| if code5 == 200 and "catalog_stats" in nabory_closed: | |
| if nabory_closed.get("count", 0) < 1 and closed_in_db > 0: | |
| checks.append("nabory closed+region count=0 despite stats.closed>0") | |
| else: | |
| print(nabory_closed) | |
| if checks: | |
| print("\nHTTP_VERIFY FAIL:", checks) | |
| return 1 | |
| print("\nHTTP_VERIFY: OK") | |
| print(f"Log saved: {LOG_PATH}") | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |