Skip to content

Commit fb2cfa2

Browse files
committed
beta6 - add sort_nodes_by_path_traversal function to get logical ordering of sites in the service path and segment
1 parent c26a0a7 commit fb2cfa2

File tree

2 files changed

+143
-1
lines changed

2 files changed

+143
-1
lines changed

cesnet_service_path_plugin/utils/utils_topology.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""
1616

1717
import logging
18+
from pprint import pprint
1819
from typing import Dict, List, Set, Optional, Any
1920

2021
logger = 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()

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "cesnet_service_path_plugin"
7-
version = "5.2.1b5"
7+
version = "5.2.1b6"
88
description = "Adds ability to create, edit and view service paths in the CESNET network."
99
authors = [
1010
{name = "Jan Krupa", email = "jan.krupa@cesnet.cz"},

0 commit comments

Comments
 (0)