61 lines
1.9 KiB
Python
61 lines
1.9 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
from urllib.error import HTTPError, URLError
|
|
from urllib.request import Request, urlopen
|
|
|
|
|
|
@dataclass
|
|
class HttpClientError(RuntimeError):
|
|
status: int
|
|
message: str
|
|
payload: dict[str, Any] | None = None
|
|
|
|
def __str__(self) -> str:
|
|
return f"HTTP {self.status}: {self.message}"
|
|
|
|
|
|
def _network_error_message(exc: BaseException, timeout: float) -> str:
|
|
reason = getattr(exc, "reason", exc)
|
|
if isinstance(exc, TimeoutError) or isinstance(reason, TimeoutError):
|
|
return f"timed out after {timeout:g}s"
|
|
return str(reason) or type(reason).__name__
|
|
|
|
|
|
def request_json(
|
|
method: str,
|
|
url: str,
|
|
payload: dict[str, Any] | None = None,
|
|
token: str | None = None,
|
|
timeout: float = 5.0,
|
|
) -> dict[str, Any]:
|
|
body = None
|
|
headers = {"Accept": "application/json"}
|
|
if payload is not None:
|
|
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
headers["Content-Type"] = "application/json; charset=utf-8"
|
|
if token:
|
|
headers["Authorization"] = f"Bearer {token}"
|
|
|
|
request = Request(url, data=body, headers=headers, method=method.upper())
|
|
try:
|
|
with urlopen(request, timeout=timeout) as response:
|
|
response_body = response.read()
|
|
if not response_body:
|
|
return {}
|
|
return json.loads(response_body.decode("utf-8"))
|
|
except HTTPError as exc:
|
|
raw = exc.read().decode("utf-8", errors="replace")
|
|
payload_obj = None
|
|
message = raw or exc.reason
|
|
try:
|
|
payload_obj = json.loads(raw)
|
|
message = payload_obj.get("error") or message
|
|
except json.JSONDecodeError:
|
|
pass
|
|
raise HttpClientError(exc.code, message, payload_obj) from exc
|
|
except (URLError, TimeoutError, OSError) as exc:
|
|
raise HttpClientError(0, _network_error_message(exc, timeout), None) from exc
|