Source code for ezmsg.dashboard.backend.catalog

"""Installed component discovery, independent of a live GraphService connection.

Enumeration never loads entry points. Details are inspected in a disposable
interpreter so a broken import cannot terminate or hang the dashboard server.
"""

from __future__ import annotations

import hashlib
import importlib.metadata as metadata
import json
import subprocess
import sys
import tempfile
from pathlib import Path

GROUP = "ezmsg.components"


[docs] def registrations() -> list[tuple[dict, metadata.EntryPoint]]: result = [] for dist in metadata.distributions(): for ep in dist.entry_points: if ep.group != GROUP: continue package = dist.metadata.get("Name", "unknown") identity = f"{package.lower()}:{ep.name}:{ep.value}" result.append( ( { "id": hashlib.sha256(identity.encode()).hexdigest(), "name": ep.name, "value": ep.value, "distribution": package, "version": dist.version, }, ep, ) ) return sorted(result, key=lambda item: (item[0]["name"], item[0]["distribution"], item[0]["value"]))
[docs] def catalog_payload() -> dict: return { "interpreter": sys.executable, "components": [record for record, _ in registrations()], }
[docs] def component_details(component_id: str) -> dict: matches = [(record, ep) for record, ep in registrations() if record["id"] == component_id] if not matches: raise KeyError(component_id) record, _ = matches[0] if len(matches) != 1: return {**record, "available": False, "error": "Duplicate installed registration"} with tempfile.TemporaryDirectory(prefix="ezmsg-component-") as directory: output = Path(directory) / "details.json" try: completed = subprocess.run( [sys.executable, "-m", "ezmsg.dashboard.backend.catalog_worker", component_id, str(output)], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=30, check=False, ) if completed.returncode or not output.exists(): raise RuntimeError(f"Component inspection exited with status {completed.returncode}") return {**record, **json.loads(output.read_text())} except (subprocess.TimeoutExpired, RuntimeError, ValueError, OSError) as exc: return {**record, "available": False, "error": str(exc)}