1515"""
1616
1717import logging
18+ from pprint import pprint
1819from typing import Dict , List , Set , Optional , Any
1920
2021logger = logging .getLogger (__name__ )
@@ -41,6 +42,142 @@ def get_topology_data(self) -> Dict[str, List[Dict]]:
4142 "edges" : self .edges ,
4243 }
4344
45+ def sort_nodes_by_path_traversal (self ) -> None :
46+ """
47+ Sort nodes (sites and circuits) within each segment by following the graph path.
48+
49+ This method:
50+ 1. Finds endpoint sites (sites with only one edge)
51+ 2. Traverses the graph from an endpoint
52+ 3. Reorders nodes to match the traversal order
53+
54+ Only affects site and circuit nodes that are children of segments.
55+ Service and segment nodes maintain their original order.
56+ """
57+ # Group nodes by parent segment
58+ segments = {}
59+ non_segment_children = []
60+
61+ for node in self .nodes :
62+ node_type = node ["data" ].get ("type" )
63+ parent = node ["data" ].get ("parent" )
64+
65+ # Keep service and segment nodes separate
66+ if node_type in ["service" , "segment" ]:
67+ non_segment_children .append (node )
68+ continue
69+
70+ # Group site and circuit nodes by their parent segment
71+ if parent and parent .startswith ("segment-" ):
72+ if parent not in segments :
73+ segments [parent ] = []
74+ segments [parent ].append (node )
75+ else :
76+ non_segment_children .append (node )
77+
78+ # Build edge lookup for quick traversal
79+ edge_map = self ._build_edge_map ()
80+
81+ # Sort nodes within each segment
82+ sorted_segment_nodes = []
83+ for segment_id , segment_nodes in segments .items ():
84+ sorted_nodes = self ._sort_segment_nodes (segment_nodes , edge_map )
85+ sorted_segment_nodes .extend (sorted_nodes )
86+
87+ # Reconstruct nodes list: service/segment nodes first, then sorted segment children
88+ self .nodes = non_segment_children + sorted_segment_nodes
89+
90+ def _build_edge_map (self ) -> Dict [str , List [str ]]:
91+ """
92+ Build a bidirectional edge map for graph traversal.
93+
94+ Returns:
95+ Dict mapping node_id -> list of connected node_ids
96+ """
97+ edge_map = {}
98+
99+ for edge in self .edges :
100+ source = edge ["data" ]["source" ]
101+ target = edge ["data" ]["target" ]
102+
103+ # Add bidirectional connections
104+ if source not in edge_map :
105+ edge_map [source ] = []
106+ edge_map [source ].append (target )
107+
108+ if target not in edge_map :
109+ edge_map [target ] = []
110+ edge_map [target ].append (source )
111+
112+ return edge_map
113+
114+ def _sort_segment_nodes (self , segment_nodes : List [Dict ], edge_map : Dict [str , List [str ]]) -> List [Dict ]:
115+ """
116+ Sort nodes within a segment by traversing the graph path.
117+
118+ Args:
119+ segment_nodes: List of site and circuit nodes in this segment
120+ edge_map: Bidirectional edge mapping
121+
122+ Returns:
123+ Sorted list of nodes following the graph path
124+ """
125+ if not segment_nodes :
126+ return []
127+
128+ # Create lookup for quick node access
129+ node_lookup = {node ["data" ]["id" ]: node for node in segment_nodes }
130+ node_ids = set (node_lookup .keys ())
131+
132+ # Find endpoint: a site with only one connection to nodes in this segment
133+ start_node_id = None
134+ for node_id in node_ids :
135+ node = node_lookup [node_id ]
136+ if node ["data" ]["type" ] == "site" :
137+ # Count connections to other nodes in this segment
138+ connections = [conn for conn in edge_map .get (node_id , []) if conn in node_ids ]
139+ if len (connections ) == 1 :
140+ start_node_id = node_id
141+ break
142+
143+ # If no endpoint found (shouldn't happen in a path), use first site
144+ if start_node_id is None :
145+ for node_id in node_ids :
146+ if node_lookup [node_id ]["data" ]["type" ] == "site" :
147+ start_node_id = node_id
148+ break
149+
150+ # If still no start node, return original order
151+ if start_node_id is None :
152+ return segment_nodes
153+
154+ # Traverse the graph
155+ sorted_nodes = []
156+ visited = set ()
157+ current_id = start_node_id
158+
159+ while current_id and current_id not in visited :
160+ # Add current node
161+ if current_id in node_lookup :
162+ sorted_nodes .append (node_lookup [current_id ])
163+ visited .add (current_id )
164+
165+ # Find next unvisited node
166+ next_id = None
167+ for connected_id in edge_map .get (current_id , []):
168+ if connected_id in node_ids and connected_id not in visited :
169+ next_id = connected_id
170+ break
171+
172+ current_id = next_id
173+
174+ # Add any remaining nodes that weren't in the main path (shouldn't happen)
175+ for node in segment_nodes :
176+ if node ["data" ]["id" ] not in visited :
177+ sorted_nodes .append (node )
178+
179+ return sorted_nodes
180+
44181 def add_service_path_node (self , service_path ) -> str :
45182 """
46183 Add a service path node to the topology.
@@ -353,6 +490,8 @@ def build_service_path_topology(service_path) -> Dict[str, List[Dict]]:
353490 for circuit in segment .circuits .all ():
354491 builder .process_circuit_with_sites (circuit , segment_id , site_a_id , site_b_id )
355492
493+ # Sort nodes by path traversal order
494+ builder .sort_nodes_by_path_traversal ()
356495 return builder .get_topology_data ()
357496
358497
@@ -397,4 +536,7 @@ def build_segment_topology(segment) -> Dict[str, List[Dict]]:
397536 for circuit in circuits :
398537 builder .process_circuit_with_sites (circuit , segment_id , site_a_id , site_b_id )
399538
539+ # Sort nodes by path traversal order
540+ builder .sort_nodes_by_path_traversal ()
541+
400542 return builder .get_topology_data ()
0 commit comments