Download src/data/http_cache.py from Dhruv1000/tracegrpo: direct link, hf CLI and curl.
- Browser
- Download file 4 kB
-
https://huggingface.co/Dhruv1000/tracegrpo/resolve/main/src/data/http_cache.py
- Command line
-
hf download hf://Dhruv1000/tracegrpo/src/data/http_cache.py
-
curl -L -o http_cache.py https://huggingface.co/Dhruv1000/tracegrpo/resolve/main/src/data/http_cache.py
4 kB
| """Disk-cached, backoff-aware HTTP GET, shared by the OOD miner and (if | |
| needed later) other long-running API-bound collection scripts. | |
| Every response is cached to disk keyed by a caller-supplied cache key, so a | |
| killed-and-restarted job never re-issues a request it already got an answer | |
| for — the cache alone makes these scripts resume-safe, no separate progress | |
| file needed. Respects both NVD-style plain 403/429 backoff and GitHub-style | |
| `X-RateLimit-Remaining`/`X-RateLimit-Reset` headers. | |
| """ | |
| from __future__ import annotations | |
| import hashlib | |
| import json | |
| import time | |
| from pathlib import Path | |
| import requests | |
| def _cache_path(cache_dir: Path, cache_key: str) -> Path: | |
| h = hashlib.sha256(cache_key.encode("utf-8")).hexdigest()[:32] | |
| return cache_dir / f"{h}.json" | |
| def cached_get( | |
| session: requests.Session, | |
| url: str, | |
| cache_dir: Path, | |
| cache_key: str, | |
| params: dict | None = None, | |
| max_retries: int = 6, | |
| base_timeout: int = 30, | |
| ) -> dict | None: | |
| """Returns the parsed JSON body, or None on a non-retryable failure | |
| (e.g. 404 — a real "this doesn't exist", not a rate limit). Caches only | |
| successful (200) responses; failures are never cached, so a transient | |
| error gets retried on the next run rather than permanently poisoning | |
| the cache. | |
| Callers that apply their own rate-limit delay between calls (as | |
| scripts/build_ood_set.py does) should check `is_cached` first and skip | |
| the delay on a hit — otherwise resuming past thousands of already-cached | |
| requests pays the full per-call delay for zero actual network traffic. | |
| """ | |
| data, _ = cached_get_ex(session, url, cache_dir, cache_key, params, max_retries, base_timeout) | |
| return data | |
| def is_cached(cache_dir: Path, cache_key: str) -> bool: | |
| return _cache_path(cache_dir, cache_key).exists() | |
| def cached_get_ex( | |
| session: requests.Session, | |
| url: str, | |
| cache_dir: Path, | |
| cache_key: str, | |
| params: dict | None = None, | |
| max_retries: int = 6, | |
| base_timeout: int = 30, | |
| ) -> tuple[dict | None, bool]: | |
| """Same as `cached_get`, but also returns whether the result came from | |
| the on-disk cache (True) or a live network call (False).""" | |
| cache_dir.mkdir(parents=True, exist_ok=True) | |
| path = _cache_path(cache_dir, cache_key) | |
| if path.exists(): | |
| try: | |
| return json.loads(path.read_text()), True | |
| except (json.JSONDecodeError, OSError): | |
| pass # corrupt cache entry — fall through and refetch | |
| for attempt in range(max_retries): | |
| try: | |
| resp = session.get(url, params=params, timeout=base_timeout) | |
| except requests.RequestException: | |
| time.sleep(min(2**attempt, 60)) | |
| continue | |
| if resp.status_code == 200: | |
| try: | |
| data = resp.json() | |
| except ValueError: | |
| return None, False | |
| path.write_text(json.dumps(data)) | |
| return data, False | |
| if resp.status_code == 404: | |
| return None, False # real "not found" — don't retry, don't cache | |
| if resp.status_code in (403, 429): | |
| wait_s = _backoff_seconds(resp, attempt) | |
| time.sleep(wait_s) | |
| continue | |
| if 500 <= resp.status_code < 600: | |
| time.sleep(min(2**attempt, 60)) | |
| continue | |
| return None, False # other 4xx: treat as non-retryable | |
| return None, False | |
| def _backoff_seconds(resp: requests.Response, attempt: int) -> float: | |
| retry_after = resp.headers.get("Retry-After") | |
| if retry_after: | |
| try: | |
| return float(retry_after) + 1 | |
| except ValueError: | |
| pass | |
| reset = resp.headers.get("X-RateLimit-Reset") | |
| remaining = resp.headers.get("X-RateLimit-Remaining") | |
| if reset and remaining == "0": | |
| wait = float(reset) - time.time() | |
| if wait > 0: | |
| return min(wait + 1, 900) # cap at 15 min so one bad header can't wedge the job | |
| return min(2**attempt * 2, 120) | |