unifi-network-maps 1.4.13__py3-none-any.whl → 1.4.15__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.
@@ -1 +1 @@
1
- __version__ = "1.4.13"
1
+ __version__ = "1.4.15"
@@ -17,6 +17,7 @@ from pathlib import Path
17
17
  from typing import IO, TYPE_CHECKING
18
18
 
19
19
  from ..io.paths import resolve_cache_dir
20
+ from ..model.vlans import build_vlan_info, normalize_networks
20
21
  from .config import Config
21
22
 
22
23
  if TYPE_CHECKING:
@@ -170,6 +171,19 @@ def _serialize_devices_for_cache(devices: Sequence[object]) -> list[dict[str, ob
170
171
  return [_serialize_device_for_cache(device) for device in devices]
171
172
 
172
173
 
174
+ def _serialize_network_for_cache(network: object) -> dict[str, object]:
175
+ return {
176
+ "name": _first_attr(network, "name", "network_name", "networkName"),
177
+ "vlan": _first_attr(network, "vlan", "vlan_id", "vlanId", "vlanid"),
178
+ "vlan_enabled": _first_attr(network, "vlan_enabled", "vlanEnabled"),
179
+ "purpose": _first_attr(network, "purpose"),
180
+ }
181
+
182
+
183
+ def _serialize_networks_for_cache(networks: Sequence[object]) -> list[dict[str, object]]:
184
+ return [_serialize_network_for_cache(network) for network in networks]
185
+
186
+
173
187
  def _cache_lock_path(path: Path) -> Path:
174
188
  return path.with_suffix(path.suffix + ".lock")
175
189
 
@@ -465,3 +479,94 @@ def fetch_clients(
465
479
  _save_cache(cache_path, clients)
466
480
  logger.debug("Fetched %d clients", len(clients))
467
481
  return clients
482
+
483
+
484
+ def fetch_networks(
485
+ config: Config,
486
+ *,
487
+ site: str | None = None,
488
+ use_cache: bool = True,
489
+ ) -> Sequence[object]:
490
+ """Fetch network inventory from UniFi Controller."""
491
+ try:
492
+ from unifi_controller_api import UnifiAuthenticationError
493
+ except ImportError as exc:
494
+ raise RuntimeError("Missing dependency: unifi-controller-api") from exc
495
+
496
+ site_name = site or config.site
497
+ ttl_seconds = _cache_ttl_seconds()
498
+ cache_path = _cache_dir() / f"networks_{_cache_key(config.url, site_name)}.json"
499
+ if use_cache and _is_cache_dir_safe(cache_path.parent):
500
+ cached = _load_cache(cache_path, ttl_seconds)
501
+ stale_cached, cache_age = _load_cache_with_age(cache_path)
502
+ else:
503
+ cached = None
504
+ stale_cached, cache_age = None, None
505
+ if cached is not None:
506
+ logger.debug("Using cached networks (%d)", len(cached))
507
+ return cached
508
+
509
+ try:
510
+ controller = _init_controller(config, is_udm_pro=True)
511
+ except UnifiAuthenticationError:
512
+ logger.debug("UDM Pro authentication failed, retrying legacy auth")
513
+ controller = _init_controller(config, is_udm_pro=False)
514
+
515
+ def _fetch() -> Sequence[object]:
516
+ # Always use raw=True to avoid model parsing issues with disabled WAN interfaces
517
+ # (the UnifiNetworkConf model requires an 'enabled' field that may be absent)
518
+ return controller.get_unifi_site_networkconf(site_name=site_name, raw=True)
519
+
520
+ try:
521
+ networks = _call_with_retries("network fetch", _fetch)
522
+ except Exception as exc: # noqa: BLE001 - fallback to cache
523
+ if stale_cached is not None:
524
+ logger.warning(
525
+ "Network fetch failed; using stale cache (%ds old): %s",
526
+ int(cache_age or 0),
527
+ exc,
528
+ )
529
+ return stale_cached
530
+ raise
531
+ if use_cache:
532
+ _save_cache(cache_path, _serialize_networks_for_cache(networks))
533
+ logger.debug("Fetched %d networks", len(networks))
534
+ return networks
535
+
536
+
537
+ def fetch_payload(
538
+ config: Config,
539
+ *,
540
+ site: str | None = None,
541
+ include_clients: bool = True,
542
+ use_cache: bool = True,
543
+ ) -> dict[str, list[object] | list[dict[str, object]]]:
544
+ """Fetch devices, clients, and VLAN inventory for payload output."""
545
+ devices = list(fetch_devices(config, site=site, detailed=True, use_cache=use_cache))
546
+ clients = _fetch_payload_clients(
547
+ config,
548
+ site=site,
549
+ include_clients=include_clients,
550
+ use_cache=use_cache,
551
+ )
552
+ networks = list(fetch_networks(config, site=site, use_cache=use_cache))
553
+ normalized_networks = normalize_networks(networks)
554
+ vlan_info = build_vlan_info(clients, normalized_networks)
555
+ return {
556
+ "devices": devices,
557
+ "clients": clients,
558
+ "networks": normalized_networks,
559
+ "vlan_info": vlan_info,
560
+ }
561
+
562
+
563
+ def _fetch_payload_clients(
564
+ config: Config,
565
+ *,
566
+ site: str | None,
567
+ include_clients: bool,
568
+ use_cache: bool,
569
+ ) -> list[object]:
570
+ if not include_clients:
571
+ return []
572
+ return list(fetch_clients(config, site=site, use_cache=use_cache))
@@ -125,7 +125,7 @@ def add_general_render_args(parser: argparse._ArgumentGroup) -> None:
125
125
  parser.add_argument(
126
126
  "--format",
127
127
  default="mermaid",
128
- choices=["mermaid", "svg", "svg-iso", "lldp-md", "mkdocs"],
128
+ choices=["mermaid", "svg", "svg-iso", "lldp-md", "mkdocs", "json"],
129
129
  help="Output format",
130
130
  )
131
131
  parser.add_argument(
@@ -3,18 +3,21 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import argparse
6
+ import json
6
7
  import logging
7
8
  from pathlib import Path
8
9
 
9
10
  from ..adapters.config import Config
11
+ from ..adapters.unifi import fetch_payload
10
12
  from ..io.export import write_output
11
- from ..io.mock_data import load_mock_data
13
+ from ..io.mock_data import load_mock_data, load_mock_payload
12
14
  from ..io.paths import (
13
15
  resolve_env_file,
14
16
  resolve_mock_data_path,
15
17
  resolve_output_path,
16
18
  resolve_theme_path,
17
19
  )
20
+ from ..model.vlans import build_vlan_info, normalize_networks
18
21
  from ..render.legend import render_legend_only, resolve_legend_style
19
22
  from ..render.theme import resolve_themes
20
23
  from .args import build_parser
@@ -117,6 +120,38 @@ def _load_runtime_context(
117
120
  return config, site, None, None
118
121
 
119
122
 
123
+ def _handle_json_format(
124
+ args: argparse.Namespace,
125
+ *,
126
+ config: Config | None,
127
+ site: str,
128
+ ) -> int | None:
129
+ if args.format != "json":
130
+ return None
131
+ payload: dict[str, list[object] | list[dict[str, object]]]
132
+ if args.mock_data:
133
+ payload = load_mock_payload(args.mock_data)
134
+ if not args.include_clients:
135
+ payload["clients"] = []
136
+ networks = normalize_networks(payload.get("networks", []))
137
+ payload["networks"] = networks
138
+ payload["vlan_info"] = build_vlan_info(payload.get("clients", []), networks)
139
+ else:
140
+ if config is None:
141
+ logging.error("Config required to run")
142
+ return 2
143
+ payload = fetch_payload(
144
+ config,
145
+ site=site,
146
+ include_clients=args.include_clients,
147
+ use_cache=not args.no_cache,
148
+ )
149
+ content = json.dumps(payload, indent=2, sort_keys=True)
150
+ output_kwargs = {"format_name": args.format} if args.output else {}
151
+ write_output(content, output_path=args.output, stdout=args.stdout, **output_kwargs)
152
+ return 0
153
+
154
+
120
155
  def main(argv: list[str] | None = None) -> int:
121
156
  logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
122
157
  for handler in logging.getLogger().handlers:
@@ -132,6 +167,9 @@ def main(argv: list[str] | None = None) -> int:
132
167
  except ValueError as exc:
133
168
  logging.error(str(exc))
134
169
  return 2
170
+ payload_result = _handle_json_format(args, config=config, site=site)
171
+ if payload_result is not None:
172
+ return payload_result
135
173
  try:
136
174
  mermaid_theme, svg_theme = resolve_themes(args.theme_file)
137
175
  except Exception as exc: # noqa: BLE001
@@ -23,3 +23,20 @@ def load_mock_data(path: str) -> tuple[list[object], list[object]]:
23
23
  devices = _as_list(payload.get("devices"), "devices")
24
24
  clients = _as_list(payload.get("clients"), "clients")
25
25
  return devices, clients
26
+
27
+
28
+ def load_mock_payload(path: str) -> dict[str, list[object] | list[dict[str, object]]]:
29
+ resolved = resolve_mock_data_path(path)
30
+ payload = json.loads(resolved.read_text(encoding="utf-8"))
31
+ if not isinstance(payload, dict):
32
+ raise ValueError("Mock data must be a JSON object")
33
+ devices = _as_list(payload.get("devices"), "devices")
34
+ clients = _as_list(payload.get("clients"), "clients")
35
+ networks = _as_list(payload.get("networks"), "networks")
36
+ vlan_info = _as_list(payload.get("vlan_info"), "vlan_info")
37
+ return {
38
+ "devices": devices,
39
+ "clients": clients,
40
+ "networks": networks,
41
+ "vlan_info": vlan_info,
42
+ }
@@ -2,11 +2,14 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import logging
5
6
  import os
6
7
  import tempfile
7
8
  from collections.abc import Iterable
8
9
  from pathlib import Path
9
10
 
11
+ logger = logging.getLogger(__name__)
12
+
10
13
 
11
14
  def _safe_home_dir() -> Path | None:
12
15
  try:
@@ -22,8 +25,9 @@ def _base_roots() -> list[Path]:
22
25
  roots.append(home)
23
26
  try:
24
27
  roots.append(Path(tempfile.gettempdir()).resolve())
25
- except Exception:
26
- pass
28
+ except OSError as exc:
29
+ # Best-effort temp dir; resolution can fail in restricted environments.
30
+ logger.debug("Failed to resolve temp directory: %s", exc)
27
31
  return roots
28
32
 
29
33
 
@@ -176,7 +180,7 @@ def resolve_output_path(path: str | Path, *, format_name: str | None) -> Path:
176
180
  extensions: set[str] | None
177
181
  if format_name == "svg" or format_name == "svg-iso":
178
182
  extensions = {".svg"}
179
- elif format_name == "mock":
183
+ elif format_name in {"mock", "json"}:
180
184
  extensions = {".json"}
181
185
  elif format_name == "mermaid":
182
186
  extensions = {".md", ".mermaid", ".mmd"}
@@ -9,6 +9,8 @@ from typing import Any
9
9
 
10
10
  from faker import Faker
11
11
 
12
+ from .vlans import build_vlan_info, normalize_networks
13
+
12
14
 
13
15
  @dataclass(frozen=True)
14
16
  class MockOptions:
@@ -34,7 +36,14 @@ def generate_mock_payload(options: MockOptions) -> dict[str, list[dict[str, Any]
34
36
  state = _build_state(options.seed)
35
37
  devices, core_switch, aps = _build_devices(options, state)
36
38
  clients = _build_clients(options, state, core_switch, aps)
37
- return {"devices": devices, "clients": clients}
39
+ networks = _build_networks()
40
+ vlan_info = build_vlan_info(clients, networks)
41
+ return {
42
+ "devices": devices,
43
+ "clients": clients,
44
+ "networks": normalize_networks(networks),
45
+ "vlan_info": vlan_info,
46
+ }
38
47
 
39
48
 
40
49
  def mock_payload_json(options: MockOptions) -> str:
@@ -108,6 +117,13 @@ def _build_clients(
108
117
  return clients
109
118
 
110
119
 
120
+ def _build_networks() -> list[dict[str, Any]]:
121
+ return [
122
+ {"name": "LAN", "vlan_enabled": False, "purpose": "corporate"},
123
+ {"name": "Guest", "vlan": 20, "vlan_enabled": True, "purpose": "guest"},
124
+ ]
125
+
126
+
111
127
  def _build_wired_clients(
112
128
  count: int, state: _MockState, core_switch: dict[str, Any]
113
129
  ) -> list[dict[str, Any]]:
@@ -302,9 +302,29 @@ def _uplink_info(device: DeviceSource) -> tuple[UplinkInfo | None, UplinkInfo |
302
302
  return uplink, last_uplink
303
303
 
304
304
 
305
+ def _get_model_display_name(device: DeviceSource) -> str | None:
306
+ """Extract the human-readable model name from device data.
307
+
308
+ UniFi stores the friendly model name (e.g., 'USW Flex 2.5G 8 PoE') in various
309
+ fields depending on controller version. This function checks multiple candidates
310
+ and returns the first non-empty value found.
311
+ """
312
+ candidates = (
313
+ "model_in_lts",
314
+ "model_in_eol",
315
+ "shortname",
316
+ "model_name",
317
+ )
318
+ for key in candidates:
319
+ value = _get_attr(device, key)
320
+ if isinstance(value, str) and value.strip():
321
+ return value.strip()
322
+ return None
323
+
324
+
305
325
  def coerce_device(device: DeviceSource) -> Device:
306
326
  name = _get_attr(device, "name")
307
- model_name = _get_attr(device, "model_name") or _get_attr(device, "model")
327
+ model_name = _get_model_display_name(device) or _get_attr(device, "model")
308
328
  model = _get_attr(device, "model")
309
329
  mac = _get_attr(device, "mac")
310
330
  ip = _get_attr(device, "ip") or _get_attr(device, "ip_address")
@@ -0,0 +1,119 @@
1
+ """VLAN inventory helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+
7
+
8
+ def _as_list(value: object | None) -> list[object]:
9
+ if value is None:
10
+ return []
11
+ if isinstance(value, list):
12
+ return value
13
+ if isinstance(value, dict):
14
+ return [value]
15
+ if isinstance(value, str | bytes):
16
+ return []
17
+ if isinstance(value, Iterable):
18
+ return list(value)
19
+ return []
20
+
21
+
22
+ def _get_attr(obj: object, name: str) -> object | None:
23
+ if isinstance(obj, dict):
24
+ return obj.get(name)
25
+ return getattr(obj, name, None)
26
+
27
+
28
+ def _first_attr(obj: object, *names: str) -> object | None:
29
+ for name in names:
30
+ value = _get_attr(obj, name)
31
+ if value is not None:
32
+ return value
33
+ return None
34
+
35
+
36
+ def _as_bool(value: object | None) -> bool:
37
+ if isinstance(value, bool):
38
+ return value
39
+ if isinstance(value, int | float):
40
+ return value != 0
41
+ if isinstance(value, str):
42
+ return value.strip().lower() in {"1", "true", "yes", "y", "on"}
43
+ return False
44
+
45
+
46
+ def _as_vlan_id(value: object | None) -> int | None:
47
+ if isinstance(value, int):
48
+ return value if value > 0 else None
49
+ if isinstance(value, str):
50
+ return int(value) if value.isdigit() and int(value) > 0 else None
51
+ return None
52
+
53
+
54
+ def _network_vlan_id(network: object) -> int | None:
55
+ vlan_value = _first_attr(network, "vlan", "vlan_id", "vlanId", "vlanid")
56
+ vlan_enabled = _as_bool(_first_attr(network, "vlan_enabled", "vlanEnabled"))
57
+ vlan_id = _as_vlan_id(vlan_value)
58
+ if vlan_id is not None:
59
+ return vlan_id
60
+ if not vlan_enabled:
61
+ return 1
62
+ return None
63
+
64
+
65
+ def normalize_networks(networks: Iterable[object]) -> list[dict[str, object]]:
66
+ normalized: list[dict[str, object]] = []
67
+ for network in _as_list(networks):
68
+ if network is None:
69
+ continue
70
+ normalized.append(
71
+ {
72
+ "network_id": _first_attr(network, "_id", "id", "network_id", "networkId"),
73
+ "name": _first_attr(network, "name", "network_name", "networkName"),
74
+ "vlan_id": _network_vlan_id(network),
75
+ "vlan_enabled": _as_bool(_first_attr(network, "vlan_enabled", "vlanEnabled")),
76
+ "purpose": _first_attr(network, "purpose"),
77
+ }
78
+ )
79
+ return normalized
80
+
81
+
82
+ def build_vlan_info(
83
+ clients: Iterable[object], networks: Iterable[object]
84
+ ) -> list[dict[str, object]]:
85
+ vlan_counts = _client_vlan_counts(clients)
86
+ vlan_entries = _network_vlan_entries(networks)
87
+ for vlan_id, count in vlan_counts.items():
88
+ entry = vlan_entries.setdefault(
89
+ vlan_id,
90
+ {"id": vlan_id, "name": None, "client_count": 0},
91
+ )
92
+ entry["client_count"] = count
93
+ return [vlan_entries[key] for key in sorted(vlan_entries)]
94
+
95
+
96
+ def _client_vlan_counts(clients: Iterable[object]) -> dict[int, int]:
97
+ vlan_counts: dict[int, int] = {}
98
+ for client in _as_list(clients):
99
+ vlan_id = _as_vlan_id(_first_attr(client, "vlan", "vlan_id", "vlanId", "vlanid"))
100
+ if vlan_id is None:
101
+ continue
102
+ vlan_counts[vlan_id] = vlan_counts.get(vlan_id, 0) + 1
103
+ return vlan_counts
104
+
105
+
106
+ def _network_vlan_entries(networks: Iterable[object]) -> dict[int, dict[str, object]]:
107
+ vlan_entries: dict[int, dict[str, object]] = {}
108
+ for network in normalize_networks(networks):
109
+ vlan_id = network.get("vlan_id")
110
+ if not isinstance(vlan_id, int):
111
+ continue
112
+ entry = vlan_entries.setdefault(
113
+ vlan_id,
114
+ {"id": vlan_id, "name": None, "client_count": 0},
115
+ )
116
+ name = network.get("name")
117
+ if name and not entry["name"]:
118
+ entry["name"] = name
119
+ return vlan_entries
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unifi-network-maps
3
- Version: 1.4.13
3
+ Version: 1.4.15
4
4
  Summary: Dynamic UniFi -> network maps in mermaid or svg
5
5
  Author: Merlijn
6
6
  License-Expression: MIT
@@ -147,6 +147,12 @@ Legend only:
147
147
  unifi-network-maps --legend-only --stdout
148
148
  ```
149
149
 
150
+ JSON payload (devices + clients + VLAN inventory):
151
+
152
+ ```bash
153
+ unifi-network-maps --format json --output ./payload.json
154
+ ```
155
+
150
156
  ## Home Assistant integration
151
157
 
152
158
  The live Home Assistant integration (Config Flow + coordinator + custom card) lives in a separate repo:
@@ -238,7 +244,7 @@ SVG:
238
244
  - `--theme-file`: load a YAML theme for Mermaid + SVG colors (see `examples/theme.yaml` and `examples/theme-dark.yaml`).
239
245
 
240
246
  Output:
241
- - `--format mermaid|svg|svg-iso|lldp-md|mkdocs`: output format (default mermaid).
247
+ - `--format mermaid|svg|svg-iso|lldp-md|mkdocs|json`: output format (default mermaid).
242
248
  - `--stdout`: write output to stdout.
243
249
  - `--markdown`: wrap Mermaid output in a code fence.
244
250
  - `--mkdocs-sidebar-legend`: write assets to place the compact legend in the MkDocs right sidebar.
@@ -1,8 +1,8 @@
1
- unifi_network_maps/__init__.py,sha256=3Jd0G1BzDzSEXU_O-VQeqAYwxMAmK7PBiIm9SYiPxoE,23
1
+ unifi_network_maps/__init__.py,sha256=hbkjjRMZQ6Z1DxtYr5zQQkJBgg7BX1jvecmMzfI1XL8,23
2
2
  unifi_network_maps/__main__.py,sha256=XsOjaqslAVgyVlOTokjVddZ2iT8apZXpJ_OB-9WEEe4,179
3
3
  unifi_network_maps/adapters/__init__.py,sha256=nzx1KsiYalL_YuXKE6acW8Dj5flmMg0-i4gyYO0gV54,22
4
4
  unifi_network_maps/adapters/config.py,sha256=gnvgAj9wPmYGGQfhcZSHRkcVGojODUYIINU_ztTcuUc,1671
5
- unifi_network_maps/adapters/unifi.py,sha256=MCW-kpLcmAF_9QZviT9T2oPfDI9hurtxALCBY8tbmfk,15601
5
+ unifi_network_maps/adapters/unifi.py,sha256=1a7NBLszpBhLnepp9WhQ5cF-HRToIG0foOA-H5xUiDI,19328
6
6
  unifi_network_maps/assets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  unifi_network_maps/assets/icons/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  unifi_network_maps/assets/icons/access-point.svg,sha256=RJOgO2s9Ino5lWRrh4V7q8Jdwffros5bQq3UeDuQYF4,742
@@ -52,23 +52,24 @@ unifi_network_maps/assets/themes/dark.yaml,sha256=6n4_H6MZbA6DKLdF2eaNcCXYLKNJjV
52
52
  unifi_network_maps/assets/themes/default.yaml,sha256=F2Jj18NmdaJ_zyERvGAn8NEWBwapjtozrtZUxayd5AU,849
53
53
  unifi_network_maps/cli/__init__.py,sha256=cds9GvFNZmYAR22Ab3TSzfriSAW--kf9jvC5U-21AoA,70
54
54
  unifi_network_maps/cli/__main__.py,sha256=nK_jh78VW3h3DRvSpjzpcf64zkCqniP2k82xUR9Hw2I,147
55
- unifi_network_maps/cli/args.py,sha256=lIgQDeob_SIhjXg76hJsnpgNOKupSjSYum_MqarWOkE,5552
56
- unifi_network_maps/cli/main.py,sha256=Fedf93kc6f9w_V_Ik28gnrUGlB7oD0Yn5cdmHnj9nI4,5719
55
+ unifi_network_maps/cli/args.py,sha256=ro3x8Yn77j7gcmJIyLDmpVsmYiYrXrzkF2LZ6QUQmHA,5560
56
+ unifi_network_maps/cli/main.py,sha256=W-iScP6x4zGRTrc3v_3bTgxkeyOCOBiRfqIjJqRiq3o,7088
57
57
  unifi_network_maps/cli/render.py,sha256=wuvsC-In-ApbaQxBn8-UofB7jCiqUnjluGXRCRZpg5A,7535
58
58
  unifi_network_maps/cli/runtime.py,sha256=cMtIShERup2z2_uCcEGcaJFJptvdI0L3FDWqKFwSevY,4830
59
59
  unifi_network_maps/io/__init__.py,sha256=nzx1KsiYalL_YuXKE6acW8Dj5flmMg0-i4gyYO0gV54,22
60
60
  unifi_network_maps/io/debug.py,sha256=eUVs6GLfs6VqI6ma8ra7NLhC3q4ek2K8OSRLnpQDF9s,1745
61
61
  unifi_network_maps/io/export.py,sha256=1_RcXpPqoerCt7WsiheV4dwKgY2fFJlpjUGRs-EkHE4,1013
62
62
  unifi_network_maps/io/mkdocs_assets.py,sha256=pszCivIql3maaLw7KFcAioQaVcyLltXgg-fPXPBvvEk,716
63
- unifi_network_maps/io/mock_data.py,sha256=3nbg83APrdhmNW2w0Ha3FWoguMNfJy1IMtolinUPu0E,774
63
+ unifi_network_maps/io/mock_data.py,sha256=D5OT0iYipuGKT-gHGx6yNGujOcOsxjmQDCfSzxmJzkI,1446
64
64
  unifi_network_maps/io/mock_generate.py,sha256=xiJz_qtNW7iVj7dezLNyVXAHtCHAi8U5CSXK5mrJE4U,233
65
- unifi_network_maps/io/paths.py,sha256=zE_kYkRXgHmPaRgzE45angYQk59aMC84JW7kcH11Qk0,5729
65
+ unifi_network_maps/io/paths.py,sha256=AgrF16EkFc63NNDNRzSnxwv1nIjq24zqNgKL7Y6VOVE,5930
66
66
  unifi_network_maps/model/__init__.py,sha256=nzx1KsiYalL_YuXKE6acW8Dj5flmMg0-i4gyYO0gV54,22
67
67
  unifi_network_maps/model/labels.py,sha256=m_k8mbzWtOSDOjjHhLUqwIw93pg98HAtGtHkiERXmek,1135
68
68
  unifi_network_maps/model/lldp.py,sha256=SrPW5XC2lfJgaGeVx-KnSFNltyok7gIWWQNg1SkOaj4,3300
69
- unifi_network_maps/model/mock.py,sha256=kd1MSiIn-7KKu_nMVmheYPfTyAN5DHt4dzRrBifF_lI,8706
69
+ unifi_network_maps/model/mock.py,sha256=kkzt7BWt5Q8dZ2wgfgK3HLmVdHHmOjAgS_snJsWawAo,9172
70
70
  unifi_network_maps/model/ports.py,sha256=o3NBlXcC5VV5iPWJsO4Ll1mRKJZC0f8zTHdlkkE34GU,609
71
- unifi_network_maps/model/topology.py,sha256=rC1J1h70JIpkm6eIuFz7AjoNtKobPpVfAbiWgMyow2w,31475
71
+ unifi_network_maps/model/topology.py,sha256=bkKSy1WlZMMkzya3bTbpYLrseXpwjhOPPoGOf4FHInM,32114
72
+ unifi_network_maps/model/vlans.py,sha256=EeRKvIuUs0FgEzrqbo2xi8Xlm5n5V_Z6b7jpXLiNUaY,3788
72
73
  unifi_network_maps/render/__init__.py,sha256=nzx1KsiYalL_YuXKE6acW8Dj5flmMg0-i4gyYO0gV54,22
73
74
  unifi_network_maps/render/device_ports_md.py,sha256=PLbP4_EDwLNkUACVJB_uM0ywaQTbEc6OWMv4QBcoJbU,16425
74
75
  unifi_network_maps/render/legend.py,sha256=TmZsxgCVOM2CZImI9zgRVyzrcg01HZFDj9F715d4CGo,772
@@ -92,9 +93,9 @@ unifi_network_maps/render/templates/mkdocs_html_block.html.j2,sha256=5l5-BbNujOc
92
93
  unifi_network_maps/render/templates/mkdocs_legend.css.j2,sha256=tkTI-RagBSgdjUygVenlTsQFenU09ePbXOfDt_Q7YRM,612
93
94
  unifi_network_maps/render/templates/mkdocs_legend.js.j2,sha256=qMYyCKsJ84uXf1wGgzbc7Bc49RU4oyuaGK9KrgQDQEI,685
94
95
  unifi_network_maps/render/templates/mkdocs_mermaid_block.md.j2,sha256=9IncllWQpoI8BN3A7b2zOQ5cksj97ddsjHJ-aBhpw7o,66
95
- unifi_network_maps-1.4.13.dist-info/licenses/LICENSE,sha256=mYo1siIIfIwyfdOuK2-Zt0ij2xBTii2hnpeTu79nD80,1074
96
- unifi_network_maps-1.4.13.dist-info/METADATA,sha256=HzxFdG6F9RwTXSAqF7o-Zl3a3a1dofXewvXZGYtKXlM,9952
97
- unifi_network_maps-1.4.13.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
98
- unifi_network_maps-1.4.13.dist-info/entry_points.txt,sha256=cdJ7jsBgNgHxSflYUOqgz5BbvuM0Nnh-x8_Hbyh_LFg,67
99
- unifi_network_maps-1.4.13.dist-info/top_level.txt,sha256=G0rUX1PNfVCn1u-KtB6QjFQHopCOVLnPMczvPOoraHg,19
100
- unifi_network_maps-1.4.13.dist-info/RECORD,,
96
+ unifi_network_maps-1.4.15.dist-info/licenses/LICENSE,sha256=mYo1siIIfIwyfdOuK2-Zt0ij2xBTii2hnpeTu79nD80,1074
97
+ unifi_network_maps-1.4.15.dist-info/METADATA,sha256=Mc5M1u-ACVLVZm6xX-Giv4KhP7r7wZ5V1jfvBoKriYY,10079
98
+ unifi_network_maps-1.4.15.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
99
+ unifi_network_maps-1.4.15.dist-info/entry_points.txt,sha256=cdJ7jsBgNgHxSflYUOqgz5BbvuM0Nnh-x8_Hbyh_LFg,67
100
+ unifi_network_maps-1.4.15.dist-info/top_level.txt,sha256=G0rUX1PNfVCn1u-KtB6QjFQHopCOVLnPMczvPOoraHg,19
101
+ unifi_network_maps-1.4.15.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.10.1)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5