Coverage for src / graphable / views / graphml.py: 100%
36 statements
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-16 21:32 +0000
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-16 21:32 +0000
1import xml.etree.ElementTree as ET
2from dataclasses import dataclass
3from logging import getLogger
4from pathlib import Path
5from typing import Any, Callable
6from xml.dom import minidom
8from ..graph import Graph
9from ..graphable import Graphable
10from ..registry import register_view
12logger = getLogger(__name__)
15@dataclass
16class GraphmlStylingConfig:
17 """
18 Configuration for GraphML serialization.
20 Attributes:
21 node_ref_fnc: Function to generate the unique ID for each node.
22 attr_mapping: Dictionary mapping Graphable attributes to GraphML data keys.
23 """
25 node_ref_fnc: Callable[[Graphable[Any]], str] = lambda n: str(n.reference)
26 # We could extend this to support more complex data mapping if needed.
29def create_topology_graphml(
30 graph: Graph, config: GraphmlStylingConfig | None = None
31) -> str:
32 """
33 Generate a GraphML (XML) representation of the graph.
35 Args:
36 graph (Graph): The graph to convert.
37 config (GraphmlStylingConfig | None): Export configuration.
39 Returns:
40 str: The GraphML XML string.
41 """
42 logger.debug("Creating GraphML representation.")
43 config = config or GraphmlStylingConfig()
45 # Create root element
46 root = ET.Element(
47 "graphml",
48 {
49 "xmlns": "http://graphml.graphdrawing.org/xmlns",
50 "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
51 "xsi:schemaLocation": "http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd",
52 },
53 )
55 # Define attributes (keys)
56 # 1. Tags
57 ET.SubElement(
58 root,
59 "key",
60 {
61 "id": "tags",
62 "for": "node",
63 "attr.name": "tags",
64 "attr.type": "string",
65 },
66 )
68 # Create graph element
69 graph_elem = ET.SubElement(root, "graph", {"id": "G", "edgedefault": "directed"})
71 # Nodes
72 for node in graph.topological_order():
73 node_id = config.node_ref_fnc(node)
74 node_elem = ET.SubElement(graph_elem, "node", {"id": node_id})
76 # Add tags as data
77 if node.tags:
78 data_elem = ET.SubElement(node_elem, "data", {"key": "tags"})
79 data_elem.text = ",".join(node.tags)
81 # Edges
82 for dependent, _ in graph.internal_dependents(node):
83 dep_id = config.node_ref_fnc(dependent)
84 ET.SubElement(
85 graph_elem,
86 "edge",
87 {
88 "id": f"e_{node_id}_{dep_id}",
89 "source": node_id,
90 "target": dep_id,
91 },
92 )
94 # Pretty print XML
95 xml_str = ET.tostring(root, encoding="utf-8")
96 parsed_xml = minidom.parseString(xml_str)
97 return parsed_xml.toprettyxml(indent=" ")
100@register_view(".graphml", creator_fnc=create_topology_graphml)
101def export_topology_graphml(
102 graph: Graph, output: Path, config: GraphmlStylingConfig | None = None
103) -> None:
104 """
105 Export the graph to a GraphML (.graphml) file.
107 Args:
108 graph (Graph): The graph to export.
109 output (Path): The output file path.
110 config (GraphmlStylingConfig | None): Export configuration.
111 """
112 logger.info(f"Exporting GraphML to: {output}")
113 with open(output, "w+") as f:
114 f.write(create_topology_graphml(graph, config))