""" Utgående, lesende HTTP-klient mot GolfAPI.io (ADR-064) -- tredje banekilde for baner utenfor TeeOffs dekning (f.eks. Tjøme Golfklubb, bekreftet IKKE i TeeOff 2026-08-12). Ren HTTP-modul -- ingen db/RLS-avhengighet, samme isolasjonsprinsipp som teeoff_client.py/handicap_engine.py. ULIKT teeoff: GolfAPI-kall koster ekte, begrenset budsjett (kjøpt 20 kall, ~2/bane -- 1 for /courses/{id}, 1 for /coordinates/{id}, søk koster 0.1). Denne modulen gjør RÅ HTTP-kall og returnerer JSON uendret -- selve cache-sjekken ("har vi denne banen fra før?") ligger i _get_or_fetch_golfapi_course() i courses.py, IKKE her, slik at denne modulen forblir en ren, testbar transportlag uten db-avhengighet. Alltid measureUnit=m i kallene -- unngår yard/meter-fellen research advarte om (internasjonale kilder returnerer ofte yards som standard). """ import httpx from .config import settings _BASE_URL = "https://golfapi.io/api/v2.3" _TIMEOUT = httpx.Timeout(10.0, connect=5.0) class GolfApiNotConfiguredError(Exception): """TEECUP_GOLFAPI_TOKEN er ikke satt i .env.""" class GolfApiUnavailableError(Exception): """GolfAPI svarte ikke (nede, timeout, uventet statuskode).""" class GolfApiNotFoundError(Exception): """Banen/klubben finnes ikke hos GolfAPI (404).""" def _headers() -> dict[str, str]: if not settings.GOLFAPI_TOKEN: raise GolfApiNotConfiguredError() return {"Authorization": f"Bearer {settings.GOLFAPI_TOKEN}"} async def search_clubs(name: str | None = None, city: str | None = None, country: str | None = None) -> list[dict]: """Koster 0.1 API-kall. Returnerer rå `clubs`-array (hver med `courses[]`).""" params = {k: v for k, v in {"name": name, "city": city, "country": country}.items() if v} try: async with httpx.AsyncClient(timeout=_TIMEOUT) as client: res = await client.get(f"{_BASE_URL}/clubs", params=params, headers=_headers()) except httpx.HTTPError as e: raise GolfApiUnavailableError(str(e)) from e if res.status_code != 200: raise GolfApiUnavailableError(f"uventet status {res.status_code}") return res.json().get("clubs", []) async def get_course(course_id: str) -> dict: """Koster 1 API-kall. Full scorekortdata: par/SI per hull, alle tees med per-hull-lengde (meter) og rating/slope.""" try: async with httpx.AsyncClient(timeout=_TIMEOUT) as client: res = await client.get(f"{_BASE_URL}/courses/{course_id}", params={"measureUnit": "m"}, headers=_headers()) except httpx.HTTPError as e: raise GolfApiUnavailableError(str(e)) from e if res.status_code == 404: raise GolfApiNotFoundError(course_id) if res.status_code != 200: raise GolfApiUnavailableError(f"uventet status {res.status_code}") return res.json() async def get_coordinates(course_id: str) -> list[dict]: """Koster 1 API-kall. Rå `coordinates`-array (poi/location/sideFW/hole/ latitude/longitude, rå GolfAPI-tallkoder -- oversettes til TeeCups tekst-enum i courses.py, ikke her).""" try: async with httpx.AsyncClient(timeout=_TIMEOUT) as client: res = await client.get(f"{_BASE_URL}/coordinates/{course_id}", headers=_headers()) except httpx.HTTPError as e: raise GolfApiUnavailableError(str(e)) from e if res.status_code == 404: raise GolfApiNotFoundError(course_id) if res.status_code != 200: raise GolfApiUnavailableError(f"uventet status {res.status_code}") return res.json().get("coordinates", [])