"""Step 3: rank the candidate domains by authority using OpenPageRank. API: GET https://openpagerank.com/api/v1.0/getPageRank Auth: header API-OPR: Params: repeated domains[]= (up to 100 per call) Response: response[] with page_rank_decimal / page_rank_integer / rank per domain. Docs: https://www.domcop.com/openpagerank/documentation """ from __future__ import annotations from typing import Dict, List import requests from . import config def _chunks(items: List[str], size: int = 100): for i in range(0, len(items), size): yield items[i : i + size] def fetch_pageranks(domains: List[str]) -> Dict[str, float]: """Return {domain: page_rank_decimal}. Missing/unknown domains map to 0.0. Raises RuntimeError if the API key is missing so the caller can surface it. """ if not config.OPR_API_KEY: raise RuntimeError( "OPR_API_KEY is not set. The Space owner must add an OpenPageRank API key " "(free at domcop.com) as a Space secret." ) scores: Dict[str, float] = {d: 0.0 for d in domains} headers = {"API-OPR": config.OPR_API_KEY} for chunk in _chunks(domains, 100): params = [("domains[]", d) for d in chunk] try: r = requests.get( config.OPR_ENDPOINT, params=params, headers=headers, timeout=config.HTTP_TIMEOUT, ) r.raise_for_status() payload = r.json() except Exception as e: # noqa: BLE001 raise RuntimeError(f"OpenPageRank request failed: {e}") from e for item in payload.get("response", []) or []: dom = (item.get("domain") or "").lower() if dom not in scores: continue try: scores[dom] = float(item.get("page_rank_decimal") or 0.0) except (TypeError, ValueError): scores[dom] = 0.0 return scores def rank_results(results: List[dict], top_k: int = config.TOP_K) -> List[dict]: """Attach OpenPageRank scores to SearXNG results and return the top-K by authority. Falls back to the incoming (SearXNG) order if the API is unavailable. """ domains = [r["domain"] for r in results] try: scores = fetch_pageranks(domains) for r in results: r["page_rank"] = scores.get(r["domain"], 0.0) ranked = sorted(results, key=lambda x: x.get("page_rank", 0.0), reverse=True) except RuntimeError: # Preserve SearXNG order; mark scores as unknown. for r in results: r.setdefault("page_rank", None) ranked = results return ranked[:top_k]