unifi-network-maps 1.4.3__py3-none-any.whl → 1.4.5__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.
Files changed (34) hide show
  1. unifi_network_maps/__init__.py +1 -1
  2. unifi_network_maps/assets/themes/dark.yaml +2 -2
  3. unifi_network_maps/cli/__init__.py +2 -38
  4. unifi_network_maps/cli/args.py +166 -0
  5. unifi_network_maps/cli/main.py +18 -747
  6. unifi_network_maps/cli/render.py +255 -0
  7. unifi_network_maps/cli/runtime.py +157 -0
  8. unifi_network_maps/io/mkdocs_assets.py +21 -0
  9. unifi_network_maps/io/mock_generate.py +2 -294
  10. unifi_network_maps/model/mock.py +307 -0
  11. unifi_network_maps/render/device_ports_md.py +44 -27
  12. unifi_network_maps/render/legend.py +30 -0
  13. unifi_network_maps/render/lldp_md.py +81 -60
  14. unifi_network_maps/render/markdown_tables.py +21 -0
  15. unifi_network_maps/render/mermaid.py +72 -85
  16. unifi_network_maps/render/mkdocs.py +167 -0
  17. unifi_network_maps/render/templates/device_port_block.md.j2 +5 -0
  18. unifi_network_maps/render/templates/legend_compact.html.j2 +14 -0
  19. unifi_network_maps/render/templates/lldp_device_section.md.j2 +15 -0
  20. unifi_network_maps/render/templates/markdown_section.md.j2 +3 -0
  21. unifi_network_maps/render/templates/mermaid_legend.mmd.j2 +30 -0
  22. unifi_network_maps/render/templates/mkdocs_document.md.j2 +23 -0
  23. unifi_network_maps/render/templates/mkdocs_dual_theme_style.html.j2 +8 -0
  24. unifi_network_maps/render/templates/mkdocs_html_block.html.j2 +2 -0
  25. unifi_network_maps/render/templates/mkdocs_legend.css.j2 +29 -0
  26. unifi_network_maps/render/templates/mkdocs_legend.js.j2 +18 -0
  27. unifi_network_maps/render/templates/mkdocs_mermaid_block.md.j2 +4 -0
  28. unifi_network_maps/render/templating.py +19 -0
  29. {unifi_network_maps-1.4.3.dist-info → unifi_network_maps-1.4.5.dist-info}/METADATA +2 -1
  30. {unifi_network_maps-1.4.3.dist-info → unifi_network_maps-1.4.5.dist-info}/RECORD +34 -14
  31. {unifi_network_maps-1.4.3.dist-info → unifi_network_maps-1.4.5.dist-info}/WHEEL +0 -0
  32. {unifi_network_maps-1.4.3.dist-info → unifi_network_maps-1.4.5.dist-info}/entry_points.txt +0 -0
  33. {unifi_network_maps-1.4.3.dist-info → unifi_network_maps-1.4.5.dist-info}/licenses/LICENSE +0 -0
  34. {unifi_network_maps-1.4.3.dist-info → unifi_network_maps-1.4.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,255 @@
1
+ """CLI rendering orchestration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import logging
7
+
8
+ from ..adapters.config import Config
9
+ from ..adapters.unifi import fetch_clients
10
+ from ..io.export import write_output
11
+ from ..io.mkdocs_assets import write_mkdocs_sidebar_assets
12
+ from ..model.topology import (
13
+ Device,
14
+ TopologyResult,
15
+ build_node_type_map,
16
+ build_port_map,
17
+ group_devices_by_type,
18
+ )
19
+ from ..render.legend import resolve_legend_style
20
+ from ..render.lldp_md import render_lldp_md
21
+ from ..render.mermaid import render_mermaid
22
+ from ..render.mermaid_theme import MermaidTheme
23
+ from ..render.mkdocs import MkdocsRenderOptions, render_mkdocs
24
+ from ..render.svg import SvgOptions, render_svg
25
+ from ..render.svg_theme import SvgTheme
26
+ from .runtime import (
27
+ build_edges_with_clients,
28
+ load_dark_mermaid_theme,
29
+ load_devices_data,
30
+ load_topology_for_render,
31
+ resolve_mkdocs_client_ports,
32
+ select_edges,
33
+ )
34
+
35
+
36
+ def render_mermaid_output(
37
+ args: argparse.Namespace,
38
+ devices: list[Device],
39
+ topology: TopologyResult,
40
+ config: Config | None,
41
+ site: str,
42
+ mermaid_theme: MermaidTheme,
43
+ *,
44
+ clients_override: list[object] | None = None,
45
+ ) -> str:
46
+ edges, _has_tree = select_edges(topology)
47
+ edges, clients = build_edges_with_clients(
48
+ args,
49
+ edges,
50
+ devices,
51
+ config,
52
+ site,
53
+ clients_override=clients_override,
54
+ )
55
+ groups = None
56
+ group_order = None
57
+ if args.group_by_type:
58
+ groups = group_devices_by_type(devices)
59
+ group_order = ["gateway", "switch", "ap", "other"]
60
+ content = render_mermaid(
61
+ edges,
62
+ direction=args.direction,
63
+ groups=groups,
64
+ group_order=group_order,
65
+ node_types=build_node_type_map(devices, clients, client_mode=args.client_scope),
66
+ theme=mermaid_theme,
67
+ )
68
+ if args.markdown:
69
+ content = f"""```mermaid
70
+ {content}```
71
+ """
72
+ return content
73
+
74
+
75
+ def render_svg_output(
76
+ args: argparse.Namespace,
77
+ devices: list[Device],
78
+ topology: TopologyResult,
79
+ config: Config | None,
80
+ site: str,
81
+ svg_theme: SvgTheme,
82
+ *,
83
+ clients_override: list[object] | None = None,
84
+ ) -> str:
85
+ edges, _has_tree = select_edges(topology)
86
+ edges, clients = build_edges_with_clients(
87
+ args,
88
+ edges,
89
+ devices,
90
+ config,
91
+ site,
92
+ clients_override=clients_override,
93
+ )
94
+ options = SvgOptions(width=args.svg_width, height=args.svg_height)
95
+ if args.format == "svg-iso":
96
+ from ..render.svg import render_svg_isometric
97
+
98
+ return render_svg_isometric(
99
+ edges,
100
+ node_types=build_node_type_map(devices, clients, client_mode=args.client_scope),
101
+ options=options,
102
+ theme=svg_theme,
103
+ )
104
+ return render_svg(
105
+ edges,
106
+ node_types=build_node_type_map(devices, clients, client_mode=args.client_scope),
107
+ options=options,
108
+ theme=svg_theme,
109
+ )
110
+
111
+
112
+ def render_mkdocs_format(
113
+ args: argparse.Namespace,
114
+ *,
115
+ devices: list[Device],
116
+ topology: TopologyResult,
117
+ config: Config | None,
118
+ site: str,
119
+ mermaid_theme: MermaidTheme,
120
+ mock_clients: list[object] | None,
121
+ ) -> str | None:
122
+ if args.mkdocs_sidebar_legend and not args.output:
123
+ logging.error("--mkdocs-sidebar-legend requires --output")
124
+ return None
125
+ if args.mkdocs_sidebar_legend:
126
+ write_mkdocs_sidebar_assets(args.output)
127
+ port_map = build_port_map(devices, only_unifi=args.only_unifi)
128
+ client_ports, error_code = resolve_mkdocs_client_ports(
129
+ args,
130
+ devices,
131
+ config,
132
+ site,
133
+ mock_clients,
134
+ )
135
+ if error_code is not None:
136
+ logging.error("Mock data required for client rendering")
137
+ return None
138
+ dark_mermaid_theme = load_dark_mermaid_theme() if args.mkdocs_dual_theme else None
139
+ edges, _has_tree = select_edges(topology)
140
+ options = MkdocsRenderOptions(
141
+ direction=args.direction,
142
+ legend_style=resolve_legend_style(
143
+ format_name=args.format,
144
+ legend_style=args.legend_style,
145
+ ),
146
+ legend_scale=args.legend_scale,
147
+ timestamp_zone=args.mkdocs_timestamp_zone,
148
+ client_scope=args.client_scope,
149
+ dual_theme=args.mkdocs_dual_theme,
150
+ )
151
+ return render_mkdocs(
152
+ edges,
153
+ devices,
154
+ mermaid_theme=mermaid_theme,
155
+ port_map=port_map,
156
+ client_ports=client_ports,
157
+ options=options,
158
+ dark_mermaid_theme=dark_mermaid_theme,
159
+ )
160
+
161
+
162
+ def render_lldp_format(
163
+ args: argparse.Namespace,
164
+ *,
165
+ config: Config | None,
166
+ site: str,
167
+ mock_devices: list[object] | None,
168
+ mock_clients: list[object] | None,
169
+ ) -> int:
170
+ try:
171
+ _raw_devices, devices = load_devices_data(
172
+ args,
173
+ config,
174
+ site,
175
+ raw_devices_override=mock_devices,
176
+ )
177
+ except Exception as exc:
178
+ logging.error("Failed to load devices: %s", exc)
179
+ return 1
180
+ if mock_clients is None:
181
+ if config is None:
182
+ logging.error("Mock data required for client rendering")
183
+ return 2
184
+ clients = list(fetch_clients(config, site=site))
185
+ else:
186
+ clients = mock_clients
187
+ content = render_lldp_md(
188
+ devices,
189
+ clients=clients,
190
+ include_ports=args.include_ports,
191
+ show_clients=args.include_clients,
192
+ client_mode=args.client_scope,
193
+ )
194
+ write_output(content, output_path=args.output, stdout=args.stdout)
195
+ return 0
196
+
197
+
198
+ def render_standard_format(
199
+ args: argparse.Namespace,
200
+ *,
201
+ config: Config | None,
202
+ site: str,
203
+ mock_devices: list[object] | None,
204
+ mock_clients: list[object] | None,
205
+ mermaid_theme: MermaidTheme,
206
+ svg_theme: SvgTheme,
207
+ ) -> int:
208
+ topology_result = load_topology_for_render(
209
+ args,
210
+ config=config,
211
+ site=site,
212
+ mock_devices=mock_devices,
213
+ )
214
+ if topology_result is None:
215
+ return 1
216
+ devices, topology = topology_result
217
+
218
+ if args.format == "mermaid":
219
+ content = render_mermaid_output(
220
+ args,
221
+ devices,
222
+ topology,
223
+ config,
224
+ site,
225
+ mermaid_theme,
226
+ clients_override=mock_clients,
227
+ )
228
+ elif args.format == "mkdocs":
229
+ content = render_mkdocs_format(
230
+ args,
231
+ devices=devices,
232
+ topology=topology,
233
+ config=config,
234
+ site=site,
235
+ mermaid_theme=mermaid_theme,
236
+ mock_clients=mock_clients,
237
+ )
238
+ if content is None:
239
+ return 2
240
+ elif args.format in {"svg", "svg-iso"}:
241
+ content = render_svg_output(
242
+ args,
243
+ devices,
244
+ topology,
245
+ config,
246
+ site,
247
+ svg_theme,
248
+ clients_override=mock_clients,
249
+ )
250
+ else:
251
+ logging.error("Unsupported format: %s", args.format)
252
+ return 2
253
+
254
+ write_output(content, output_path=args.output, stdout=args.stdout)
255
+ return 0
@@ -0,0 +1,157 @@
1
+ """Runtime data preparation for CLI rendering."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import logging
7
+ from pathlib import Path
8
+
9
+ from ..adapters.config import Config
10
+ from ..adapters.unifi import fetch_clients, fetch_devices
11
+ from ..io.debug import debug_dump_devices
12
+ from ..model.topology import (
13
+ ClientPortMap,
14
+ Device,
15
+ TopologyResult,
16
+ build_client_edges,
17
+ build_client_port_map,
18
+ build_device_index,
19
+ build_topology,
20
+ group_devices_by_type,
21
+ normalize_devices,
22
+ )
23
+ from ..render.mermaid_theme import MermaidTheme
24
+ from ..render.theme import load_theme
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ def load_devices_data(
30
+ args: argparse.Namespace,
31
+ config: Config | None,
32
+ site: str,
33
+ *,
34
+ raw_devices_override: list[object] | None = None,
35
+ ) -> tuple[list[object], list[Device]]:
36
+ if raw_devices_override is None:
37
+ if config is None:
38
+ raise ValueError("Config required to fetch devices")
39
+ raw_devices = list(
40
+ fetch_devices(config, site=site, detailed=True, use_cache=not args.no_cache)
41
+ )
42
+ else:
43
+ raw_devices = raw_devices_override
44
+ devices = normalize_devices(raw_devices)
45
+ if args.debug_dump:
46
+ debug_dump_devices(raw_devices, devices, sample_count=max(0, args.debug_sample))
47
+ return raw_devices, devices
48
+
49
+
50
+ def build_topology_data(
51
+ args: argparse.Namespace,
52
+ config: Config | None,
53
+ site: str,
54
+ *,
55
+ include_ports: bool | None = None,
56
+ raw_devices_override: list[object] | None = None,
57
+ ) -> tuple[list[Device], list[str], TopologyResult]:
58
+ _raw_devices, devices = load_devices_data(
59
+ args,
60
+ config,
61
+ site,
62
+ raw_devices_override=raw_devices_override,
63
+ )
64
+ groups_for_rank = group_devices_by_type(devices)
65
+ gateways = groups_for_rank.get("gateway", [])
66
+ topology = build_topology(
67
+ devices,
68
+ include_ports=include_ports if include_ports is not None else args.include_ports,
69
+ only_unifi=args.only_unifi,
70
+ gateways=gateways,
71
+ )
72
+ return devices, gateways, topology
73
+
74
+
75
+ def build_edges_with_clients(
76
+ args: argparse.Namespace,
77
+ edges: list,
78
+ devices: list[Device],
79
+ config: Config | None,
80
+ site: str,
81
+ *,
82
+ clients_override: list[object] | None = None,
83
+ ) -> tuple[list, list | None]:
84
+ clients = None
85
+ if args.include_clients:
86
+ if clients_override is None:
87
+ if config is None:
88
+ raise ValueError("Config required to fetch clients")
89
+ clients = list(fetch_clients(config, site=site, use_cache=not args.no_cache))
90
+ else:
91
+ clients = clients_override
92
+ device_index = build_device_index(devices)
93
+ edges = edges + build_client_edges(
94
+ clients,
95
+ device_index,
96
+ include_ports=args.include_ports,
97
+ client_mode=args.client_scope,
98
+ )
99
+ return edges, clients
100
+
101
+
102
+ def select_edges(topology: TopologyResult) -> tuple[list, bool]:
103
+ if topology.tree_edges:
104
+ return topology.tree_edges, True
105
+ logging.warning("No gateway found for hierarchy; rendering raw edges.")
106
+ return topology.raw_edges, False
107
+
108
+
109
+ def load_topology_for_render(
110
+ args: argparse.Namespace,
111
+ *,
112
+ config: Config | None,
113
+ site: str,
114
+ mock_devices: list[object] | None,
115
+ ) -> tuple[list[Device], TopologyResult] | None:
116
+ try:
117
+ include_ports = True if args.format == "mkdocs" else None
118
+ devices, _gateways, topology = build_topology_data(
119
+ args,
120
+ config,
121
+ site,
122
+ include_ports=include_ports,
123
+ raw_devices_override=mock_devices,
124
+ )
125
+ except Exception as exc:
126
+ logging.error("Failed to build topology: %s", exc)
127
+ return None
128
+ return devices, topology
129
+
130
+
131
+ def load_dark_mermaid_theme() -> MermaidTheme | None:
132
+ dark_theme_path = Path(__file__).resolve().parents[1] / "assets" / "themes" / "dark.yaml"
133
+ try:
134
+ dark_theme, _ = load_theme(dark_theme_path)
135
+ except Exception as exc: # noqa: BLE001
136
+ logger.warning("Failed to load dark theme: %s", exc)
137
+ return None
138
+ return dark_theme
139
+
140
+
141
+ def resolve_mkdocs_client_ports(
142
+ args: argparse.Namespace,
143
+ devices: list[Device],
144
+ config: Config | None,
145
+ site: str,
146
+ mock_clients: list[object] | None,
147
+ ) -> tuple[ClientPortMap | None, int | None]:
148
+ if not args.include_clients:
149
+ return None, None
150
+ if mock_clients is None:
151
+ if config is None:
152
+ return None, 2
153
+ clients = list(fetch_clients(config, site=site))
154
+ else:
155
+ clients = mock_clients
156
+ client_ports = build_client_port_map(devices, clients, client_mode=args.client_scope)
157
+ return client_ports, None
@@ -0,0 +1,21 @@
1
+ """MkDocs asset output helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from ..render.templating import render_template
8
+
9
+
10
+ def write_mkdocs_sidebar_assets(output_path: str) -> None:
11
+ output_dir = Path(output_path).resolve().parent
12
+ assets_dir = output_dir / "assets"
13
+ assets_dir.mkdir(parents=True, exist_ok=True)
14
+ (assets_dir / "legend.js").write_text(
15
+ render_template("mkdocs_legend.js.j2"),
16
+ encoding="utf-8",
17
+ )
18
+ (assets_dir / "legend.css").write_text(
19
+ render_template("mkdocs_legend.css.j2"),
20
+ encoding="utf-8",
21
+ )