"""HTTP client for the mock-suite-sat-service audit log. Thin wrapper over ``GET /tiles/audit`` (and its alias ``GET /mock/audit``) that the FT-P-17 scenario uses to verify every generated tile was accepted with HTTP 202 at landing-time upload. Reading the audit log is a one-shot, end-of-run operation, so the helper is synchronous httpx — no streaming, no retries (the service is co-located in the compose harness and a failure to reach it is itself a test failure, not something to paper over). """ from __future__ import annotations from typing import Any import httpx DEFAULT_TIMEOUT_S = 10.0 DEFAULT_AUDIT_PATH = "/tiles/audit" def fetch_audit( base_url: str, run_id: str, *, timeout_s: float = DEFAULT_TIMEOUT_S, audit_path: str = DEFAULT_AUDIT_PATH, transport: httpx.BaseTransport | None = None, ) -> list[dict[str, Any]]: """Return the ``entries`` list from the mock-suite-sat-service audit log. The endpoint returns ``{"run_id": str, "entries": [...]}``; this helper unwraps the list. An empty list is a legal response (the SUT may not have uploaded anything yet). Raises ``RuntimeError`` on non-2xx HTTP status or a malformed response shape — the scenario test wants those to fail loudly. ``transport`` is a unit-test seam: pass an ``httpx.MockTransport`` to feed canned responses without spinning up the real service. """ if not base_url: raise RuntimeError("fetch_audit: base_url must be a non-empty string") if not run_id: raise RuntimeError("fetch_audit: run_id must be a non-empty string") url = base_url.rstrip("/") + audit_path client_kwargs: dict[str, Any] = {"timeout": timeout_s} if transport is not None: client_kwargs["transport"] = transport with httpx.Client(**client_kwargs) as client: resp = client.get(url, params={"run_id": run_id}) if resp.status_code >= 300: raise RuntimeError( f"fetch_audit: {url}?run_id={run_id} returned HTTP " f"{resp.status_code}: body={resp.text[:200]!r}" ) try: body = resp.json() except ValueError as exc: raise RuntimeError( f"fetch_audit: {url}?run_id={run_id} body is not valid JSON: {exc}" ) from exc if not isinstance(body, dict): raise RuntimeError( f"fetch_audit: {url}?run_id={run_id} body is not a JSON object: " f"got {type(body).__name__}" ) entries = body.get("entries") if not isinstance(entries, list): raise RuntimeError( f"fetch_audit: {url}?run_id={run_id} body missing 'entries' list: " f"keys={list(body.keys())}" ) return entries