from __future__ import annotations from urllib.parse import quote import httpx from app.config import settings class ManifestClient: def fetch_manifest(self, app_id: str, version: str) -> dict: app_id_part = quote(app_id, safe="") version_part = quote(version, safe="") url = f"{settings.robot_package_base_url}/api/apps/{app_id_part}/versions/{version_part}/manifest" response = httpx.get(url, follow_redirects=True, timeout=30) if response.is_error: raise RuntimeError(_format_manifest_error(response)) return response.json() def _format_manifest_error(response: httpx.Response) -> str: base_message = f"Manifest request failed with HTTP {response.status_code}: {response.url}" try: payload = response.json() except ValueError: detail = response.text.strip() return f"{base_message}. {detail}" if detail else base_message if not isinstance(payload, dict): return base_message message_parts = [] error = str(payload.get("error") or "").strip() detail = str(payload.get("detail") or "").strip() if error: message_parts.append(error) if detail: message_parts.append(detail) missing_files = payload.get("missingPackageFiles") if isinstance(missing_files, list) and missing_files: descriptions = [] for item in missing_files: if not isinstance(item, dict): continue package_name = str(item.get("packageName") or item.get("componentId") or "package").strip() version = str(item.get("version") or "").strip() descriptions.append(f"{package_name} {version}".strip()) if descriptions: message_parts.append(f"Missing package files: {', '.join(descriptions)}") return f"{base_message}. {' '.join(message_parts)}" if message_parts else base_message