"""Fragment client for fetching HTML fragments from coop apps. Lightweight httpx-based client (no Quart dependency) for Art-DAG to consume coop app fragments like nav-tree, auth-menu, and cart-mini. """ from __future__ import annotations import asyncio import logging import os from typing import Sequence import httpx log = logging.getLogger(__name__) FRAGMENT_HEADER = "X-Fragment-Request" _client: httpx.AsyncClient | None = None _DEFAULT_TIMEOUT = 2.0 def _get_client() -> httpx.AsyncClient: global _client if _client is None or _client.is_closed: _client = httpx.AsyncClient( timeout=httpx.Timeout(_DEFAULT_TIMEOUT), follow_redirects=False, ) return _client def _internal_url(app_name: str) -> str: """Resolve internal base URL for a coop app. Looks up ``INTERNAL_URL_{APP}`` first, falls back to ``http://{app}:8000``. """ env_key = f"INTERNAL_URL_{app_name.upper()}" return os.getenv(env_key, f"http://{app_name}:8000").rstrip("/") async def fetch_fragment( app_name: str, fragment_type: str, *, params: dict | None = None, timeout: float = _DEFAULT_TIMEOUT, required: bool = False, ) -> str: """Fetch an HTML fragment from a coop app. Returns empty string on failure by default (required=False). """ base = _internal_url(app_name) url = f"{base}/internal/fragments/{fragment_type}" try: resp = await _get_client().get( url, params=params, headers={FRAGMENT_HEADER: "1"}, timeout=timeout, ) if resp.status_code == 200: return resp.text msg = f"Fragment {app_name}/{fragment_type} returned {resp.status_code}" log.warning(msg) if required: raise RuntimeError(msg) return "" except RuntimeError: raise except Exception as exc: msg = f"Fragment {app_name}/{fragment_type} failed: {exc}" log.warning(msg) if required: raise RuntimeError(msg) from exc return "" async def fetch_fragments( requests: Sequence[tuple[str, str, dict | None]], *, timeout: float = _DEFAULT_TIMEOUT, required: bool = False, ) -> list[str]: """Fetch multiple fragments concurrently.""" return list(await asyncio.gather(*( fetch_fragment(app, ftype, params=params, timeout=timeout, required=required) for app, ftype, params in requests )))