| from __future__ import annotations |
|
|
| import json |
| import urllib.error |
| import urllib.request |
| from typing import Any |
|
|
| from time_machine.domain.errors import AdapterConfigurationError |
|
|
|
|
| class ModalEndpointClient: |
| def __init__( |
| self, |
| url: str, |
| timeout_seconds: float = 120.0, |
| bearer_token: str | None = None, |
| ) -> None: |
| clean_url = url.strip() |
| if not clean_url: |
| raise AdapterConfigurationError("Modal endpoint URL must not be empty.") |
| self.url = clean_url |
| self.timeout_seconds = timeout_seconds |
| self.bearer_token = bearer_token.strip() if bearer_token else None |
|
|
| def post_json(self, payload: dict[str, Any]) -> dict[str, Any]: |
| body = json.dumps(payload).encode("utf-8") |
| headers = { |
| "Content-Type": "application/json", |
| "Accept": "application/json", |
| } |
| if self.bearer_token: |
| headers["Authorization"] = f"Bearer {self.bearer_token}" |
|
|
| request = urllib.request.Request( |
| self.url, |
| data=body, |
| headers=headers, |
| method="POST", |
| ) |
| try: |
| with urllib.request.urlopen(request, timeout=self.timeout_seconds) as response: |
| response_body = response.read().decode("utf-8") |
| except urllib.error.HTTPError as exc: |
| detail = exc.read().decode("utf-8", errors="replace") |
| raise RuntimeError( |
| f"Modal endpoint {self.url} returned HTTP {exc.code}: {detail}" |
| ) from exc |
| except urllib.error.URLError as exc: |
| raise RuntimeError(f"Modal endpoint {self.url} is not reachable: {exc}") from exc |
|
|
| try: |
| decoded = json.loads(response_body) |
| except json.JSONDecodeError as exc: |
| raise RuntimeError( |
| f"Modal endpoint {self.url} returned non-JSON response." |
| ) from exc |
| if not isinstance(decoded, dict): |
| raise RuntimeError(f"Modal endpoint {self.url} returned a non-object JSON value.") |
| return decoded |
|
|