unifi-network-maps 1.4.12__tar.gz → 1.4.14__tar.gz
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-1.4.12 → unifi_network_maps-1.4.14}/CHANGELOG.md +19 -1
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/PKG-INFO +9 -3
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/README.md +7 -1
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/pyproject.toml +3 -3
- unifi_network_maps-1.4.14/src/unifi_network_maps/__init__.py +1 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/adapters/config.py +6 -2
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/adapters/unifi.py +124 -7
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/cli/args.py +1 -1
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/cli/main.py +85 -4
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/cli/render.py +4 -2
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/io/export.py +11 -2
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/io/mkdocs_assets.py +4 -2
- unifi_network_maps-1.4.14/src/unifi_network_maps/io/mock_data.py +42 -0
- unifi_network_maps-1.4.14/src/unifi_network_maps/io/paths.py +201 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/model/mock.py +17 -1
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/model/topology.py +3 -3
- unifi_network_maps-1.4.14/src/unifi_network_maps/model/vlans.py +119 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/device_ports_md.py +27 -18
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/lldp_md.py +4 -1
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/theme.py +2 -1
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps.egg-info/PKG-INFO +9 -3
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps.egg-info/SOURCES.txt +5 -1
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps.egg-info/requires.txt +1 -1
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/tests/test_cli.py +86 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/tests/test_device_ports_md.py +36 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/tests/test_lldp_md.py +112 -1
- unifi_network_maps-1.4.14/tests/test_mkdocs.py +113 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/tests/test_mock_generate.py +2 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/tests/test_topology.py +130 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/tests/test_unifi.py +102 -0
- unifi_network_maps-1.4.14/tests/test_vlan_info.py +30 -0
- unifi_network_maps-1.4.12/src/unifi_network_maps/__init__.py +0 -1
- unifi_network_maps-1.4.12/src/unifi_network_maps/io/mock_data.py +0 -23
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/CONTRIBUTING.md +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/LICENSE +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/LICENSES.md +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/MANIFEST.in +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/RELEASING.md +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/SECURITY.md +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/setup.cfg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/__main__.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/adapters/__init__.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/__init__.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/__init__.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/access-point.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/ISOPACKS_LICENSE +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/block.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/cache.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/cardterminal.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/cloud.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/cronjob.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/cube.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/desktop.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/diamond.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/dns.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/document.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/firewall.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/function-module.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/image.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/laptop.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/loadbalancer.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/lock.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/mail.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/mailmultiple.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/mobiledevice.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/office.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/package-module.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/paymentcard.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/plane.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/printer.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/pyramid.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/queue.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/router.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/server.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/speech.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/sphere.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/storage.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/switch-module.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/tower.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/truck-2.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/truck.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/user.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/isometric/vm.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/laptop.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/router-network.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/server-network.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/icons/server.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/themes/dark.yaml +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/assets/themes/default.yaml +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/cli/__init__.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/cli/__main__.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/cli/runtime.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/io/__init__.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/io/debug.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/io/mock_generate.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/model/__init__.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/model/labels.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/model/lldp.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/model/ports.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/__init__.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/legend.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/markdown_tables.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/mermaid.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/mermaid_theme.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/mkdocs.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/svg.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/svg_theme.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/templates/device_port_block.md.j2 +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/templates/legend_compact.html.j2 +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/templates/lldp_device_section.md.j2 +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/templates/markdown_section.md.j2 +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/templates/mermaid_legend.mmd.j2 +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/templates/mkdocs_document.md.j2 +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/templates/mkdocs_dual_theme_style.html.j2 +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/templates/mkdocs_html_block.html.j2 +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/templates/mkdocs_legend.css.j2 +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/templates/mkdocs_legend.js.j2 +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/templates/mkdocs_mermaid_block.md.j2 +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/render/templating.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps.egg-info/dependency_links.txt +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps.egg-info/entry_points.txt +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps.egg-info/top_level.txt +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/tests/test_clients.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/tests/test_config.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/tests/test_contract_unifi.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/tests/test_contract_unifi_live.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/tests/test_debug.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/tests/test_export.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/tests/test_groups.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/tests/test_labels.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/tests/test_lldp.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/tests/test_mermaid.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/tests/test_svg.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/tests/test_svg_iso.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/tests/test_theme.py +0 -0
|
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.4.14] - 2026-02-01
|
|
9
|
+
### Added
|
|
10
|
+
- JSON output with VLAN inventory
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Added log message when /tmp can't be resolved
|
|
14
|
+
|
|
15
|
+
## [1.4.13] - 2026-01-25
|
|
16
|
+
### Fixed
|
|
17
|
+
- Path Traversal Vulnerability in File Operations
|
|
18
|
+
- Cache Directory Symlink Attack vector
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- Improved escaping in Markdown Output
|
|
22
|
+
- Made logging less chatty, moved messages to debug level
|
|
23
|
+
|
|
8
24
|
## [1.4.12] - 2026-01-21
|
|
9
25
|
### Added
|
|
10
26
|
- Filter UniFi clients with --only-unifi, and not only neighbors
|
|
@@ -183,7 +199,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
183
199
|
- Introduced SVG renderer and tree layout fixes.
|
|
184
200
|
- Increased test coverage and added coverage tooling.
|
|
185
201
|
|
|
186
|
-
[Unreleased]: https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.
|
|
202
|
+
[Unreleased]: https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.14...HEAD
|
|
203
|
+
[1.4.14]:https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.11...v1.4.14
|
|
204
|
+
[1.4.13]:https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.11...v1.4.13
|
|
187
205
|
[1.4.12]:https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.11...v1.4.12
|
|
188
206
|
[1.4.11]:https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.10...v1.4.11
|
|
189
207
|
[1.4.10]:https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.9...v1.4.10
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: unifi-network-maps
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.14
|
|
4
4
|
Summary: Dynamic UniFi -> network maps in mermaid or svg
|
|
5
5
|
Author: Merlijn
|
|
6
6
|
License-Expression: MIT
|
|
@@ -32,7 +32,7 @@ Requires-Dist: pre-commit==4.5.1; extra == "dev"
|
|
|
32
32
|
Requires-Dist: pytest==9.0.2; extra == "dev"
|
|
33
33
|
Requires-Dist: pytest-cov==7.0.0; extra == "dev"
|
|
34
34
|
Requires-Dist: pyright==1.1.408; extra == "dev"
|
|
35
|
-
Requires-Dist: ruff==0.14.
|
|
35
|
+
Requires-Dist: ruff==0.14.14; extra == "dev"
|
|
36
36
|
Dynamic: license-file
|
|
37
37
|
|
|
38
38
|
# unifi-network-maps
|
|
@@ -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.
|
|
@@ -110,6 +110,12 @@ Legend only:
|
|
|
110
110
|
unifi-network-maps --legend-only --stdout
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
+
JSON payload (devices + clients + VLAN inventory):
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
unifi-network-maps --format json --output ./payload.json
|
|
117
|
+
```
|
|
118
|
+
|
|
113
119
|
## Home Assistant integration
|
|
114
120
|
|
|
115
121
|
The live Home Assistant integration (Config Flow + coordinator + custom card) lives in a separate repo:
|
|
@@ -201,7 +207,7 @@ SVG:
|
|
|
201
207
|
- `--theme-file`: load a YAML theme for Mermaid + SVG colors (see `examples/theme.yaml` and `examples/theme-dark.yaml`).
|
|
202
208
|
|
|
203
209
|
Output:
|
|
204
|
-
- `--format mermaid|svg|svg-iso|lldp-md|mkdocs`: output format (default mermaid).
|
|
210
|
+
- `--format mermaid|svg|svg-iso|lldp-md|mkdocs|json`: output format (default mermaid).
|
|
205
211
|
- `--stdout`: write output to stdout.
|
|
206
212
|
- `--markdown`: wrap Mermaid output in a code fence.
|
|
207
213
|
- `--mkdocs-sidebar-legend`: write assets to place the compact legend in the MkDocs right sidebar.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools==80.
|
|
2
|
+
requires = ["setuptools==80.10.2", "wheel==0.46.3"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "unifi-network-maps"
|
|
7
|
-
version = "1.4.
|
|
7
|
+
version = "1.4.14"
|
|
8
8
|
description = "Dynamic UniFi -> network maps in mermaid or svg"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -44,7 +44,7 @@ dev = [
|
|
|
44
44
|
"pytest==9.0.2",
|
|
45
45
|
"pytest-cov==7.0.0",
|
|
46
46
|
"pyright==1.1.408",
|
|
47
|
-
"ruff==0.14.
|
|
47
|
+
"ruff==0.14.14"
|
|
48
48
|
]
|
|
49
49
|
|
|
50
50
|
[project.scripts]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.4.14"
|
{unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/adapters/config.py
RENAMED
|
@@ -4,6 +4,9 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
6
|
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from ..io.paths import resolve_env_file
|
|
7
10
|
|
|
8
11
|
|
|
9
12
|
def _parse_bool(value: str | None, default: bool = True) -> bool:
|
|
@@ -26,13 +29,14 @@ class Config:
|
|
|
26
29
|
verify_ssl: bool
|
|
27
30
|
|
|
28
31
|
@classmethod
|
|
29
|
-
def from_env(cls, *, env_file: str | None = None) -> Config:
|
|
32
|
+
def from_env(cls, *, env_file: str | Path | None = None) -> Config:
|
|
30
33
|
if env_file:
|
|
31
34
|
try:
|
|
32
35
|
from dotenv import load_dotenv
|
|
33
36
|
except ImportError:
|
|
34
37
|
raise ValueError("python-dotenv required for --env-file") from None
|
|
35
|
-
|
|
38
|
+
env_path = resolve_env_file(env_file)
|
|
39
|
+
load_dotenv(dotenv_path=env_path)
|
|
36
40
|
url = os.environ.get("UNIFI_URL", "").strip()
|
|
37
41
|
site = os.environ.get("UNIFI_SITE", "default").strip()
|
|
38
42
|
user = os.environ.get("UNIFI_USER", "").strip()
|
{unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/adapters/unifi.py
RENAMED
|
@@ -7,6 +7,7 @@ import json
|
|
|
7
7
|
import logging
|
|
8
8
|
import os
|
|
9
9
|
import stat
|
|
10
|
+
import tempfile
|
|
10
11
|
import time
|
|
11
12
|
from collections.abc import Callable, Iterator, Sequence
|
|
12
13
|
from concurrent.futures import ThreadPoolExecutor
|
|
@@ -15,6 +16,8 @@ from contextlib import contextmanager
|
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
from typing import IO, TYPE_CHECKING
|
|
17
18
|
|
|
19
|
+
from ..io.paths import resolve_cache_dir
|
|
20
|
+
from ..model.vlans import build_vlan_info, normalize_networks
|
|
18
21
|
from .config import Config
|
|
19
22
|
|
|
20
23
|
if TYPE_CHECKING:
|
|
@@ -24,7 +27,15 @@ logger = logging.getLogger(__name__)
|
|
|
24
27
|
|
|
25
28
|
|
|
26
29
|
def _cache_dir() -> Path:
|
|
27
|
-
|
|
30
|
+
default_dir = ".cache/unifi_network_maps"
|
|
31
|
+
if os.environ.get("PYTEST_CURRENT_TEST"):
|
|
32
|
+
default_dir = str(Path(tempfile.gettempdir()) / f"unifi_network_maps_pytest_{os.getpid()}")
|
|
33
|
+
value = os.environ.get("UNIFI_CACHE_DIR", default_dir)
|
|
34
|
+
try:
|
|
35
|
+
return resolve_cache_dir(value)
|
|
36
|
+
except ValueError as exc:
|
|
37
|
+
logger.warning("Invalid UNIFI_CACHE_DIR (%s); using default: %s", value, exc)
|
|
38
|
+
return resolve_cache_dir(".cache/unifi_network_maps")
|
|
28
39
|
|
|
29
40
|
|
|
30
41
|
def _device_attr(device: object, name: str) -> object | None:
|
|
@@ -160,6 +171,19 @@ def _serialize_devices_for_cache(devices: Sequence[object]) -> list[dict[str, ob
|
|
|
160
171
|
return [_serialize_device_for_cache(device) for device in devices]
|
|
161
172
|
|
|
162
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
|
+
|
|
163
187
|
def _cache_lock_path(path: Path) -> Path:
|
|
164
188
|
return path.with_suffix(path.suffix + ".lock")
|
|
165
189
|
|
|
@@ -377,13 +401,13 @@ def fetch_devices(
|
|
|
377
401
|
cached = None
|
|
378
402
|
stale_cached, cache_age = None, None
|
|
379
403
|
if cached is not None:
|
|
380
|
-
logger.
|
|
404
|
+
logger.debug("Using cached devices (%d)", len(cached))
|
|
381
405
|
return cached
|
|
382
406
|
|
|
383
407
|
try:
|
|
384
408
|
controller = _init_controller(config, is_udm_pro=True)
|
|
385
409
|
except UnifiAuthenticationError:
|
|
386
|
-
logger.
|
|
410
|
+
logger.debug("UDM Pro authentication failed, retrying legacy auth")
|
|
387
411
|
controller = _init_controller(config, is_udm_pro=False)
|
|
388
412
|
|
|
389
413
|
def _fetch() -> Sequence[object]:
|
|
@@ -402,7 +426,7 @@ def fetch_devices(
|
|
|
402
426
|
raise
|
|
403
427
|
if use_cache:
|
|
404
428
|
_save_cache(cache_path, _serialize_devices_for_cache(devices))
|
|
405
|
-
logger.
|
|
429
|
+
logger.debug("Fetched %d devices", len(devices))
|
|
406
430
|
return devices
|
|
407
431
|
|
|
408
432
|
|
|
@@ -428,13 +452,13 @@ def fetch_clients(
|
|
|
428
452
|
cached = None
|
|
429
453
|
stale_cached, cache_age = None, None
|
|
430
454
|
if cached is not None:
|
|
431
|
-
logger.
|
|
455
|
+
logger.debug("Using cached clients (%d)", len(cached))
|
|
432
456
|
return cached
|
|
433
457
|
|
|
434
458
|
try:
|
|
435
459
|
controller = _init_controller(config, is_udm_pro=True)
|
|
436
460
|
except UnifiAuthenticationError:
|
|
437
|
-
logger.
|
|
461
|
+
logger.debug("UDM Pro authentication failed, retrying legacy auth")
|
|
438
462
|
controller = _init_controller(config, is_udm_pro=False)
|
|
439
463
|
|
|
440
464
|
def _fetch() -> Sequence[object]:
|
|
@@ -453,5 +477,98 @@ def fetch_clients(
|
|
|
453
477
|
raise
|
|
454
478
|
if use_cache:
|
|
455
479
|
_save_cache(cache_path, clients)
|
|
456
|
-
logger.
|
|
480
|
+
logger.debug("Fetched %d clients", len(clients))
|
|
457
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
|
+
try:
|
|
517
|
+
return controller.get_unifi_site_networkconf(site_name=site_name, raw=False)
|
|
518
|
+
except Exception as exc: # noqa: BLE001 - fallback to raw network data
|
|
519
|
+
logger.warning("Networkconf model parse failed; retrying raw fetch: %s", exc)
|
|
520
|
+
return controller.get_unifi_site_networkconf(site_name=site_name, raw=True)
|
|
521
|
+
|
|
522
|
+
try:
|
|
523
|
+
networks = _call_with_retries("network fetch", _fetch)
|
|
524
|
+
except Exception as exc: # noqa: BLE001 - fallback to cache
|
|
525
|
+
if stale_cached is not None:
|
|
526
|
+
logger.warning(
|
|
527
|
+
"Network fetch failed; using stale cache (%ds old): %s",
|
|
528
|
+
int(cache_age or 0),
|
|
529
|
+
exc,
|
|
530
|
+
)
|
|
531
|
+
return stale_cached
|
|
532
|
+
raise
|
|
533
|
+
if use_cache:
|
|
534
|
+
_save_cache(cache_path, _serialize_networks_for_cache(networks))
|
|
535
|
+
logger.debug("Fetched %d networks", len(networks))
|
|
536
|
+
return networks
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def fetch_payload(
|
|
540
|
+
config: Config,
|
|
541
|
+
*,
|
|
542
|
+
site: str | None = None,
|
|
543
|
+
include_clients: bool = True,
|
|
544
|
+
use_cache: bool = True,
|
|
545
|
+
) -> dict[str, list[object] | list[dict[str, object]]]:
|
|
546
|
+
"""Fetch devices, clients, and VLAN inventory for payload output."""
|
|
547
|
+
devices = list(fetch_devices(config, site=site, detailed=True, use_cache=use_cache))
|
|
548
|
+
clients = _fetch_payload_clients(
|
|
549
|
+
config,
|
|
550
|
+
site=site,
|
|
551
|
+
include_clients=include_clients,
|
|
552
|
+
use_cache=use_cache,
|
|
553
|
+
)
|
|
554
|
+
networks = list(fetch_networks(config, site=site, use_cache=use_cache))
|
|
555
|
+
normalized_networks = normalize_networks(networks)
|
|
556
|
+
vlan_info = build_vlan_info(clients, normalized_networks)
|
|
557
|
+
return {
|
|
558
|
+
"devices": devices,
|
|
559
|
+
"clients": clients,
|
|
560
|
+
"networks": normalized_networks,
|
|
561
|
+
"vlan_info": vlan_info,
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _fetch_payload_clients(
|
|
566
|
+
config: Config,
|
|
567
|
+
*,
|
|
568
|
+
site: str | None,
|
|
569
|
+
include_clients: bool,
|
|
570
|
+
use_cache: bool,
|
|
571
|
+
) -> list[object]:
|
|
572
|
+
if not include_clients:
|
|
573
|
+
return []
|
|
574
|
+
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,11 +3,21 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
|
+
import json
|
|
6
7
|
import logging
|
|
8
|
+
from pathlib import Path
|
|
7
9
|
|
|
8
10
|
from ..adapters.config import Config
|
|
11
|
+
from ..adapters.unifi import fetch_payload
|
|
9
12
|
from ..io.export import write_output
|
|
10
|
-
from ..io.mock_data import load_mock_data
|
|
13
|
+
from ..io.mock_data import load_mock_data, load_mock_payload
|
|
14
|
+
from ..io.paths import (
|
|
15
|
+
resolve_env_file,
|
|
16
|
+
resolve_mock_data_path,
|
|
17
|
+
resolve_output_path,
|
|
18
|
+
resolve_theme_path,
|
|
19
|
+
)
|
|
20
|
+
from ..model.vlans import build_vlan_info, normalize_networks
|
|
11
21
|
from ..render.legend import render_legend_only, resolve_legend_style
|
|
12
22
|
from ..render.theme import resolve_themes
|
|
13
23
|
from .args import build_parser
|
|
@@ -16,7 +26,7 @@ from .render import render_lldp_format, render_standard_format
|
|
|
16
26
|
logger = logging.getLogger(__name__)
|
|
17
27
|
|
|
18
28
|
|
|
19
|
-
def _load_dotenv(env_file: str | None = None) -> None:
|
|
29
|
+
def _load_dotenv(env_file: str | Path | None = None) -> None:
|
|
20
30
|
try:
|
|
21
31
|
from dotenv import load_dotenv
|
|
22
32
|
except ImportError:
|
|
@@ -30,6 +40,36 @@ def _parse_args(argv: list[str] | None) -> argparse.Namespace:
|
|
|
30
40
|
return parser.parse_args(argv)
|
|
31
41
|
|
|
32
42
|
|
|
43
|
+
class _DowngradeInfoToDebugFilter(logging.Filter):
|
|
44
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
45
|
+
if record.name.startswith("unifi_controller_api") and record.levelno == logging.INFO:
|
|
46
|
+
record.levelno = logging.DEBUG
|
|
47
|
+
record.levelname = logging.getLevelName(logging.DEBUG)
|
|
48
|
+
return True
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _downgrade_unifi_controller_logs() -> logging.Filter:
|
|
52
|
+
return _DowngradeInfoToDebugFilter()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _validate_paths(args: argparse.Namespace) -> bool:
|
|
56
|
+
try:
|
|
57
|
+
if args.env_file:
|
|
58
|
+
resolve_env_file(args.env_file)
|
|
59
|
+
if args.mock_data:
|
|
60
|
+
resolve_mock_data_path(args.mock_data, require_exists=False)
|
|
61
|
+
if args.theme_file:
|
|
62
|
+
resolve_theme_path(args.theme_file, require_exists=False)
|
|
63
|
+
if args.generate_mock:
|
|
64
|
+
resolve_output_path(args.generate_mock, format_name="mock")
|
|
65
|
+
if args.output:
|
|
66
|
+
resolve_output_path(args.output, format_name=args.format)
|
|
67
|
+
except ValueError as exc:
|
|
68
|
+
logging.error(str(exc))
|
|
69
|
+
return False
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
|
|
33
73
|
def _load_config(args: argparse.Namespace) -> Config | None:
|
|
34
74
|
try:
|
|
35
75
|
_load_dotenv(args.env_file)
|
|
@@ -59,7 +99,8 @@ def _handle_generate_mock(args: argparse.Namespace) -> int | None:
|
|
|
59
99
|
wireless_client_count=max(0, args.mock_wireless_clients),
|
|
60
100
|
)
|
|
61
101
|
content = mock_payload_json(options)
|
|
62
|
-
|
|
102
|
+
output_kwargs = {"format_name": "mock"} if args.generate_mock else {}
|
|
103
|
+
write_output(content, output_path=args.generate_mock, stdout=args.stdout, **output_kwargs)
|
|
63
104
|
return 0
|
|
64
105
|
|
|
65
106
|
|
|
@@ -79,9 +120,45 @@ def _load_runtime_context(
|
|
|
79
120
|
return config, site, None, None
|
|
80
121
|
|
|
81
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
|
+
|
|
82
155
|
def main(argv: list[str] | None = None) -> int:
|
|
83
156
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
|
|
157
|
+
for handler in logging.getLogger().handlers:
|
|
158
|
+
handler.addFilter(_downgrade_unifi_controller_logs())
|
|
84
159
|
args = _parse_args(argv)
|
|
160
|
+
if not _validate_paths(args):
|
|
161
|
+
return 2
|
|
85
162
|
mock_result = _handle_generate_mock(args)
|
|
86
163
|
if mock_result is not None:
|
|
87
164
|
return mock_result
|
|
@@ -90,6 +167,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
90
167
|
except ValueError as exc:
|
|
91
168
|
logging.error(str(exc))
|
|
92
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
|
|
93
173
|
try:
|
|
94
174
|
mermaid_theme, svg_theme = resolve_themes(args.theme_file)
|
|
95
175
|
except Exception as exc: # noqa: BLE001
|
|
@@ -107,7 +187,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
107
187
|
markdown=args.markdown,
|
|
108
188
|
theme=mermaid_theme,
|
|
109
189
|
)
|
|
110
|
-
|
|
190
|
+
output_kwargs = {"format_name": args.format} if args.output else {}
|
|
191
|
+
write_output(content, output_path=args.output, stdout=args.stdout, **output_kwargs)
|
|
111
192
|
return 0
|
|
112
193
|
|
|
113
194
|
if args.format == "lldp-md":
|
{unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/cli/render.py
RENAMED
|
@@ -207,7 +207,8 @@ def render_lldp_format(
|
|
|
207
207
|
client_mode=args.client_scope,
|
|
208
208
|
only_unifi=args.only_unifi,
|
|
209
209
|
)
|
|
210
|
-
|
|
210
|
+
output_kwargs = {"format_name": args.format} if args.output else {}
|
|
211
|
+
write_output(content, output_path=args.output, stdout=args.stdout, **output_kwargs)
|
|
211
212
|
return 0
|
|
212
213
|
|
|
213
214
|
|
|
@@ -267,5 +268,6 @@ def render_standard_format(
|
|
|
267
268
|
logging.error("Unsupported format: %s", args.format)
|
|
268
269
|
return 2
|
|
269
270
|
|
|
270
|
-
|
|
271
|
+
output_kwargs = {"format_name": args.format} if args.output else {}
|
|
272
|
+
write_output(content, output_path=args.output, stdout=args.stdout, **output_kwargs)
|
|
271
273
|
return 0
|
|
@@ -7,10 +7,19 @@ import sys
|
|
|
7
7
|
import tempfile
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
|
+
from .paths import resolve_output_path
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
|
|
13
|
+
def write_output(
|
|
14
|
+
content: str,
|
|
15
|
+
*,
|
|
16
|
+
output_path: str | Path | None,
|
|
17
|
+
stdout: bool,
|
|
18
|
+
format_name: str | None = None,
|
|
19
|
+
) -> None:
|
|
12
20
|
if output_path:
|
|
13
|
-
|
|
21
|
+
resolved = resolve_output_path(output_path, format_name=format_name)
|
|
22
|
+
_write_atomic(resolved, content)
|
|
14
23
|
if stdout or not output_path:
|
|
15
24
|
sys.stdout.write(content)
|
|
16
25
|
|
{unifi_network_maps-1.4.12 → unifi_network_maps-1.4.14}/src/unifi_network_maps/io/mkdocs_assets.py
RENAMED
|
@@ -5,10 +5,12 @@ from __future__ import annotations
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
7
|
from ..render.templating import render_template
|
|
8
|
+
from .paths import resolve_output_file
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
def write_mkdocs_sidebar_assets(output_path: str) -> None:
|
|
11
|
-
|
|
11
|
+
def write_mkdocs_sidebar_assets(output_path: str | Path) -> None:
|
|
12
|
+
resolved = resolve_output_file(output_path, extensions=None, label="MkDocs output file")
|
|
13
|
+
output_dir = resolved.parent
|
|
12
14
|
assets_dir = output_dir / "assets"
|
|
13
15
|
assets_dir.mkdir(parents=True, exist_ok=True)
|
|
14
16
|
(assets_dir / "legend.js").write_text(
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Load mock UniFi data from JSON fixtures."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from .paths import resolve_mock_data_path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _as_list(value: object, name: str) -> list[object]:
|
|
11
|
+
if value is None:
|
|
12
|
+
return []
|
|
13
|
+
if isinstance(value, list):
|
|
14
|
+
return value
|
|
15
|
+
raise ValueError(f"Mock data field '{name}' must be a list")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_mock_data(path: str) -> tuple[list[object], list[object]]:
|
|
19
|
+
resolved = resolve_mock_data_path(path)
|
|
20
|
+
payload = json.loads(resolved.read_text(encoding="utf-8"))
|
|
21
|
+
if not isinstance(payload, dict):
|
|
22
|
+
raise ValueError("Mock data must be a JSON object")
|
|
23
|
+
devices = _as_list(payload.get("devices"), "devices")
|
|
24
|
+
clients = _as_list(payload.get("clients"), "clients")
|
|
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
|
+
}
|