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.
@@ -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
- return label.replace('"', '\\"')
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 render_mermaid(
58
- edges: Iterable[Edge],
59
- direction: str = "LR",
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
- groups: dict[str, list[str]] | None = None,
62
- group_order: list[str] | None = None,
63
- node_types: dict[str, str] | None = None,
64
- theme: MermaidTheme = DEFAULT_THEME,
65
- ) -> str:
66
- edge_list = list(edges)
67
- group_nodes: list[str] = []
68
- if groups:
69
- for members in groups.values():
70
- group_nodes.extend(members)
71
- id_map = _build_id_map(edge_list, group_nodes)
72
- lines = [f"graph {direction}"]
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
- link_index = 0
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(link_index)
111
+ poe_links.append(index)
103
112
  if edge.wireless:
104
- wireless_links.append(link_index)
105
- link_index += 1
106
- if node_types:
107
- class_map = {
108
- "gateway": "node_gateway",
109
- "switch": "node_switch",
110
- "ap": "node_ap",
111
- "client": "node_client",
112
- "other": "node_other",
113
- }
114
- if node_types:
115
- for name, node_type in node_types.items():
116
- class_name = class_map.get(node_type, "node_other")
117
- node_id = id_map.get(name)
118
- if node_id:
119
- lines.append(f" class {node_id} {class_name};")
120
- lines.extend(class_defs(theme))
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
- f" classDef node_gateway fill:{theme.node_gateway[0]},stroke:{theme.node_gateway[1]},stroke-width:1px;",
41
- f" classDef node_switch fill:{theme.node_switch[0]},stroke:{theme.node_switch[1]},stroke-width:1px;",
42
- f" classDef node_ap fill:{theme.node_ap[0]},stroke:{theme.node_ap[1]},stroke-width:1px;",
43
- f" classDef node_client fill:{theme.node_client[0]},stroke:{theme.node_client[1]},stroke-width:1px;",
44
- f" classDef node_other fill:{theme.node_other[0]},stroke:{theme.node_other[1]},stroke-width:1px;",
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
  ]