from __future__ import annotations from datetime import datetime from typing import Any import httpx from app.core.config import settings class NotFoundError(RuntimeError): pass class WebsiteContentAPIError(RuntimeError): pass class WebsiteContentService: """Authenticated HTTPS client for the website administration API.""" def __init__(self, api_url=None, api_token=None, client: httpx.Client | None = None): self.api_url = (api_url or settings.WEBSITE_CONTENT_API_URL).rstrip("/") explicit_token = api_token is not None self.api_token = api_token if explicit_token else settings.WEBSITE_CONTENT_API_TOKEN if not explicit_token and not self.api_token and settings.WEBSITE_CONTENT_MYSQL_PASSWORD: self.api_token = settings.WEBSITE_CONTENT_MYSQL_PASSWORD self.client = client or httpx.Client(timeout=settings.WEBSITE_CONTENT_API_TIMEOUT_SECONDS) def _request(self, method: str, resource: str, *, item_id=None, action=None, data=None, files=None, expect_bytes=False, extra_params=None): if not self.api_url or not self.api_token: raise WebsiteContentAPIError("Website admin-API er ikke konfigureret") params = {"resource": resource, **(extra_params or {})} if item_id is not None: params["id"] = item_id if action: params["action"] = action headers = {"X-Website-Admin-Token": self.api_token, "Accept": "application/json"} if settings.WEBSITE_CONTENT_API_HOST_HEADER: headers["Host"] = settings.WEBSITE_CONTENT_API_HOST_HEADER try: response = self.client.request( method, self.api_url, params=params, headers=headers, json=self._json_values(data) if data is not None and files is None else None, files=files, ) except httpx.HTTPError as exc: raise WebsiteContentAPIError("Kunne ikke kontakte website admin-API") from exc if response.status_code == 404: raise NotFoundError() if not response.is_success: try: payload = response.json() detail = payload.get("message") or payload.get("error") except (ValueError, AttributeError): detail = None raise WebsiteContentAPIError(detail or f"Website admin-API svarede {response.status_code}") if expect_bytes: return response.content, response.headers.get("content-type", "application/octet-stream") return response.json() def list(self, kind: str, include_hidden: bool = True) -> list[dict[str, Any]]: result = self._request("GET", kind, extra_params={"include_hidden": int(include_hidden)}) return result.get("items", result) if isinstance(result, dict) else result def get(self, kind: str, item_id: int) -> dict[str, Any]: return self._request("GET", kind, item_id=item_id) def create(self, kind: str, data: dict[str, Any]) -> dict[str, Any]: return self._request("POST", kind, data=data) def update(self, kind: str, item_id: int, data: dict[str, Any]) -> dict[str, Any]: return self._request("PATCH", kind, item_id=item_id, data=data) def upload_logo(self, item_id: int, content: bytes, mime_type: str) -> dict[str, Any]: return self._request("POST", "customers", item_id=item_id, action="logo", files={"logo": ("logo", content, mime_type)}) def logo(self, item_id: int) -> tuple[bytes, str]: return self._request("GET", "customers", item_id=item_id, action="logo", expect_bytes=True) def complete_operation(self, item_id: int, ends_at: datetime | None, is_public: bool): return self._request("POST", "operations", item_id=item_id, action="complete", data={"ends_at": ends_at, "is_public": is_public}) @classmethod def _json_values(cls, data): if data is None: return None return {key: value.isoformat() if isinstance(value, datetime) else str(value) if cls._is_url(value) else value for key, value in data.items()} @staticmethod def _is_url(value: Any) -> bool: return value is not None and value.__class__.__module__.startswith("pydantic.networks") website_content_service = WebsiteContentService()