"""Read-only client for the reversible SBOM Nexus compatibility facade.""" from __future__ import annotations from typing import Any import httpx from api.config import settings class SBOMNexusError(RuntimeError): """An SBOM Nexus request failed without a usable compatibility response.""" def __init__(self, status_code: int, detail: str): super().__init__(detail) self.status_code = status_code self.detail = detail def reads_from_nexus() -> bool: return settings.sbom_nexus_read_mode == "nexus" def writes_to_nexus() -> bool: return settings.sbom_nexus_write_mode == "nexus" async def request_json( method: str, path: str, *, params: dict[str, Any] | None = None, body: dict[str, Any] | None = None, ) -> Any: """Call Nexus; never fall back silently to legacy storage.""" if not settings.sbom_nexus_url: raise SBOMNexusError(503, "SBOM Nexus read mode is enabled without SBOM_NEXUS_URL") try: async with httpx.AsyncClient( base_url=settings.sbom_nexus_url.rstrip("/"), timeout=settings.sbom_nexus_timeout_seconds, ) as client: response = await client.request(method, path, params=params, json=body) except httpx.RequestError as exc: raise SBOMNexusError(502, f"SBOM Nexus is unavailable: {exc.__class__.__name__}") from exc if response.status_code >= 400: detail = f"SBOM Nexus returned HTTP {response.status_code}" try: payload = response.json() if isinstance(payload, dict) and isinstance(payload.get("detail"), str): detail = payload["detail"] except ValueError: pass status_code = 404 if response.status_code == 404 else 502 raise SBOMNexusError(status_code, detail) try: return response.json() except ValueError as exc: raise SBOMNexusError(502, "SBOM Nexus returned invalid JSON") from exc async def get_json(path: str, *, params: dict[str, Any] | None = None) -> Any: return await request_json("GET", path, params=params) async def post_json(path: str, *, body: dict[str, Any]) -> Any: return await request_json("POST", path, body=body)