"""POST request JSON — HTTP client sin dependencias externas.""" import json import urllib.error import urllib.request def http_post_json( url: str, body: dict, headers: dict[str, str] | None = None, timeout: float = 30.0, ) -> dict: """Realiza un POST request con body JSON y parsea la respuesta como JSON. Agrega automaticamente ``Content-Type: application/json`` y ``Accept: application/json``. Si el status es >= 400 lanza RuntimeError con status code, url y los primeros 200 caracteres del body. Args: url: URL del endpoint. body: Datos a serializar como JSON en el cuerpo del request. headers: Headers HTTP adicionales. Se fusionan con los defaults. timeout: Segundos maximo de espera (default 30). Returns: Respuesta parseada como dict o list. Raises: RuntimeError: Si status >= 400 o si el body de respuesta no es JSON valido. """ all_headers: dict[str, str] = { "Content-Type": "application/json", "Accept": "application/json", } if headers: all_headers.update(headers) data = json.dumps(body, ensure_ascii=False).encode("utf-8") req = urllib.request.Request(url, data=data, headers=all_headers, method="POST") 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_post_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_post_json: response is not valid JSON — {preview}" ) from e