unifi-network-maps 1.4.4__py3-none-any.whl → 1.4.6__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/cli/__init__.py +2 -38
- unifi_network_maps/cli/args.py +166 -0
- unifi_network_maps/cli/main.py +23 -753
- unifi_network_maps/cli/render.py +255 -0
- unifi_network_maps/cli/runtime.py +157 -0
- unifi_network_maps/io/mkdocs_assets.py +21 -0
- unifi_network_maps/io/mock_generate.py +2 -294
- unifi_network_maps/model/mock.py +307 -0
- unifi_network_maps/render/device_ports_md.py +44 -27
- unifi_network_maps/render/legend.py +30 -0
- unifi_network_maps/render/lldp_md.py +81 -60
- unifi_network_maps/render/markdown_tables.py +21 -0
- unifi_network_maps/render/mermaid.py +72 -85
- unifi_network_maps/render/mkdocs.py +167 -0
- unifi_network_maps/render/svg.py +34 -3
- unifi_network_maps/render/templates/device_port_block.md.j2 +5 -0
- unifi_network_maps/render/templates/legend_compact.html.j2 +14 -0
- unifi_network_maps/render/templates/lldp_device_section.md.j2 +15 -0
- unifi_network_maps/render/templates/markdown_section.md.j2 +3 -0
- unifi_network_maps/render/templates/mermaid_legend.mmd.j2 +30 -0
- unifi_network_maps/render/templates/mkdocs_document.md.j2 +23 -0
- unifi_network_maps/render/templates/mkdocs_dual_theme_style.html.j2 +8 -0
- unifi_network_maps/render/templates/mkdocs_html_block.html.j2 +2 -0
- unifi_network_maps/render/templates/mkdocs_legend.css.j2 +29 -0
- unifi_network_maps/render/templates/mkdocs_legend.js.j2 +18 -0
- unifi_network_maps/render/templates/mkdocs_mermaid_block.md.j2 +4 -0
- unifi_network_maps/render/templating.py +19 -0
- {unifi_network_maps-1.4.4.dist-info → unifi_network_maps-1.4.6.dist-info}/METADATA +7 -1
- {unifi_network_maps-1.4.4.dist-info → unifi_network_maps-1.4.6.dist-info}/RECORD +34 -14
- {unifi_network_maps-1.4.4.dist-info → unifi_network_maps-1.4.6.dist-info}/WHEEL +0 -0
- {unifi_network_maps-1.4.4.dist-info → unifi_network_maps-1.4.6.dist-info}/entry_points.txt +0 -0
- {unifi_network_maps-1.4.4.dist-info → unifi_network_maps-1.4.6.dist-info}/licenses/LICENSE +0 -0
- {unifi_network_maps-1.4.4.dist-info → unifi_network_maps-1.4.6.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
|
+
)
|