unifi-network-maps 1.4.11__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 (99) hide show
  1. unifi_network_maps/__init__.py +1 -0
  2. unifi_network_maps/__main__.py +8 -0
  3. unifi_network_maps/adapters/__init__.py +1 -0
  4. unifi_network_maps/adapters/config.py +49 -0
  5. unifi_network_maps/adapters/unifi.py +457 -0
  6. unifi_network_maps/assets/__init__.py +0 -0
  7. unifi_network_maps/assets/icons/__init__.py +0 -0
  8. unifi_network_maps/assets/icons/access-point.svg +1 -0
  9. unifi_network_maps/assets/icons/isometric/ISOPACKS_LICENSE +7 -0
  10. unifi_network_maps/assets/icons/isometric/block.svg +23 -0
  11. unifi_network_maps/assets/icons/isometric/cache.svg +48 -0
  12. unifi_network_maps/assets/icons/isometric/cardterminal.svg +316 -0
  13. unifi_network_maps/assets/icons/isometric/cloud.svg +89 -0
  14. unifi_network_maps/assets/icons/isometric/cronjob.svg +409 -0
  15. unifi_network_maps/assets/icons/isometric/cube.svg +24 -0
  16. unifi_network_maps/assets/icons/isometric/desktop.svg +107 -0
  17. unifi_network_maps/assets/icons/isometric/diamond.svg +23 -0
  18. unifi_network_maps/assets/icons/isometric/dns.svg +46 -0
  19. unifi_network_maps/assets/icons/isometric/document.svg +62 -0
  20. unifi_network_maps/assets/icons/isometric/firewall.svg +200 -0
  21. unifi_network_maps/assets/icons/isometric/function-module.svg +215 -0
  22. unifi_network_maps/assets/icons/isometric/image.svg +65 -0
  23. unifi_network_maps/assets/icons/isometric/laptop.svg +37 -0
  24. unifi_network_maps/assets/icons/isometric/loadbalancer.svg +65 -0
  25. unifi_network_maps/assets/icons/isometric/lock.svg +155 -0
  26. unifi_network_maps/assets/icons/isometric/mail.svg +35 -0
  27. unifi_network_maps/assets/icons/isometric/mailmultiple.svg +91 -0
  28. unifi_network_maps/assets/icons/isometric/mobiledevice.svg +66 -0
  29. unifi_network_maps/assets/icons/isometric/office.svg +136 -0
  30. unifi_network_maps/assets/icons/isometric/package-module.svg +39 -0
  31. unifi_network_maps/assets/icons/isometric/paymentcard.svg +92 -0
  32. unifi_network_maps/assets/icons/isometric/plane.svg +1 -0
  33. unifi_network_maps/assets/icons/isometric/printer.svg +122 -0
  34. unifi_network_maps/assets/icons/isometric/pyramid.svg +28 -0
  35. unifi_network_maps/assets/icons/isometric/queue.svg +38 -0
  36. unifi_network_maps/assets/icons/isometric/router.svg +39 -0
  37. unifi_network_maps/assets/icons/isometric/server.svg +112 -0
  38. unifi_network_maps/assets/icons/isometric/speech.svg +70 -0
  39. unifi_network_maps/assets/icons/isometric/sphere.svg +15 -0
  40. unifi_network_maps/assets/icons/isometric/storage.svg +92 -0
  41. unifi_network_maps/assets/icons/isometric/switch-module.svg +45 -0
  42. unifi_network_maps/assets/icons/isometric/tower.svg +50 -0
  43. unifi_network_maps/assets/icons/isometric/truck-2.svg +1 -0
  44. unifi_network_maps/assets/icons/isometric/truck.svg +1 -0
  45. unifi_network_maps/assets/icons/isometric/user.svg +231 -0
  46. unifi_network_maps/assets/icons/isometric/vm.svg +50 -0
  47. unifi_network_maps/assets/icons/laptop.svg +1 -0
  48. unifi_network_maps/assets/icons/router-network.svg +1 -0
  49. unifi_network_maps/assets/icons/server-network.svg +1 -0
  50. unifi_network_maps/assets/icons/server.svg +1 -0
  51. unifi_network_maps/assets/themes/dark.yaml +50 -0
  52. unifi_network_maps/assets/themes/default.yaml +47 -0
  53. unifi_network_maps/cli/__init__.py +5 -0
  54. unifi_network_maps/cli/__main__.py +8 -0
  55. unifi_network_maps/cli/args.py +166 -0
  56. unifi_network_maps/cli/main.py +134 -0
  57. unifi_network_maps/cli/render.py +255 -0
  58. unifi_network_maps/cli/runtime.py +157 -0
  59. unifi_network_maps/io/__init__.py +1 -0
  60. unifi_network_maps/io/debug.py +60 -0
  61. unifi_network_maps/io/export.py +32 -0
  62. unifi_network_maps/io/mkdocs_assets.py +21 -0
  63. unifi_network_maps/io/mock_data.py +23 -0
  64. unifi_network_maps/io/mock_generate.py +7 -0
  65. unifi_network_maps/model/__init__.py +1 -0
  66. unifi_network_maps/model/labels.py +35 -0
  67. unifi_network_maps/model/lldp.py +99 -0
  68. unifi_network_maps/model/mock.py +307 -0
  69. unifi_network_maps/model/ports.py +23 -0
  70. unifi_network_maps/model/topology.py +909 -0
  71. unifi_network_maps/render/__init__.py +1 -0
  72. unifi_network_maps/render/device_ports_md.py +492 -0
  73. unifi_network_maps/render/legend.py +30 -0
  74. unifi_network_maps/render/lldp_md.py +352 -0
  75. unifi_network_maps/render/markdown_tables.py +21 -0
  76. unifi_network_maps/render/mermaid.py +273 -0
  77. unifi_network_maps/render/mermaid_theme.py +56 -0
  78. unifi_network_maps/render/mkdocs.py +167 -0
  79. unifi_network_maps/render/svg.py +1235 -0
  80. unifi_network_maps/render/svg_theme.py +64 -0
  81. unifi_network_maps/render/templates/device_port_block.md.j2 +5 -0
  82. unifi_network_maps/render/templates/legend_compact.html.j2 +14 -0
  83. unifi_network_maps/render/templates/lldp_device_section.md.j2 +15 -0
  84. unifi_network_maps/render/templates/markdown_section.md.j2 +3 -0
  85. unifi_network_maps/render/templates/mermaid_legend.mmd.j2 +30 -0
  86. unifi_network_maps/render/templates/mkdocs_document.md.j2 +23 -0
  87. unifi_network_maps/render/templates/mkdocs_dual_theme_style.html.j2 +8 -0
  88. unifi_network_maps/render/templates/mkdocs_html_block.html.j2 +2 -0
  89. unifi_network_maps/render/templates/mkdocs_legend.css.j2 +29 -0
  90. unifi_network_maps/render/templates/mkdocs_legend.js.j2 +18 -0
  91. unifi_network_maps/render/templates/mkdocs_mermaid_block.md.j2 +4 -0
  92. unifi_network_maps/render/templating.py +19 -0
  93. unifi_network_maps/render/theme.py +109 -0
  94. unifi_network_maps-1.4.11.dist-info/METADATA +290 -0
  95. unifi_network_maps-1.4.11.dist-info/RECORD +99 -0
  96. unifi_network_maps-1.4.11.dist-info/WHEEL +5 -0
  97. unifi_network_maps-1.4.11.dist-info/entry_points.txt +2 -0
  98. unifi_network_maps-1.4.11.dist-info/licenses/LICENSE +21 -0
  99. unifi_network_maps-1.4.11.dist-info/top_level.txt +1 -0
@@ -0,0 +1,307 @@
1
+ """Generate mock UniFi data using Faker."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import random
7
+ from dataclasses import dataclass, field
8
+ from typing import Any
9
+
10
+ from faker import Faker
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class MockOptions:
15
+ seed: int = 1337
16
+ switch_count: int = 1
17
+ ap_count: int = 2
18
+ wired_client_count: int = 2
19
+ wireless_client_count: int = 2
20
+
21
+
22
+ @dataclass
23
+ class _MockState:
24
+ fake: Faker
25
+ rng: random.Random
26
+ used_macs: set[str] = field(default_factory=set)
27
+ used_ips: set[str] = field(default_factory=set)
28
+ used_names: set[str] = field(default_factory=set)
29
+ used_rooms: set[str] = field(default_factory=set)
30
+ core_port_next: int = 2
31
+
32
+
33
+ def generate_mock_payload(options: MockOptions) -> dict[str, list[dict[str, Any]]]:
34
+ state = _build_state(options.seed)
35
+ devices, core_switch, aps = _build_devices(options, state)
36
+ clients = _build_clients(options, state, core_switch, aps)
37
+ return {"devices": devices, "clients": clients}
38
+
39
+
40
+ def mock_payload_json(options: MockOptions) -> str:
41
+ payload = generate_mock_payload(options)
42
+ return json.dumps(payload, indent=2, sort_keys=True)
43
+
44
+
45
+ def _build_state(seed: int) -> _MockState:
46
+ fake = Faker("en_US")
47
+ fake.seed_instance(seed)
48
+ rng = random.Random(seed)
49
+ return _MockState(fake=fake, rng=rng)
50
+
51
+
52
+ def _build_devices(
53
+ options: MockOptions, state: _MockState
54
+ ) -> tuple[list[dict[str, Any]], dict[str, Any], list[dict[str, Any]]]:
55
+ gateway = _build_gateway(state)
56
+ core_switch = _build_core_switch(state)
57
+ _link_gateway_to_switch(state, gateway, core_switch)
58
+ access_switches = _build_access_switches(options.switch_count - 1, state, core_switch)
59
+ aps = _build_aps(options.ap_count, state, core_switch)
60
+ devices = [gateway, core_switch] + access_switches + aps
61
+ return devices, core_switch, aps
62
+
63
+
64
+ def _build_gateway(state: _MockState) -> dict[str, Any]:
65
+ device = _device_base(state, "Cloud Gateway", "udm", "UniFi Dream Machine Pro", "UDM-Pro")
66
+ _add_port(device, 9, poe_enabled=False, rng=state.rng)
67
+ return device
68
+
69
+
70
+ def _build_core_switch(state: _MockState) -> dict[str, Any]:
71
+ return _device_base(state, "Core Switch", "usw", "UniFi Switch 24 PoE", "USW-24-PoE")
72
+
73
+
74
+ def _build_access_switches(
75
+ count: int, state: _MockState, core_switch: dict[str, Any]
76
+ ) -> list[dict[str, Any]]:
77
+ switches = []
78
+ for _ in range(max(0, count)):
79
+ room = _unique_room(state)
80
+ name = _unique_name(state, f"Switch {room}")
81
+ device = _device_base(state, name, "usw", "UniFi Switch Lite 8 PoE", "USW-Lite-8-PoE")
82
+ _link_core_device(state, core_switch, device, poe_enabled=False)
83
+ switches.append(device)
84
+ return switches
85
+
86
+
87
+ def _build_aps(count: int, state: _MockState, core_switch: dict[str, Any]) -> list[dict[str, Any]]:
88
+ aps = []
89
+ for _ in range(max(0, count)):
90
+ room = _unique_room(state)
91
+ name = _unique_name(state, f"AP {room}")
92
+ device = _device_base(state, name, "uap", "UniFi AP 6 Lite", "U6-Lite")
93
+ _add_port(device, 1, poe_enabled=True, rng=state.rng)
94
+ _link_core_device(state, core_switch, device, poe_enabled=True)
95
+ aps.append(device)
96
+ return aps
97
+
98
+
99
+ def _build_clients(
100
+ options: MockOptions,
101
+ state: _MockState,
102
+ core_switch: dict[str, Any],
103
+ aps: list[dict[str, Any]],
104
+ ) -> list[dict[str, Any]]:
105
+ clients = []
106
+ clients.extend(_build_wired_clients(options.wired_client_count, state, core_switch))
107
+ clients.extend(_build_wireless_clients(options.wireless_client_count, state, aps))
108
+ return clients
109
+
110
+
111
+ def _build_wired_clients(
112
+ count: int, state: _MockState, core_switch: dict[str, Any]
113
+ ) -> list[dict[str, Any]]:
114
+ clients = []
115
+ for _ in range(max(0, count)):
116
+ port_idx = _next_core_port(state)
117
+ _add_port(core_switch, port_idx, poe_enabled=False, rng=state.rng)
118
+ name = _unique_client_name(state)
119
+ clients.append(
120
+ {
121
+ "name": name,
122
+ "is_wired": True,
123
+ "sw_mac": core_switch["mac"],
124
+ "sw_port": port_idx,
125
+ }
126
+ )
127
+ return clients
128
+
129
+
130
+ def _build_wireless_clients(
131
+ count: int, state: _MockState, aps: list[dict[str, Any]]
132
+ ) -> list[dict[str, Any]]:
133
+ if not aps:
134
+ return []
135
+ clients = []
136
+ for idx in range(max(0, count)):
137
+ ap = aps[idx % len(aps)]
138
+ name = _unique_client_name(state)
139
+ clients.append(
140
+ {
141
+ "name": name,
142
+ "is_wired": False,
143
+ "ap_mac": ap["mac"],
144
+ "ap_port": 1,
145
+ }
146
+ )
147
+ return clients
148
+
149
+
150
+ def _device_base(
151
+ state: _MockState, name: str, dev_type: str, model_name: str, model: str
152
+ ) -> dict[str, Any]:
153
+ version = _pick_version(state, dev_type)
154
+ return {
155
+ "name": name,
156
+ "model_name": model_name,
157
+ "model": model,
158
+ "mac": _unique_mac(state),
159
+ "ip": _unique_ip(state),
160
+ "type": dev_type,
161
+ "version": version,
162
+ "port_table": [],
163
+ "lldp_info": [],
164
+ }
165
+
166
+
167
+ def _pick_version(state: _MockState, dev_type: str) -> str:
168
+ versions = {
169
+ "udm": ["3.1.0", "3.1.1"],
170
+ "usw": ["7.0.0", "7.1.2"],
171
+ "uap": ["6.6.55", "6.7.10"],
172
+ }
173
+ return state.rng.choice(versions.get(dev_type, ["1.0.0"]))
174
+
175
+
176
+ def _link_gateway_to_switch(
177
+ state: _MockState, gateway: dict[str, Any], core_switch: dict[str, Any]
178
+ ) -> None:
179
+ _add_port(core_switch, 1, poe_enabled=False, rng=state.rng)
180
+ _add_lldp_link(
181
+ gateway,
182
+ core_switch,
183
+ local_port=9,
184
+ remote_port=1,
185
+ remote_name=core_switch["name"],
186
+ )
187
+ _add_lldp_link(
188
+ core_switch,
189
+ gateway,
190
+ local_port=1,
191
+ remote_port=9,
192
+ remote_name=gateway["name"],
193
+ )
194
+
195
+
196
+ def _link_core_device(
197
+ state: _MockState,
198
+ core_switch: dict[str, Any],
199
+ device: dict[str, Any],
200
+ *,
201
+ poe_enabled: bool,
202
+ ) -> None:
203
+ port_idx = _next_core_port(state)
204
+ _add_port(core_switch, port_idx, poe_enabled=poe_enabled, rng=state.rng)
205
+ _add_lldp_link(
206
+ core_switch,
207
+ device,
208
+ local_port=port_idx,
209
+ remote_port=1,
210
+ remote_name=device["name"],
211
+ )
212
+ _add_lldp_link(
213
+ device,
214
+ core_switch,
215
+ local_port=1,
216
+ remote_port=port_idx,
217
+ remote_name=core_switch["name"],
218
+ )
219
+
220
+
221
+ def _add_lldp_link(
222
+ source: dict[str, Any],
223
+ target: dict[str, Any],
224
+ *,
225
+ local_port: int,
226
+ remote_port: int,
227
+ remote_name: str,
228
+ ) -> None:
229
+ source["lldp_info"].append(
230
+ {
231
+ "chassis_id": target["mac"],
232
+ "port_id": f"Port {remote_port}",
233
+ "port_desc": remote_name,
234
+ "local_port_idx": local_port,
235
+ "local_port_name": f"Port {local_port}",
236
+ }
237
+ )
238
+
239
+
240
+ def _add_port(
241
+ device: dict[str, Any],
242
+ port_idx: int,
243
+ *,
244
+ poe_enabled: bool,
245
+ rng: random.Random,
246
+ ) -> None:
247
+ device["port_table"].append(
248
+ {
249
+ "port_idx": port_idx,
250
+ "name": f"Port {port_idx}",
251
+ "ifname": f"eth{port_idx}",
252
+ "is_uplink": False,
253
+ "poe_enable": poe_enabled,
254
+ "port_poe": poe_enabled,
255
+ "poe_class": 4 if poe_enabled else 0,
256
+ "poe_power": round(rng.uniform(2.0, 6.0), 2) if poe_enabled else 0.0,
257
+ "poe_good": poe_enabled,
258
+ "poe_voltage": round(rng.uniform(44.0, 52.0), 1) if poe_enabled else 0.0,
259
+ "poe_current": round(rng.uniform(0.05, 0.12), 3) if poe_enabled else 0.0,
260
+ }
261
+ )
262
+
263
+
264
+ def _next_core_port(state: _MockState) -> int:
265
+ port_idx = state.core_port_next
266
+ state.core_port_next += 1
267
+ return port_idx
268
+
269
+
270
+ def _unique_name(state: _MockState, prefix: str) -> str:
271
+ name = prefix
272
+ while name in state.used_names:
273
+ name = f"{prefix} {state.rng.randint(2, 9)}"
274
+ state.used_names.add(name)
275
+ return name
276
+
277
+
278
+ def _unique_client_name(state: _MockState) -> str:
279
+ name = state.fake.first_name()
280
+ while name in state.used_names:
281
+ name = state.fake.first_name()
282
+ state.used_names.add(name)
283
+ return name
284
+
285
+
286
+ def _unique_room(state: _MockState) -> str:
287
+ room = state.fake.word().title()
288
+ while room in state.used_rooms:
289
+ room = state.fake.word().title()
290
+ state.used_rooms.add(room)
291
+ return room
292
+
293
+
294
+ def _unique_mac(state: _MockState) -> str:
295
+ mac = state.fake.mac_address()
296
+ while mac in state.used_macs:
297
+ mac = state.fake.mac_address()
298
+ state.used_macs.add(mac)
299
+ return mac
300
+
301
+
302
+ def _unique_ip(state: _MockState) -> str:
303
+ ip = state.fake.ipv4_private()
304
+ while ip in state.used_ips:
305
+ ip = state.fake.ipv4_private()
306
+ state.used_ips.add(ip)
307
+ return ip
@@ -0,0 +1,23 @@
1
+ """Port parsing helpers shared across modules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+
8
+ def extract_port_number(label: str | None) -> int | None:
9
+ if not label:
10
+ return None
11
+ # Matches: "Port 3", "eth1"; non-matches: "wan", "portX".
12
+ match = re.search(r"(?:^|[^0-9])(?:port|eth)\s*([0-9]+)", label.strip(), re.IGNORECASE)
13
+ if match:
14
+ return int(match.group(1))
15
+ return None
16
+
17
+
18
+ def normalize_port_label(label: str) -> str:
19
+ trimmed = label.strip()
20
+ number = extract_port_number(trimmed)
21
+ if number is not None:
22
+ return f"Port {number}"
23
+ return trimmed