""" Thin synchronous HTTP helper built on :mod:`urllib.request`. Translates HTTP errors into typed :mod:`markitect.llm.exceptions`. """ import json import urllib.request import urllib.error from typing import Dict, Any, Optional from markitect.llm.exceptions import ( LLMAPIError, LLMRateLimitError, LLMTimeoutError, ) def post_json( url: str, payload: Dict[str, Any], headers: Optional[Dict[str, str]] = None, timeout: int = 300, ) -> Dict[str, Any]: """POST *payload* as JSON and return the parsed response body. Raises: LLMRateLimitError: on HTTP 429 LLMAPIError: on other non-2xx responses LLMTimeoutError: on socket / read timeout """ data = json.dumps(payload).encode() req = urllib.request.Request( url, data=data, headers={"Content-Type": "application/json", **(headers or {})}, method="POST", ) try: with urllib.request.urlopen(req, timeout=timeout) as resp: body = resp.read().decode() return json.loads(body) except urllib.error.HTTPError as exc: body = "" try: body = exc.read().decode() except Exception: pass if exc.code == 429: raise LLMRateLimitError( f"Rate limited (429) from {url}", status_code=429, response_body=body, cause=exc, ) from exc raise LLMAPIError( f"HTTP {exc.code} from {url}", status_code=exc.code, response_body=body, cause=exc, ) from exc except urllib.error.URLError as exc: if "timed out" in str(exc.reason): raise LLMTimeoutError( f"Request to {url} timed out after {timeout}s", cause=exc, ) from exc raise LLMAPIError( f"URL error for {url}: {exc.reason}", cause=exc, ) from exc except TimeoutError as exc: raise LLMTimeoutError( f"Request to {url} timed out after {timeout}s", cause=exc, ) from exc