unifi-network-maps 1.4.0__py3-none-any.whl → 1.4.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- unifi_network_maps/__init__.py +1 -1
- unifi_network_maps/__main__.py +8 -0
- unifi_network_maps/adapters/unifi.py +266 -24
- unifi_network_maps/cli/main.py +352 -107
- unifi_network_maps/io/debug.py +15 -5
- unifi_network_maps/io/export.py +20 -1
- unifi_network_maps/model/topology.py +125 -71
- unifi_network_maps/render/device_ports_md.py +31 -18
- unifi_network_maps/render/lldp_md.py +87 -43
- unifi_network_maps/render/mermaid.py +96 -49
- unifi_network_maps/render/svg.py +614 -318
- {unifi_network_maps-1.4.0.dist-info → unifi_network_maps-1.4.2.dist-info}/METADATA +57 -82
- {unifi_network_maps-1.4.0.dist-info → unifi_network_maps-1.4.2.dist-info}/RECORD +17 -16
- {unifi_network_maps-1.4.0.dist-info → unifi_network_maps-1.4.2.dist-info}/WHEEL +0 -0
- {unifi_network_maps-1.4.0.dist-info → unifi_network_maps-1.4.2.dist-info}/entry_points.txt +0 -0
- {unifi_network_maps-1.4.0.dist-info → unifi_network_maps-1.4.2.dist-info}/licenses/LICENSE +0 -0
- {unifi_network_maps-1.4.0.dist-info → unifi_network_maps-1.4.2.dist-info}/top_level.txt +0 -0
|
@@ -9,7 +9,9 @@ from .mermaid_theme import DEFAULT_THEME, MermaidTheme, class_defs
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def _escape(label: str) -> str:
|
|
12
|
-
|
|
12
|
+
normalized = label.replace("\r\n", "\n").replace("\r", "\n")
|
|
13
|
+
escaped = normalized.replace("\\", "\\\\").replace("\n", "\\n")
|
|
14
|
+
return escaped.replace('"', '\\"')
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
def _slugify(value: str) -> str:
|
|
@@ -54,39 +56,45 @@ def _node_ref(name: str, node_id: str) -> str:
|
|
|
54
56
|
return f'{node_id}["{_escape(name)}"]'
|
|
55
57
|
|
|
56
58
|
|
|
57
|
-
def
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
def _group_nodes(groups: dict[str, list[str]] | None) -> list[str]:
|
|
60
|
+
if not groups:
|
|
61
|
+
return []
|
|
62
|
+
nodes: list[str] = []
|
|
63
|
+
for members in groups.values():
|
|
64
|
+
nodes.extend(members)
|
|
65
|
+
return nodes
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _render_group_sections(
|
|
69
|
+
lines: list[str],
|
|
70
|
+
groups: dict[str, list[str]],
|
|
60
71
|
*,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
group_order: list[str] | None,
|
|
73
|
+
id_map: dict[str, str],
|
|
74
|
+
) -> None:
|
|
75
|
+
ordered = group_order or list(groups.keys())
|
|
76
|
+
for group_name in ordered:
|
|
77
|
+
members = groups.get(group_name, [])
|
|
78
|
+
if not members:
|
|
79
|
+
continue
|
|
80
|
+
group_id = _slugify(f"group_{group_name}")
|
|
81
|
+
label = group_name.replace("_", " ").title()
|
|
82
|
+
lines.append(f' subgraph {group_id}["{_escape(label)}"];')
|
|
83
|
+
for member in members:
|
|
84
|
+
lines.append(f" {_node_ref(member, id_map[member])};")
|
|
85
|
+
lines.append(" end")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _render_edge_lines(
|
|
89
|
+
lines: list[str],
|
|
90
|
+
edges: list[Edge],
|
|
91
|
+
*,
|
|
92
|
+
id_map: dict[str, str],
|
|
93
|
+
use_node_labels: bool,
|
|
94
|
+
) -> tuple[list[int], list[int]]:
|
|
73
95
|
poe_links: list[int] = []
|
|
74
96
|
wireless_links: list[int] = []
|
|
75
|
-
|
|
76
|
-
if groups:
|
|
77
|
-
ordered = group_order or list(groups.keys())
|
|
78
|
-
for group_name in ordered:
|
|
79
|
-
members = groups.get(group_name, [])
|
|
80
|
-
if not members:
|
|
81
|
-
continue
|
|
82
|
-
group_id = _slugify(f"group_{group_name}")
|
|
83
|
-
label = group_name.replace("_", " ").title()
|
|
84
|
-
lines.append(f' subgraph {group_id}["{_escape(label)}"];')
|
|
85
|
-
for member in members:
|
|
86
|
-
lines.append(f" {_node_ref(member, id_map[member])};")
|
|
87
|
-
lines.append(" end")
|
|
88
|
-
use_node_labels = not groups
|
|
89
|
-
for edge in edge_list:
|
|
97
|
+
for index, edge in enumerate(edges):
|
|
90
98
|
if use_node_labels:
|
|
91
99
|
left = _node_ref(edge.left, id_map[edge.left])
|
|
92
100
|
right = _node_ref(edge.right, id_map[edge.right])
|
|
@@ -99,25 +107,41 @@ def render_mermaid(
|
|
|
99
107
|
else:
|
|
100
108
|
lines.append(f" {left} --- {right};")
|
|
101
109
|
if edge.poe:
|
|
102
|
-
poe_links.append(
|
|
110
|
+
poe_links.append(index)
|
|
103
111
|
if edge.wireless:
|
|
104
|
-
wireless_links.append(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
112
|
+
wireless_links.append(index)
|
|
113
|
+
return poe_links, wireless_links
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _render_node_classes(
|
|
117
|
+
lines: list[str],
|
|
118
|
+
*,
|
|
119
|
+
node_types: dict[str, str],
|
|
120
|
+
id_map: dict[str, str],
|
|
121
|
+
theme: MermaidTheme,
|
|
122
|
+
) -> None:
|
|
123
|
+
class_map = {
|
|
124
|
+
"gateway": "node_gateway",
|
|
125
|
+
"switch": "node_switch",
|
|
126
|
+
"ap": "node_ap",
|
|
127
|
+
"client": "node_client",
|
|
128
|
+
"other": "node_other",
|
|
129
|
+
}
|
|
130
|
+
for name, node_type in node_types.items():
|
|
131
|
+
class_name = class_map.get(node_type, "node_other")
|
|
132
|
+
node_id = id_map.get(name)
|
|
133
|
+
if node_id:
|
|
134
|
+
lines.append(f" class {node_id} {class_name};")
|
|
135
|
+
lines.extend(class_defs(theme))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _render_link_styles(
|
|
139
|
+
lines: list[str],
|
|
140
|
+
*,
|
|
141
|
+
poe_links: list[int],
|
|
142
|
+
wireless_links: list[int],
|
|
143
|
+
theme: MermaidTheme,
|
|
144
|
+
) -> None:
|
|
121
145
|
for index in poe_links:
|
|
122
146
|
lines.append(
|
|
123
147
|
" linkStyle "
|
|
@@ -126,6 +150,29 @@ def render_mermaid(
|
|
|
126
150
|
)
|
|
127
151
|
for index in wireless_links:
|
|
128
152
|
lines.append(f" linkStyle {index} stroke-dasharray: 5 4;")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def render_mermaid(
|
|
156
|
+
edges: Iterable[Edge],
|
|
157
|
+
direction: str = "LR",
|
|
158
|
+
*,
|
|
159
|
+
groups: dict[str, list[str]] | None = None,
|
|
160
|
+
group_order: list[str] | None = None,
|
|
161
|
+
node_types: dict[str, str] | None = None,
|
|
162
|
+
theme: MermaidTheme = DEFAULT_THEME,
|
|
163
|
+
) -> str:
|
|
164
|
+
edge_list = list(edges)
|
|
165
|
+
id_map = _build_id_map(edge_list, _group_nodes(groups))
|
|
166
|
+
lines = [f"graph {direction}"]
|
|
167
|
+
if groups:
|
|
168
|
+
_render_group_sections(lines, groups, group_order=group_order, id_map=id_map)
|
|
169
|
+
use_node_labels = not groups
|
|
170
|
+
poe_links, wireless_links = _render_edge_lines(
|
|
171
|
+
lines, edge_list, id_map=id_map, use_node_labels=use_node_labels
|
|
172
|
+
)
|
|
173
|
+
if node_types:
|
|
174
|
+
_render_node_classes(lines, node_types=node_types, id_map=id_map, theme=theme)
|
|
175
|
+
_render_link_styles(lines, poe_links=poe_links, wireless_links=wireless_links, theme=theme)
|
|
129
176
|
return "\n".join(lines) + "\n"
|
|
130
177
|
|
|
131
178
|
|