from __future__ import annotations import xml.etree.ElementTree as ET from typing import Dict, List, Optional from .graph import FieldDef, NodeDef, ConnectionDef, WorkflowGraph def parse_workflow(path: str) -> WorkflowGraph: """Parse a .yxmd XML file into a WorkflowGraph.""" tree = ET.parse(path) root = tree.getroot() nodes: Dict[int, NodeDef] = {} _collect_nodes(root, nodes) connections: List[ConnectionDef] = [] for conn_el in root.findall("Connections/Connection"): orig = conn_el.find("Origin") dest = conn_el.find("Destination") if orig is None or dest is None: continue connections.append(ConnectionDef( origin_id=int(orig.attrib["ToolID"]), origin_anchor=orig.attrib.get("Connection", "Output"), dest_id=int(dest.attrib["ToolID"]), dest_anchor=dest.attrib.get("Connection", "Input"), name=conn_el.attrib.get("name"), wireless=conn_el.attrib.get("Wireless", "False") == "True", )) props = root.find("Properties") return WorkflowGraph(nodes=nodes, connections=connections, properties=props) def _collect_nodes(parent: ET.Element, nodes: Dict[int, NodeDef]) -> None: """Recursively collect Node elements, flattening ChildNodes containers.""" for node_el in parent.findall("Nodes/Node"): _parse_node(node_el, nodes) # Recurse into ChildNodes (tool containers) child_nodes = node_el.find("ChildNodes") if child_nodes is not None: for child in child_nodes.findall("Node"): _parse_node(child, nodes) grandchildren = child.find("ChildNodes") if grandchildren is not None: _collect_nodes_flat(grandchildren, nodes) def _collect_nodes_flat(parent: ET.Element, nodes: Dict[int, NodeDef]) -> None: for node_el in parent.findall("Node"): _parse_node(node_el, nodes) child_nodes = node_el.find("ChildNodes") if child_nodes is not None: _collect_nodes_flat(child_nodes, nodes) def _parse_node(node_el: ET.Element, nodes: Dict[int, NodeDef]) -> None: tid = int(node_el.attrib["ToolID"]) gui = node_el.find("GuiSettings") plugin = gui.attrib.get("Plugin", "") if gui is not None else "" config = node_el.find("Properties/Configuration") pos_el = gui.find("Position") if gui is not None else None pos = ( int(pos_el.attrib.get("x", 0)), int(pos_el.attrib.get("y", 0)), ) if pos_el is not None else (0, 0) schema = _parse_schema(node_el) nodes[tid] = NodeDef(tool_id=tid, plugin=plugin, config=config, output_schema=schema, position=pos) def _parse_schema(node_el: ET.Element) -> List[FieldDef]: fields = [] for f in node_el.findall(".//MetaInfo/RecordInfo/Field"): size_str = f.attrib.get("size") size = int(float(size_str)) if size_str else None fields.append(FieldDef( name=f.attrib["name"], type=f.attrib.get("type", "V_String"), size=size, source=f.attrib.get("source"), )) return fields