1515"""
1616
1717import logging
18- from pprint import pprint
1918from typing import Dict , List , Set , Optional , Any
2019
2120logger = logging .getLogger (__name__ )
@@ -48,12 +47,15 @@ def sort_nodes_by_path_traversal(self) -> None:
4847
4948 This method:
5049 1. Finds endpoint sites (sites with only one edge)
51- 2. Traverses the graph from an endpoint
50+ 2. Traverses the graph from an endpoint (preferring site_a)
5251 3. Reorders nodes to match the traversal order
5352
5453 Only affects site and circuit nodes that are children of segments.
5554 Service and segment nodes maintain their original order.
5655 """
56+ # First pass: mark site_a nodes for each segment
57+ self ._mark_site_a_nodes ()
58+
5759 # Group nodes by parent segment
5860 segments = {}
5961 non_segment_children = []
@@ -81,12 +83,35 @@ def sort_nodes_by_path_traversal(self) -> None:
8183 # Sort nodes within each segment
8284 sorted_segment_nodes = []
8385 for segment_id , segment_nodes in segments .items ():
84- sorted_nodes = self ._sort_segment_nodes (segment_nodes , edge_map )
86+ sorted_nodes = self ._sort_segment_nodes (segment_nodes , edge_map , segment_id )
8587 sorted_segment_nodes .extend (sorted_nodes )
8688
8789 # Reconstruct nodes list: service/segment nodes first, then sorted segment children
8890 self .nodes = non_segment_children + sorted_segment_nodes
8991
92+ def _mark_site_a_nodes (self ) -> None :
93+ """
94+ Mark nodes that represent site_a in their segments.
95+ This helps determine the correct starting point for traversal.
96+ """
97+ # Build a map of segment_id -> site_a_id from segment nodes
98+ segment_site_a_map = {}
99+
100+ for node in self .nodes :
101+ if node ["data" ].get ("type" ) == "segment" :
102+ segment_id = node ["data" ]["id" ]
103+ # Look for site_a_id stored during segment creation
104+ if "site_a_id" in node ["data" ]:
105+ segment_site_a_map [segment_id ] = node ["data" ]["site_a_id" ]
106+
107+ # Mark site nodes that are site_a
108+ for node in self .nodes :
109+ if node ["data" ].get ("type" ) == "site" :
110+ parent_segment = node ["data" ].get ("parent" )
111+ if parent_segment and parent_segment in segment_site_a_map :
112+ if node ["data" ]["id" ] == segment_site_a_map [parent_segment ]:
113+ node ["data" ]["is_site_a" ] = True
114+
90115 def _build_edge_map (self ) -> Dict [str , List [str ]]:
91116 """
92117 Build a bidirectional edge map for graph traversal.
@@ -111,13 +136,16 @@ def _build_edge_map(self) -> Dict[str, List[str]]:
111136
112137 return edge_map
113138
114- def _sort_segment_nodes (self , segment_nodes : List [Dict ], edge_map : Dict [str , List [str ]]) -> List [Dict ]:
139+ def _sort_segment_nodes (
140+ self , segment_nodes : List [Dict ], edge_map : Dict [str , List [str ]], segment_id : str = None
141+ ) -> List [Dict ]:
115142 """
116143 Sort nodes within a segment by traversing the graph path.
117144
118145 Args:
119146 segment_nodes: List of site and circuit nodes in this segment
120147 edge_map: Bidirectional edge mapping
148+ segment_id: Optional segment ID to determine site_a preference
121149
122150 Returns:
123151 Sorted list of nodes following the graph path
@@ -129,17 +157,37 @@ def _sort_segment_nodes(self, segment_nodes: List[Dict], edge_map: Dict[str, Lis
129157 node_lookup = {node ["data" ]["id" ]: node for node in segment_nodes }
130158 node_ids = set (node_lookup .keys ())
131159
132- # Find endpoint: a site with only one connection to nodes in this segment
133- start_node_id = None
160+ # Find all endpoint sites (sites with only one connection)
161+ endpoint_sites = []
134162 for node_id in node_ids :
135163 node = node_lookup [node_id ]
136164 if node ["data" ]["type" ] == "site" :
137165 # Count connections to other nodes in this segment
138166 connections = [conn for conn in edge_map .get (node_id , []) if conn in node_ids ]
139167 if len (connections ) == 1 :
168+ endpoint_sites .append (node_id )
169+
170+ # Determine starting node
171+ start_node_id = None
172+
173+ # If we have segment_id and can identify site_a, prefer it as start
174+ if segment_id and endpoint_sites :
175+ # Look for site_a marker in the segment nodes
176+ # Check if any endpoint has site_a_id in its data
177+ for node_id in endpoint_sites :
178+ node = node_lookup [node_id ]
179+ # Check if this node has a marker indicating it's site_a
180+ if node ["data" ].get ("is_site_a" ):
140181 start_node_id = node_id
141182 break
142183
184+ # If no site_a marker found, use first endpoint
185+ if start_node_id is None and endpoint_sites :
186+ start_node_id = endpoint_sites [0 ]
187+ elif endpoint_sites :
188+ # No segment context, just use first endpoint
189+ start_node_id = endpoint_sites [0 ]
190+
143191 # If no endpoint found (shouldn't happen in a path), use first site
144192 if start_node_id is None :
145193 for node_id in node_ids :
@@ -204,13 +252,14 @@ def add_service_path_node(self, service_path) -> str:
204252 )
205253 return node_id
206254
207- def add_segment_node (self , segment , parent_id : Optional [str ] = None ) -> str :
255+ def add_segment_node (self , segment , parent_id : Optional [str ] = None , site_a_id : Optional [ str ] = None ) -> str :
208256 """
209257 Add a segment node to the topology.
210258
211259 Args:
212260 segment: Segment model instance
213261 parent_id: Optional parent node ID (e.g., service path ID)
262+ site_a_id: Optional site_a ID for ordering reference
214263
215264 Returns:
216265 str: The node ID for the segment
@@ -229,6 +278,9 @@ def add_segment_node(self, segment, parent_id: Optional[str] = None) -> str:
229278 if parent_id :
230279 node_data ["parent" ] = parent_id
231280
281+ if site_a_id :
282+ node_data ["site_a_id" ] = site_a_id
283+
232284 self .nodes .append ({"data" : node_data })
233285 return node_id
234286
@@ -480,7 +532,12 @@ def build_service_path_topology(service_path) -> Dict[str, List[Dict]]:
480532
481533 # Process each segment
482534 for segment in segments :
483- segment_id = builder .add_segment_node (segment , parent_id = service_path_id )
535+ # Add segment sites first to get their IDs
536+ site_a_id = f"site-{ segment .site_a .pk } "
537+ site_b_id = f"site-{ segment .site_b .pk } "
538+
539+ # Add segment node with site_a reference
540+ segment_id = builder .add_segment_node (segment , parent_id = service_path_id , site_a_id = site_a_id )
484541
485542 # Add segment sites
486543 site_a_id = builder .add_or_update_site_node (segment .site_a , segment_id )
@@ -492,6 +549,7 @@ def build_service_path_topology(service_path) -> Dict[str, List[Dict]]:
492549
493550 # Sort nodes by path traversal order
494551 builder .sort_nodes_by_path_traversal ()
552+
495553 return builder .get_topology_data ()
496554
497555
@@ -512,8 +570,12 @@ def build_segment_topology(segment) -> Dict[str, List[Dict]]:
512570 """
513571 builder = TopologyBuilder ()
514572
515- # Add segment as root node (no parent)
516- segment_id = builder .add_segment_node (segment , parent_id = None )
573+ # Get site IDs first
574+ site_a_id = f"site-{ segment .site_a .pk } "
575+ site_b_id = f"site-{ segment .site_b .pk } "
576+
577+ # Add segment as root node (no parent) with site_a reference
578+ segment_id = builder .add_segment_node (segment , parent_id = None , site_a_id = site_a_id )
517579
518580 # Add segment sites
519581 site_a_id = builder .add_or_update_site_node (segment .site_a , segment_id )
0 commit comments