Source code for package_maximizer.utils.exporters

"""
Result exporters for Package Maximizer.

Serialize maximization results and dependency/conflict graphs to multiple
formats: JSON, CSV, and GraphML (for visualization in external tools such as
yEd, Gephi, or NetworkX).
"""

from __future__ import annotations

import csv
import io
import json
from typing import Iterable, Sequence

from ..core.package import Package


[docs] def to_json( packages: Sequence[Package], selected: Iterable[str], *, metadata: dict | None = None, ) -> str: """ Export results as a JSON string. Args: packages: All candidate packages. selected: Names of selected (maximized) packages. metadata: Optional extra fields to include at top level. Returns: JSON-formatted string. """ selected_set = set(selected) payload: dict = { "selected": sorted(selected_set), "rejected": sorted(p.name for p in packages if p.name not in selected_set), "packages": [ { "name": p.name, "version": p.version, "selected": p.name in selected_set, "conflicts": list(p.conflicts), "depends": list(p.depends), } for p in packages ], } if metadata: payload["metadata"] = metadata return json.dumps(payload, indent=2, ensure_ascii=False)
[docs] def to_csv(packages: Sequence[Package], selected: Iterable[str]) -> str: """ Export results as CSV (name,version,selected,conflicts,depends). Args: packages: All candidate packages. selected: Names of selected packages. Returns: CSV-formatted string. """ selected_set = set(selected) buf = io.StringIO() writer = csv.writer(buf) writer.writerow(["name", "version", "selected", "conflicts", "depends"]) for p in packages: writer.writerow( [ p.name, p.version, "true" if p.name in selected_set else "false", ";".join(p.conflicts), ";".join(p.depends), ] ) return buf.getvalue()
[docs] def to_graphml(packages: Sequence[Package], selected: Iterable[str]) -> str: """ Export the conflict/dependency graph as GraphML. Nodes are packages (``selected`` attribute marks the result set). Edges are conflicts (type="conflict") and dependencies (type="depends"). Args: packages: All candidate packages. selected: Names of selected packages. Returns: GraphML-formatted XML string. """ selected_set = set(selected) names = {p.name for p in packages} nodes: list[str] = [] for p in packages: nodes.append( f' <node id="{_esc(p.name)}">\n' f' <data key="selected">{str(p.name in selected_set).lower()}</data>\n' f' <data key="version">{_esc(p.version)}</data>\n' f" </node>" ) edges: list[str] = [] edge_id = 0 seen_edges: set[frozenset] = set() for p in packages: for dep in p.depends: if dep in names: key = frozenset({p.name, dep}) if key not in seen_edges: seen_edges.add(key) edges.append(_edge(edge_id, p.name, dep, "depends")) edge_id += 1 for conflict in p.conflicts: if conflict in names: key = frozenset({p.name, conflict}) if key not in seen_edges: seen_edges.add(key) edges.append(_edge(edge_id, p.name, conflict, "conflict")) edge_id += 1 return ( '<?xml version="1.0" encoding="UTF-8"?>\n' '<graphml xmlns="http://graphml.graphdrawing.org/xmlns">\n' ' <key id="selected" for="node" attr.name="selected" attr.type="boolean"/>\n' ' <key id="version" for="node" attr.name="version" attr.type="string"/>\n' ' <key id="etype" for="edge" attr.name="type" attr.type="string"/>\n' ' <graph id="package-maximizer" edgedefault="undirected">\n' f"{chr(10).join(nodes)}\n" f"{chr(10).join(edges)}\n" " </graph>\n" "</graphml>\n" )
def _edge(edge_id: int, src: str, dst: str, etype: str) -> str: return ( f' <edge id="e{edge_id}" source="{_esc(src)}" target="{_esc(dst)}">\n' f' <data key="etype">{etype}</data>\n' f" </edge>" ) def _esc(value: str) -> str: return ( value.replace("&", "&amp;") .replace("<", "&lt;") .replace(">", "&gt;") .replace('"', "&quot;") )