unifi-network-maps 1.4.1__py3-none-any.whl → 1.4.3__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/adapters/unifi.py +266 -24
- unifi_network_maps/assets/themes/dark.yaml +3 -0
- 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 +105 -49
- unifi_network_maps/render/mermaid_theme.py +15 -5
- unifi_network_maps/render/svg.py +614 -318
- unifi_network_maps/render/theme.py +19 -0
- {unifi_network_maps-1.4.1.dist-info → unifi_network_maps-1.4.3.dist-info}/METADATA +57 -82
- {unifi_network_maps-1.4.1.dist-info → unifi_network_maps-1.4.3.dist-info}/RECORD +19 -19
- {unifi_network_maps-1.4.1.dist-info → unifi_network_maps-1.4.3.dist-info}/WHEEL +0 -0
- {unifi_network_maps-1.4.1.dist-info → unifi_network_maps-1.4.3.dist-info}/entry_points.txt +0 -0
- {unifi_network_maps-1.4.1.dist-info → unifi_network_maps-1.4.3.dist-info}/licenses/LICENSE +0 -0
- {unifi_network_maps-1.4.1.dist-info → unifi_network_maps-1.4.3.dist-info}/top_level.txt +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
from collections.abc import Iterable
|
|
6
7
|
|
|
7
8
|
from ..model.topology import Edge
|
|
@@ -9,7 +10,9 @@ from .mermaid_theme import DEFAULT_THEME, MermaidTheme, class_defs
|
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def _escape(label: str) -> str:
|
|
12
|
-
|
|
13
|
+
normalized = label.replace("\r\n", "\n").replace("\r", "\n")
|
|
14
|
+
escaped = normalized.replace("\\", "\\\\").replace("\n", "\\n")
|
|
15
|
+
return escaped.replace('"', '\\"')
|
|
13
16
|
|
|
14
17
|
|
|
15
18
|
def _slugify(value: str) -> str:
|
|
@@ -54,39 +57,45 @@ def _node_ref(name: str, node_id: str) -> str:
|
|
|
54
57
|
return f'{node_id}["{_escape(name)}"]'
|
|
55
58
|
|
|
56
59
|
|
|
57
|
-
def
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
def _group_nodes(groups: dict[str, list[str]] | None) -> list[str]:
|
|
61
|
+
if not groups:
|
|
62
|
+
return []
|
|
63
|
+
nodes: list[str] = []
|
|
64
|
+
for members in groups.values():
|
|
65
|
+
nodes.extend(members)
|
|
66
|
+
return nodes
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _render_group_sections(
|
|
70
|
+
lines: list[str],
|
|
71
|
+
groups: dict[str, list[str]],
|
|
60
72
|
*,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
group_order: list[str] | None,
|
|
74
|
+
id_map: dict[str, str],
|
|
75
|
+
) -> None:
|
|
76
|
+
ordered = group_order or list(groups.keys())
|
|
77
|
+
for group_name in ordered:
|
|
78
|
+
members = groups.get(group_name, [])
|
|
79
|
+
if not members:
|
|
80
|
+
continue
|
|
81
|
+
group_id = _slugify(f"group_{group_name}")
|
|
82
|
+
label = group_name.replace("_", " ").title()
|
|
83
|
+
lines.append(f' subgraph {group_id}["{_escape(label)}"];')
|
|
84
|
+
for member in members:
|
|
85
|
+
lines.append(f" {_node_ref(member, id_map[member])};")
|
|
86
|
+
lines.append(" end")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _render_edge_lines(
|
|
90
|
+
lines: list[str],
|
|
91
|
+
edges: list[Edge],
|
|
92
|
+
*,
|
|
93
|
+
id_map: dict[str, str],
|
|
94
|
+
use_node_labels: bool,
|
|
95
|
+
) -> tuple[list[int], list[int]]:
|
|
73
96
|
poe_links: list[int] = []
|
|
74
97
|
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:
|
|
98
|
+
for index, edge in enumerate(edges):
|
|
90
99
|
if use_node_labels:
|
|
91
100
|
left = _node_ref(edge.left, id_map[edge.left])
|
|
92
101
|
right = _node_ref(edge.right, id_map[edge.right])
|
|
@@ -99,25 +108,41 @@ def render_mermaid(
|
|
|
99
108
|
else:
|
|
100
109
|
lines.append(f" {left} --- {right};")
|
|
101
110
|
if edge.poe:
|
|
102
|
-
poe_links.append(
|
|
111
|
+
poe_links.append(index)
|
|
103
112
|
if edge.wireless:
|
|
104
|
-
wireless_links.append(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
113
|
+
wireless_links.append(index)
|
|
114
|
+
return poe_links, wireless_links
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _render_node_classes(
|
|
118
|
+
lines: list[str],
|
|
119
|
+
*,
|
|
120
|
+
node_types: dict[str, str],
|
|
121
|
+
id_map: dict[str, str],
|
|
122
|
+
theme: MermaidTheme,
|
|
123
|
+
) -> None:
|
|
124
|
+
class_map = {
|
|
125
|
+
"gateway": "node_gateway",
|
|
126
|
+
"switch": "node_switch",
|
|
127
|
+
"ap": "node_ap",
|
|
128
|
+
"client": "node_client",
|
|
129
|
+
"other": "node_other",
|
|
130
|
+
}
|
|
131
|
+
for name, node_type in node_types.items():
|
|
132
|
+
class_name = class_map.get(node_type, "node_other")
|
|
133
|
+
node_id = id_map.get(name)
|
|
134
|
+
if node_id:
|
|
135
|
+
lines.append(f" class {node_id} {class_name};")
|
|
136
|
+
lines.extend(class_defs(theme))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _render_link_styles(
|
|
140
|
+
lines: list[str],
|
|
141
|
+
*,
|
|
142
|
+
poe_links: list[int],
|
|
143
|
+
wireless_links: list[int],
|
|
144
|
+
theme: MermaidTheme,
|
|
145
|
+
) -> None:
|
|
121
146
|
for index in poe_links:
|
|
122
147
|
lines.append(
|
|
123
148
|
" linkStyle "
|
|
@@ -126,6 +151,37 @@ def render_mermaid(
|
|
|
126
151
|
)
|
|
127
152
|
for index in wireless_links:
|
|
128
153
|
lines.append(f" linkStyle {index} stroke-dasharray: 5 4;")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def render_mermaid(
|
|
157
|
+
edges: Iterable[Edge],
|
|
158
|
+
direction: str = "LR",
|
|
159
|
+
*,
|
|
160
|
+
groups: dict[str, list[str]] | None = None,
|
|
161
|
+
group_order: list[str] | None = None,
|
|
162
|
+
node_types: dict[str, str] | None = None,
|
|
163
|
+
theme: MermaidTheme = DEFAULT_THEME,
|
|
164
|
+
) -> str:
|
|
165
|
+
edge_list = list(edges)
|
|
166
|
+
id_map = _build_id_map(edge_list, _group_nodes(groups))
|
|
167
|
+
theme_vars: dict[str, object] = {}
|
|
168
|
+
if theme.edge_label_border:
|
|
169
|
+
theme_vars["edgeLabelBorderColor"] = theme.edge_label_border
|
|
170
|
+
if theme.edge_label_border_width:
|
|
171
|
+
theme_vars["edgeLabelBorderWidth"] = theme.edge_label_border_width
|
|
172
|
+
lines = []
|
|
173
|
+
if theme_vars:
|
|
174
|
+
lines.append(f'%%{{init: {{"themeVariables": {json.dumps(theme_vars)}}}}}%%')
|
|
175
|
+
lines.append(f"graph {direction}")
|
|
176
|
+
if groups:
|
|
177
|
+
_render_group_sections(lines, groups, group_order=group_order, id_map=id_map)
|
|
178
|
+
use_node_labels = not groups
|
|
179
|
+
poe_links, wireless_links = _render_edge_lines(
|
|
180
|
+
lines, edge_list, id_map=id_map, use_node_labels=use_node_labels
|
|
181
|
+
)
|
|
182
|
+
if node_types:
|
|
183
|
+
_render_node_classes(lines, node_types=node_types, id_map=id_map, theme=theme)
|
|
184
|
+
_render_link_styles(lines, poe_links=poe_links, wireless_links=wireless_links, theme=theme)
|
|
129
185
|
return "\n".join(lines) + "\n"
|
|
130
186
|
|
|
131
187
|
|
|
@@ -18,6 +18,9 @@ class MermaidTheme:
|
|
|
18
18
|
standard_link: str
|
|
19
19
|
standard_link_width: int
|
|
20
20
|
standard_link_arrow: str
|
|
21
|
+
node_text: str | None = None
|
|
22
|
+
edge_label_border: str | None = None
|
|
23
|
+
edge_label_border_width: int | None = None
|
|
21
24
|
|
|
22
25
|
|
|
23
26
|
DEFAULT_THEME = MermaidTheme(
|
|
@@ -32,15 +35,22 @@ DEFAULT_THEME = MermaidTheme(
|
|
|
32
35
|
standard_link="#2ecc71",
|
|
33
36
|
standard_link_width=2,
|
|
34
37
|
standard_link_arrow="none",
|
|
38
|
+
node_text=None,
|
|
39
|
+
edge_label_border=None,
|
|
40
|
+
edge_label_border_width=None,
|
|
35
41
|
)
|
|
36
42
|
|
|
37
43
|
|
|
38
44
|
def class_defs(theme: MermaidTheme = DEFAULT_THEME) -> list[str]:
|
|
45
|
+
def node_def(name: str, fill: str, stroke: str) -> str:
|
|
46
|
+
color = f",color:{theme.node_text}" if theme.node_text else ""
|
|
47
|
+
return f" classDef {name} fill:{fill},stroke:{stroke},stroke-width:1px{color};"
|
|
48
|
+
|
|
39
49
|
return [
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
node_def("node_gateway", theme.node_gateway[0], theme.node_gateway[1]),
|
|
51
|
+
node_def("node_switch", theme.node_switch[0], theme.node_switch[1]),
|
|
52
|
+
node_def("node_ap", theme.node_ap[0], theme.node_ap[1]),
|
|
53
|
+
node_def("node_client", theme.node_client[0], theme.node_client[1]),
|
|
54
|
+
node_def("node_other", theme.node_other[0], theme.node_other[1]),
|
|
45
55
|
" classDef node_legend font-size:10px;",
|
|
46
56
|
]
|