#!/usr/bin/env python3 """MedGenesis – **Open Targets** GraphQL helper (async, cached). Updated version adds: * Retry with exponential back‑off (2→4→8 s) * 12‑h LRU cache (256 genes) * Graceful empty‑list fallback & doc‑string examples. """ from __future__ import annotations import asyncio, textwrap, httpx from functools import lru_cache from typing import List, Dict _OT_URL = "https://api.platform.opentargets.org/api/v4/graphql" _QUERY = textwrap.dedent( """ query Assoc($gene: String!, $size: Int!) { associations(geneSymbol: $gene, size: $size) { rows { score datatypeId datasourceId disease { id name } target { id symbol } } } } """ ) async def _post(payload: Dict, retries: int = 3) -> Dict: """POST with back‑off retry (2×, 4×); returns JSON.""" delay = 2 last_resp = None for _ in range(retries): async with httpx.AsyncClient(timeout=15) as cli: last_resp = await cli.post(_OT_URL, json=payload) if last_resp.status_code == 200: return last_resp.json() await asyncio.sleep(delay) delay *= 2 # Raise from final attempt if still failing last_resp.raise_for_status() # type: ignore @lru_cache(maxsize=256) async def fetch_ot_associations(gene_symbol: str, *, size: int = 30) -> List[Dict]: """Return association rows for *gene_symbol* (or []). Example:: rows = await fetch_ot_associations("TP53", size=20) """ payload = {"query": _QUERY, "variables": {"gene": gene_symbol, "size": size}} data = await _post(payload) return data.get("data", {}).get("associations", {}).get("rows", [])