"""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
)))