Spaces:
Sleeping
Sleeping
| """Semantic Scholar API client for citation count enrichment.""" | |
| from __future__ import annotations | |
| import time | |
| import httpx | |
| from config import SEMANTIC_SCHOLAR_BASE | |
| from logging_config import get_logger | |
| _logger = get_logger("ingestion.semantic_scholar") | |
| _BATCH_SIZE = 500 # Semantic Scholar batch endpoint limit | |
| def fetch_citation_counts(pmids: list[str]) -> dict[str, int]: | |
| """ | |
| Fetch citation counts for a list of PMIDs via Semantic Scholar batch API. | |
| Returns {pmid: citation_count}. PMIDs with no S2 record are omitted. | |
| """ | |
| if not pmids: | |
| return {} | |
| result: dict[str, int] = {} | |
| for i in range(0, len(pmids), _BATCH_SIZE): | |
| batch = pmids[i : i + _BATCH_SIZE] | |
| ids = [f"PMID:{pmid}" for pmid in batch] | |
| for attempt in range(3): | |
| try: | |
| resp = httpx.post( | |
| f"{SEMANTIC_SCHOLAR_BASE}/paper/batch", | |
| params={"fields": "citationCount,externalIds"}, | |
| json={"ids": ids}, | |
| timeout=30, | |
| ) | |
| resp.raise_for_status() | |
| papers = resp.json() | |
| break | |
| except httpx.HTTPError as exc: | |
| if attempt == 2: | |
| _logger.warning(f"Semantic Scholar batch failed: {exc}") | |
| papers = [] | |
| break | |
| time.sleep(2 ** attempt) | |
| for paper in papers: | |
| if paper is None: | |
| continue | |
| ext = paper.get("externalIds") or {} | |
| pmid = ext.get("PubMed") | |
| count = paper.get("citationCount") | |
| if pmid and count is not None: | |
| result[str(pmid)] = int(count) | |
| # Free tier allows ~100 requests per 5 minutes; 1s delay keeps us safe | |
| time.sleep(1.0) | |
| _logger.info( | |
| "Semantic Scholar citation fetch", | |
| extra={"data": {"requested": len(pmids), "found": len(result)}}, | |
| ) | |
| return result | |