"""GET request JSON — HTTP client sin dependencias externas.""" import json import urllib.error import urllib.parse import urllib.request def http_get_json( url: str, headers: dict[str, str] | None = None, params: dict[str, str] | None = None, timeout: float = 30.0, ) -> dict: """Realiza un GET request y parsea la respuesta como JSON. Agrega automaticamente el header ``Accept: application/json``. Si el status es >= 400 lanza RuntimeError con status code, url y los primeros 200 caracteres del body para facilitar el debugging. Args: url: URL del endpoint. headers: Headers HTTP adicionales. Se fusionan con Accept por defecto. params: Query string params. Se serializa con urllib.parse.urlencode. timeout: Segundos maximo de espera (default 30). Returns: Respuesta parseada como dict o list. Raises: RuntimeError: Si status >= 400 o si el body no es JSON valido. """ if params: url = f"{url}?{urllib.parse.urlencode(params)}" all_headers: dict[str, str] = {"Accept": "application/json"} if headers: all_headers.update(headers) req = urllib.request.Request(url, headers=all_headers, method="GET") try: with urllib.request.urlopen(req, timeout=timeout) as resp: raw = resp.read() except urllib.error.HTTPError as e: body_preview = e.read(200).decode("utf-8", errors="replace") short_url = url[:100] if len(url) > 100 else url raise RuntimeError( f"http_get_json: HTTP {e.code} at {short_url!r} — {body_preview}" ) from e try: return json.loads(raw) except json.JSONDecodeError as e: preview = raw[:200].decode("utf-8", errors="replace") raise RuntimeError( f"http_get_json: response is not valid JSON — {preview}" ) from e