"""Built-in exporter plugins and their registry.
Exporters are the package's hook into other programs: each one adapts a planned network
to a downstream consumer without that consumer's concerns reaching back into the core.
"""
from __future__ import annotations
from importlib import import_module
from typing import Any
from rbfenetmap.core.exceptions import PluginError
from rbfenetmap.core.pluginregistry import PluginRegistry, PluginSpec
__all__ = (
"BUILTIN_EXPORTERS",
"EXPORTER_PROFILES",
"available_exporters",
"create_exporter",
"create_exporter_registry",
"list_active_exporters",
"register_exporters",
"require_exporters",
)
_KIND = "exporter"
_MODULE_MAP = {
"JSONExporter": "rbfenetmap.plugins.exporters.basic_exporters",
"EdgeListExporter": "rbfenetmap.plugins.exporters.basic_exporters",
"GraphMLExporter": "rbfenetmap.plugins.exporters.basic_exporters",
"AmberExporter": "rbfenetmap.plugins.exporters.amber_exporter",
"HTMLGalleryExporter": "rbfenetmap.plugins.exporters.html_exporter",
}
BUILTIN_EXPORTERS: dict[str, PluginSpec] = {
"json": PluginSpec(
name="json",
kind=_KIND,
target="rbfenetmap.plugins.exporters.basic_exporters:JSONExporter",
description="Full round-trippable network, including rejected candidates.",
requires=(),
),
"edgelist": PluginSpec(
name="edgelist",
kind=_KIND,
target="rbfenetmap.plugins.exporters.basic_exporters:EdgeListExporter",
description="Plain 'source target cost' text, for shell and workflow use.",
requires=(),
),
"graphml": PluginSpec(
name="graphml",
kind=_KIND,
target="rbfenetmap.plugins.exporters.basic_exporters:GraphMLExporter",
description="GraphML for Cytoscape, Gephi, and similar viewers.",
requires=("networkx",),
),
"amber": PluginSpec(
name="amber",
kind=_KIND,
target="rbfenetmap.plugins.exporters.amber_exporter:AmberExporter",
description="amberstudio/guimapper edges.dat plus atommap_*.runconfig files.",
requires=("yaml",),
),
"html": PluginSpec(
name="html",
kind=_KIND,
target="rbfenetmap.plugins.exporters.html_exporter:HTMLGalleryExporter",
description="Self-contained HTML report with depictions and rejections.",
requires=("rdkit",),
),
}
EXPORTER_PROFILES: dict[str, tuple[str, ...]] = {
"all": tuple(BUILTIN_EXPORTERS),
"examples": ("json", "edgelist", "html"),
}
[docs]
def available_exporters() -> dict[str, PluginSpec]:
"""Return the built-in exporters whose requirements are importable."""
return {name: spec for name, spec in BUILTIN_EXPORTERS.items() if spec.available}
[docs]
def register_exporters(registry: PluginRegistry, names: tuple[str, ...] | None = None) -> PluginRegistry:
"""Register the named exporters (default: all built-ins) into *registry*."""
for name in names or tuple(BUILTIN_EXPORTERS):
try:
registry.register(BUILTIN_EXPORTERS[name])
except KeyError:
raise PluginError(f"Unknown built-in exporter {name!r}. Known: {sorted(BUILTIN_EXPORTERS)}.") from None
return registry
[docs]
def create_exporter_registry(profile: str = "all") -> PluginRegistry:
"""Return a registry with the exporters of *profile* registered and activated."""
if profile not in EXPORTER_PROFILES:
raise PluginError(f"Unknown exporter profile {profile!r}. Known: {sorted(EXPORTER_PROFILES)}.")
registry = register_exporters(PluginRegistry(), EXPORTER_PROFILES[profile])
for name in EXPORTER_PROFILES[profile]:
registry.activate(name, _KIND)
return registry
[docs]
def create_exporter(name: str, profile: str = "all", **kwargs: Any) -> Any:
"""Instantiate the exporter *name*."""
return create_exporter_registry(profile).create(name, _KIND, **kwargs)
[docs]
def list_active_exporters(profile: str = "all") -> list[str]:
"""Return the names of the exporters in *profile* that can be created."""
registry = create_exporter_registry(profile)
return sorted(spec.name for spec in registry.list_plugins(_KIND, active_only=True) if spec.available)
[docs]
def require_exporters(names: tuple[str, ...], profile: str = "all") -> None:
"""Raise unless every exporter in *names* is available."""
available = available_exporters()
missing = {n: BUILTIN_EXPORTERS[n].missing_requirements for n in names if n not in available}
if missing:
detail = "; ".join(f"{n} needs {list(mods)}" for n, mods in sorted(missing.items()))
raise PluginError(f"Required exporter(s) unavailable: {detail}.")
def __getattr__(name: str) -> Any:
"""Import exporter classes lazily (PEP 562)."""
if name not in _MODULE_MAP:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
return getattr(import_module(_MODULE_MAP[name]), name)