unifi-network-maps 1.4.11__tar.gz → 1.4.13__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.11 → unifi_network_maps-1.4.13}/CHANGELOG.md +20 -2
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/PKG-INFO +3 -3
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/README.md +1 -1
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/pyproject.toml +3 -3
- unifi_network_maps-1.4.13/src/unifi_network_maps/__init__.py +1 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/adapters/config.py +6 -2
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/adapters/unifi.py +17 -7
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/cli/main.py +46 -3
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/cli/render.py +23 -5
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/cli/runtime.py +7 -1
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/io/export.py +11 -2
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/io/mkdocs_assets.py +4 -2
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/io/mock_data.py +4 -2
- unifi_network_maps-1.4.13/src/unifi_network_maps/io/paths.py +197 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/model/topology.py +91 -7
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/device_ports_md.py +27 -18
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/lldp_md.py +104 -5
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/theme.py +2 -1
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps.egg-info/PKG-INFO +3 -3
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps.egg-info/SOURCES.txt +2 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps.egg-info/requires.txt +1 -1
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/tests/test_clients.py +65 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/tests/test_device_ports_md.py +36 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/tests/test_lldp_md.py +167 -1
- unifi_network_maps-1.4.13/tests/test_mkdocs.py +113 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/tests/test_topology.py +162 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/tests/test_unifi.py +102 -0
- unifi_network_maps-1.4.11/src/unifi_network_maps/__init__.py +0 -1
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/CONTRIBUTING.md +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/LICENSE +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/LICENSES.md +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/MANIFEST.in +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/RELEASING.md +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/SECURITY.md +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/setup.cfg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/__main__.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/adapters/__init__.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/__init__.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/__init__.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/access-point.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/ISOPACKS_LICENSE +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/block.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/cache.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/cardterminal.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/cloud.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/cronjob.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/cube.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/desktop.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/diamond.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/dns.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/document.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/firewall.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/function-module.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/image.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/laptop.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/loadbalancer.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/lock.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/mail.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/mailmultiple.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/mobiledevice.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/office.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/package-module.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/paymentcard.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/plane.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/printer.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/pyramid.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/queue.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/router.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/server.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/speech.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/sphere.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/storage.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/switch-module.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/tower.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/truck-2.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/truck.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/user.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/vm.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/laptop.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/router-network.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/server-network.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/server.svg +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/themes/dark.yaml +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/themes/default.yaml +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/cli/__init__.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/cli/__main__.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/cli/args.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/io/__init__.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/io/debug.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/io/mock_generate.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/model/__init__.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/model/labels.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/model/lldp.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/model/mock.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/model/ports.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/__init__.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/legend.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/markdown_tables.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/mermaid.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/mermaid_theme.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/mkdocs.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/svg.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/svg_theme.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/templates/device_port_block.md.j2 +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/templates/legend_compact.html.j2 +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/templates/lldp_device_section.md.j2 +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/templates/markdown_section.md.j2 +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/templates/mermaid_legend.mmd.j2 +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/templates/mkdocs_document.md.j2 +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/templates/mkdocs_dual_theme_style.html.j2 +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/templates/mkdocs_html_block.html.j2 +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/templates/mkdocs_legend.css.j2 +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/templates/mkdocs_legend.js.j2 +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/templates/mkdocs_mermaid_block.md.j2 +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/templating.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps.egg-info/dependency_links.txt +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps.egg-info/entry_points.txt +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps.egg-info/top_level.txt +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/tests/test_cli.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/tests/test_config.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/tests/test_contract_unifi.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/tests/test_contract_unifi_live.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/tests/test_debug.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/tests/test_export.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/tests/test_groups.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/tests/test_labels.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/tests/test_lldp.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/tests/test_mermaid.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/tests/test_mock_generate.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/tests/test_svg.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/tests/test_svg_iso.py +0 -0
- {unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/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.13] - 2026-01-25
|
|
9
|
+
### Fixed
|
|
10
|
+
- Path Traversal Vulnerability in File Operations
|
|
11
|
+
- Cache Directory Symlink Attack vector
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Improved escaping in Markdown Output
|
|
15
|
+
- Made logging less chatty, moved messages to debug level
|
|
16
|
+
|
|
17
|
+
## [1.4.12] - 2026-01-21
|
|
18
|
+
### Added
|
|
19
|
+
- Filter UniFi clients with --only-unifi, and not only neighbors
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- inconsistencies in --only-unifi
|
|
23
|
+
|
|
8
24
|
## [1.4.11] - 2026-01-19
|
|
9
25
|
### Added
|
|
10
26
|
- Add data-edge-left/right attributes to SVG paths
|
|
@@ -176,8 +192,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
176
192
|
- Introduced SVG renderer and tree layout fixes.
|
|
177
193
|
- Increased test coverage and added coverage tooling.
|
|
178
194
|
|
|
179
|
-
[Unreleased]: https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.
|
|
180
|
-
[1.4.
|
|
195
|
+
[Unreleased]: https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.13...HEAD
|
|
196
|
+
[1.4.13]:https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.11...v1.4.13
|
|
197
|
+
[1.4.12]:https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.11...v1.4.12
|
|
198
|
+
[1.4.11]:https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.10...v1.4.11
|
|
181
199
|
[1.4.10]:https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.9...v1.4.10
|
|
182
200
|
[1.4.9]: https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.8...v1.4.9
|
|
183
201
|
[1.4.8]: https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.7...v1.4.8
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: unifi-network-maps
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.13
|
|
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
|
|
@@ -223,7 +223,7 @@ Functional:
|
|
|
223
223
|
- `--include-ports`: show port labels (Mermaid shows both ends; SVG shows compact labels).
|
|
224
224
|
- `--include-clients`: add active wired clients as leaf nodes.
|
|
225
225
|
- `--client-scope wired|wireless|all`: which client types to include (default wired).
|
|
226
|
-
- `--only-unifi`: only include neighbors that are UniFi devices.
|
|
226
|
+
- `--only-unifi`: only include neighbors that are UniFi devices; when clients are included, filters to UniFi-managed clients (by explicit UniFi flags or vendor/OUI).
|
|
227
227
|
- `--no-cache`: disable UniFi API cache reads and writes.
|
|
228
228
|
|
|
229
229
|
Mermaid:
|
|
@@ -186,7 +186,7 @@ Functional:
|
|
|
186
186
|
- `--include-ports`: show port labels (Mermaid shows both ends; SVG shows compact labels).
|
|
187
187
|
- `--include-clients`: add active wired clients as leaf nodes.
|
|
188
188
|
- `--client-scope wired|wireless|all`: which client types to include (default wired).
|
|
189
|
-
- `--only-unifi`: only include neighbors that are UniFi devices.
|
|
189
|
+
- `--only-unifi`: only include neighbors that are UniFi devices; when clients are included, filters to UniFi-managed clients (by explicit UniFi flags or vendor/OUI).
|
|
190
190
|
- `--no-cache`: disable UniFi API cache reads and writes.
|
|
191
191
|
|
|
192
192
|
Mermaid:
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools==80.
|
|
2
|
+
requires = ["setuptools==80.10.1", "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.13"
|
|
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.13"
|
{unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/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.11 → unifi_network_maps-1.4.13}/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,7 @@ 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
|
|
18
20
|
from .config import Config
|
|
19
21
|
|
|
20
22
|
if TYPE_CHECKING:
|
|
@@ -24,7 +26,15 @@ logger = logging.getLogger(__name__)
|
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
def _cache_dir() -> Path:
|
|
27
|
-
|
|
29
|
+
default_dir = ".cache/unifi_network_maps"
|
|
30
|
+
if os.environ.get("PYTEST_CURRENT_TEST"):
|
|
31
|
+
default_dir = str(Path(tempfile.gettempdir()) / f"unifi_network_maps_pytest_{os.getpid()}")
|
|
32
|
+
value = os.environ.get("UNIFI_CACHE_DIR", default_dir)
|
|
33
|
+
try:
|
|
34
|
+
return resolve_cache_dir(value)
|
|
35
|
+
except ValueError as exc:
|
|
36
|
+
logger.warning("Invalid UNIFI_CACHE_DIR (%s); using default: %s", value, exc)
|
|
37
|
+
return resolve_cache_dir(".cache/unifi_network_maps")
|
|
28
38
|
|
|
29
39
|
|
|
30
40
|
def _device_attr(device: object, name: str) -> object | None:
|
|
@@ -377,13 +387,13 @@ def fetch_devices(
|
|
|
377
387
|
cached = None
|
|
378
388
|
stale_cached, cache_age = None, None
|
|
379
389
|
if cached is not None:
|
|
380
|
-
logger.
|
|
390
|
+
logger.debug("Using cached devices (%d)", len(cached))
|
|
381
391
|
return cached
|
|
382
392
|
|
|
383
393
|
try:
|
|
384
394
|
controller = _init_controller(config, is_udm_pro=True)
|
|
385
395
|
except UnifiAuthenticationError:
|
|
386
|
-
logger.
|
|
396
|
+
logger.debug("UDM Pro authentication failed, retrying legacy auth")
|
|
387
397
|
controller = _init_controller(config, is_udm_pro=False)
|
|
388
398
|
|
|
389
399
|
def _fetch() -> Sequence[object]:
|
|
@@ -402,7 +412,7 @@ def fetch_devices(
|
|
|
402
412
|
raise
|
|
403
413
|
if use_cache:
|
|
404
414
|
_save_cache(cache_path, _serialize_devices_for_cache(devices))
|
|
405
|
-
logger.
|
|
415
|
+
logger.debug("Fetched %d devices", len(devices))
|
|
406
416
|
return devices
|
|
407
417
|
|
|
408
418
|
|
|
@@ -428,13 +438,13 @@ def fetch_clients(
|
|
|
428
438
|
cached = None
|
|
429
439
|
stale_cached, cache_age = None, None
|
|
430
440
|
if cached is not None:
|
|
431
|
-
logger.
|
|
441
|
+
logger.debug("Using cached clients (%d)", len(cached))
|
|
432
442
|
return cached
|
|
433
443
|
|
|
434
444
|
try:
|
|
435
445
|
controller = _init_controller(config, is_udm_pro=True)
|
|
436
446
|
except UnifiAuthenticationError:
|
|
437
|
-
logger.
|
|
447
|
+
logger.debug("UDM Pro authentication failed, retrying legacy auth")
|
|
438
448
|
controller = _init_controller(config, is_udm_pro=False)
|
|
439
449
|
|
|
440
450
|
def _fetch() -> Sequence[object]:
|
|
@@ -453,5 +463,5 @@ def fetch_clients(
|
|
|
453
463
|
raise
|
|
454
464
|
if use_cache:
|
|
455
465
|
_save_cache(cache_path, clients)
|
|
456
|
-
logger.
|
|
466
|
+
logger.debug("Fetched %d clients", len(clients))
|
|
457
467
|
return clients
|
|
@@ -4,10 +4,17 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
6
|
import logging
|
|
7
|
+
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
from ..adapters.config import Config
|
|
9
10
|
from ..io.export import write_output
|
|
10
11
|
from ..io.mock_data import load_mock_data
|
|
12
|
+
from ..io.paths import (
|
|
13
|
+
resolve_env_file,
|
|
14
|
+
resolve_mock_data_path,
|
|
15
|
+
resolve_output_path,
|
|
16
|
+
resolve_theme_path,
|
|
17
|
+
)
|
|
11
18
|
from ..render.legend import render_legend_only, resolve_legend_style
|
|
12
19
|
from ..render.theme import resolve_themes
|
|
13
20
|
from .args import build_parser
|
|
@@ -16,7 +23,7 @@ from .render import render_lldp_format, render_standard_format
|
|
|
16
23
|
logger = logging.getLogger(__name__)
|
|
17
24
|
|
|
18
25
|
|
|
19
|
-
def _load_dotenv(env_file: str | None = None) -> None:
|
|
26
|
+
def _load_dotenv(env_file: str | Path | None = None) -> None:
|
|
20
27
|
try:
|
|
21
28
|
from dotenv import load_dotenv
|
|
22
29
|
except ImportError:
|
|
@@ -30,6 +37,36 @@ def _parse_args(argv: list[str] | None) -> argparse.Namespace:
|
|
|
30
37
|
return parser.parse_args(argv)
|
|
31
38
|
|
|
32
39
|
|
|
40
|
+
class _DowngradeInfoToDebugFilter(logging.Filter):
|
|
41
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
42
|
+
if record.name.startswith("unifi_controller_api") and record.levelno == logging.INFO:
|
|
43
|
+
record.levelno = logging.DEBUG
|
|
44
|
+
record.levelname = logging.getLevelName(logging.DEBUG)
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _downgrade_unifi_controller_logs() -> logging.Filter:
|
|
49
|
+
return _DowngradeInfoToDebugFilter()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _validate_paths(args: argparse.Namespace) -> bool:
|
|
53
|
+
try:
|
|
54
|
+
if args.env_file:
|
|
55
|
+
resolve_env_file(args.env_file)
|
|
56
|
+
if args.mock_data:
|
|
57
|
+
resolve_mock_data_path(args.mock_data, require_exists=False)
|
|
58
|
+
if args.theme_file:
|
|
59
|
+
resolve_theme_path(args.theme_file, require_exists=False)
|
|
60
|
+
if args.generate_mock:
|
|
61
|
+
resolve_output_path(args.generate_mock, format_name="mock")
|
|
62
|
+
if args.output:
|
|
63
|
+
resolve_output_path(args.output, format_name=args.format)
|
|
64
|
+
except ValueError as exc:
|
|
65
|
+
logging.error(str(exc))
|
|
66
|
+
return False
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
|
|
33
70
|
def _load_config(args: argparse.Namespace) -> Config | None:
|
|
34
71
|
try:
|
|
35
72
|
_load_dotenv(args.env_file)
|
|
@@ -59,7 +96,8 @@ def _handle_generate_mock(args: argparse.Namespace) -> int | None:
|
|
|
59
96
|
wireless_client_count=max(0, args.mock_wireless_clients),
|
|
60
97
|
)
|
|
61
98
|
content = mock_payload_json(options)
|
|
62
|
-
|
|
99
|
+
output_kwargs = {"format_name": "mock"} if args.generate_mock else {}
|
|
100
|
+
write_output(content, output_path=args.generate_mock, stdout=args.stdout, **output_kwargs)
|
|
63
101
|
return 0
|
|
64
102
|
|
|
65
103
|
|
|
@@ -81,7 +119,11 @@ def _load_runtime_context(
|
|
|
81
119
|
|
|
82
120
|
def main(argv: list[str] | None = None) -> int:
|
|
83
121
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
|
|
122
|
+
for handler in logging.getLogger().handlers:
|
|
123
|
+
handler.addFilter(_downgrade_unifi_controller_logs())
|
|
84
124
|
args = _parse_args(argv)
|
|
125
|
+
if not _validate_paths(args):
|
|
126
|
+
return 2
|
|
85
127
|
mock_result = _handle_generate_mock(args)
|
|
86
128
|
if mock_result is not None:
|
|
87
129
|
return mock_result
|
|
@@ -107,7 +149,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
107
149
|
markdown=args.markdown,
|
|
108
150
|
theme=mermaid_theme,
|
|
109
151
|
)
|
|
110
|
-
|
|
152
|
+
output_kwargs = {"format_name": args.format} if args.output else {}
|
|
153
|
+
write_output(content, output_path=args.output, stdout=args.stdout, **output_kwargs)
|
|
111
154
|
return 0
|
|
112
155
|
|
|
113
156
|
if args.format == "lldp-md":
|
{unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/cli/render.py
RENAMED
|
@@ -62,7 +62,12 @@ def render_mermaid_output(
|
|
|
62
62
|
direction=args.direction,
|
|
63
63
|
groups=groups,
|
|
64
64
|
group_order=group_order,
|
|
65
|
-
node_types=build_node_type_map(
|
|
65
|
+
node_types=build_node_type_map(
|
|
66
|
+
devices,
|
|
67
|
+
clients,
|
|
68
|
+
client_mode=args.client_scope,
|
|
69
|
+
only_unifi=args.only_unifi,
|
|
70
|
+
),
|
|
66
71
|
theme=mermaid_theme,
|
|
67
72
|
)
|
|
68
73
|
if args.markdown:
|
|
@@ -97,13 +102,23 @@ def render_svg_output(
|
|
|
97
102
|
|
|
98
103
|
return render_svg_isometric(
|
|
99
104
|
edges,
|
|
100
|
-
node_types=build_node_type_map(
|
|
105
|
+
node_types=build_node_type_map(
|
|
106
|
+
devices,
|
|
107
|
+
clients,
|
|
108
|
+
client_mode=args.client_scope,
|
|
109
|
+
only_unifi=args.only_unifi,
|
|
110
|
+
),
|
|
101
111
|
options=options,
|
|
102
112
|
theme=svg_theme,
|
|
103
113
|
)
|
|
104
114
|
return render_svg(
|
|
105
115
|
edges,
|
|
106
|
-
node_types=build_node_type_map(
|
|
116
|
+
node_types=build_node_type_map(
|
|
117
|
+
devices,
|
|
118
|
+
clients,
|
|
119
|
+
client_mode=args.client_scope,
|
|
120
|
+
only_unifi=args.only_unifi,
|
|
121
|
+
),
|
|
107
122
|
options=options,
|
|
108
123
|
theme=svg_theme,
|
|
109
124
|
)
|
|
@@ -190,8 +205,10 @@ def render_lldp_format(
|
|
|
190
205
|
include_ports=args.include_ports,
|
|
191
206
|
show_clients=args.include_clients,
|
|
192
207
|
client_mode=args.client_scope,
|
|
208
|
+
only_unifi=args.only_unifi,
|
|
193
209
|
)
|
|
194
|
-
|
|
210
|
+
output_kwargs = {"format_name": args.format} if args.output else {}
|
|
211
|
+
write_output(content, output_path=args.output, stdout=args.stdout, **output_kwargs)
|
|
195
212
|
return 0
|
|
196
213
|
|
|
197
214
|
|
|
@@ -251,5 +268,6 @@ def render_standard_format(
|
|
|
251
268
|
logging.error("Unsupported format: %s", args.format)
|
|
252
269
|
return 2
|
|
253
270
|
|
|
254
|
-
|
|
271
|
+
output_kwargs = {"format_name": args.format} if args.output else {}
|
|
272
|
+
write_output(content, output_path=args.output, stdout=args.stdout, **output_kwargs)
|
|
255
273
|
return 0
|
{unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/cli/runtime.py
RENAMED
|
@@ -95,6 +95,7 @@ def build_edges_with_clients(
|
|
|
95
95
|
device_index,
|
|
96
96
|
include_ports=args.include_ports,
|
|
97
97
|
client_mode=args.client_scope,
|
|
98
|
+
only_unifi=args.only_unifi,
|
|
98
99
|
)
|
|
99
100
|
return edges, clients
|
|
100
101
|
|
|
@@ -153,5 +154,10 @@ def resolve_mkdocs_client_ports(
|
|
|
153
154
|
clients = list(fetch_clients(config, site=site))
|
|
154
155
|
else:
|
|
155
156
|
clients = mock_clients
|
|
156
|
-
client_ports = build_client_port_map(
|
|
157
|
+
client_ports = build_client_port_map(
|
|
158
|
+
devices,
|
|
159
|
+
clients,
|
|
160
|
+
client_mode=args.client_scope,
|
|
161
|
+
only_unifi=args.only_unifi,
|
|
162
|
+
)
|
|
157
163
|
return client_ports, None
|
|
@@ -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.11 → unifi_network_maps-1.4.13}/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(
|
{unifi_network_maps-1.4.11 → unifi_network_maps-1.4.13}/src/unifi_network_maps/io/mock_data.py
RENAMED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
from .paths import resolve_mock_data_path
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
def _as_list(value: object, name: str) -> list[object]:
|
|
@@ -15,7 +16,8 @@ def _as_list(value: object, name: str) -> list[object]:
|
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
def load_mock_data(path: str) -> tuple[list[object], list[object]]:
|
|
18
|
-
|
|
19
|
+
resolved = resolve_mock_data_path(path)
|
|
20
|
+
payload = json.loads(resolved.read_text(encoding="utf-8"))
|
|
19
21
|
if not isinstance(payload, dict):
|
|
20
22
|
raise ValueError("Mock data must be a JSON object")
|
|
21
23
|
devices = _as_list(payload.get("devices"), "devices")
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Path validation helpers for user-supplied file system inputs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
from collections.abc import Iterable
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _safe_home_dir() -> Path | None:
|
|
12
|
+
try:
|
|
13
|
+
return Path.home().resolve()
|
|
14
|
+
except Exception:
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _base_roots() -> list[Path]:
|
|
19
|
+
roots = [Path.cwd().resolve()]
|
|
20
|
+
home = _safe_home_dir()
|
|
21
|
+
if home:
|
|
22
|
+
roots.append(home)
|
|
23
|
+
try:
|
|
24
|
+
roots.append(Path(tempfile.gettempdir()).resolve())
|
|
25
|
+
except Exception:
|
|
26
|
+
pass
|
|
27
|
+
return roots
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _extra_roots_from_env() -> list[Path]:
|
|
31
|
+
extra = os.environ.get("UNIFI_ALLOWED_PATHS", "")
|
|
32
|
+
roots: list[Path] = []
|
|
33
|
+
if extra:
|
|
34
|
+
for raw in extra.split(os.pathsep):
|
|
35
|
+
raw = raw.strip()
|
|
36
|
+
if raw:
|
|
37
|
+
roots.append(Path(raw).expanduser().resolve())
|
|
38
|
+
return roots
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _allowed_roots() -> tuple[Path, ...]:
|
|
42
|
+
roots = _base_roots() + _extra_roots_from_env()
|
|
43
|
+
seen: set[str] = set()
|
|
44
|
+
unique: list[Path] = []
|
|
45
|
+
for root in roots:
|
|
46
|
+
key = str(root)
|
|
47
|
+
if key not in seen:
|
|
48
|
+
seen.add(key)
|
|
49
|
+
unique.append(root)
|
|
50
|
+
return tuple(unique)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _resolve_user_path(path: str | Path) -> Path:
|
|
54
|
+
return Path(path).expanduser().resolve(strict=False)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _ensure_within_allowed(path: Path, roots: Iterable[Path], *, label: str) -> None:
|
|
58
|
+
for root in roots:
|
|
59
|
+
try:
|
|
60
|
+
path.relative_to(root)
|
|
61
|
+
except ValueError:
|
|
62
|
+
continue
|
|
63
|
+
else:
|
|
64
|
+
return
|
|
65
|
+
root_list = ", ".join(str(root) for root in roots)
|
|
66
|
+
raise ValueError(f"{label} must be within: {root_list}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _ensure_no_symlink(path: Path, *, label: str) -> None:
|
|
70
|
+
if path.exists() and path.is_symlink():
|
|
71
|
+
raise ValueError(f"{label} must not be a symlink: {path}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _ensure_no_symlink_in_parents(path: Path, *, label: str) -> None:
|
|
75
|
+
for parent in path.parents:
|
|
76
|
+
if parent.exists() and parent.is_symlink():
|
|
77
|
+
raise ValueError(f"{label} parent must not be a symlink: {parent}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _normalize_extensions(extensions: Iterable[str]) -> set[str]:
|
|
81
|
+
normalized = set()
|
|
82
|
+
for ext in extensions:
|
|
83
|
+
ext = ext.strip().lower()
|
|
84
|
+
if not ext:
|
|
85
|
+
continue
|
|
86
|
+
if not ext.startswith("."):
|
|
87
|
+
ext = f".{ext}"
|
|
88
|
+
normalized.add(ext)
|
|
89
|
+
return normalized
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _ensure_extension(
|
|
93
|
+
path: Path,
|
|
94
|
+
extensions: Iterable[str] | None,
|
|
95
|
+
*,
|
|
96
|
+
label: str,
|
|
97
|
+
allow_missing: bool = False,
|
|
98
|
+
) -> None:
|
|
99
|
+
if not extensions:
|
|
100
|
+
return
|
|
101
|
+
allowed = _normalize_extensions(extensions)
|
|
102
|
+
suffix = path.suffix.lower()
|
|
103
|
+
if not suffix:
|
|
104
|
+
if allow_missing:
|
|
105
|
+
return
|
|
106
|
+
raise ValueError(f"{label} must have one of: {', '.join(sorted(allowed))}")
|
|
107
|
+
if suffix not in allowed:
|
|
108
|
+
raise ValueError(f"{label} must have one of: {', '.join(sorted(allowed))}")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def resolve_input_file(
|
|
112
|
+
path: str | Path,
|
|
113
|
+
*,
|
|
114
|
+
extensions: Iterable[str] | None,
|
|
115
|
+
label: str,
|
|
116
|
+
require_exists: bool = True,
|
|
117
|
+
) -> Path:
|
|
118
|
+
resolved = _resolve_user_path(path)
|
|
119
|
+
_ensure_within_allowed(resolved, _allowed_roots(), label=label)
|
|
120
|
+
_ensure_extension(resolved, extensions, label=label)
|
|
121
|
+
if require_exists:
|
|
122
|
+
if not resolved.exists():
|
|
123
|
+
raise ValueError(f"{label} does not exist: {resolved}")
|
|
124
|
+
if not resolved.is_file():
|
|
125
|
+
raise ValueError(f"{label} must be a file: {resolved}")
|
|
126
|
+
return resolved
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def resolve_output_file(
|
|
130
|
+
path: str | Path,
|
|
131
|
+
*,
|
|
132
|
+
extensions: Iterable[str] | None,
|
|
133
|
+
label: str,
|
|
134
|
+
allow_missing_extension: bool = False,
|
|
135
|
+
) -> Path:
|
|
136
|
+
resolved = _resolve_user_path(path)
|
|
137
|
+
_ensure_within_allowed(resolved, _allowed_roots(), label=label)
|
|
138
|
+
_ensure_extension(
|
|
139
|
+
resolved,
|
|
140
|
+
extensions,
|
|
141
|
+
label=label,
|
|
142
|
+
allow_missing=allow_missing_extension,
|
|
143
|
+
)
|
|
144
|
+
return resolved
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def resolve_env_file(path: str | Path) -> Path:
|
|
148
|
+
resolved = _resolve_user_path(path)
|
|
149
|
+
_ensure_within_allowed(resolved, _allowed_roots(), label="Env file")
|
|
150
|
+
if not (resolved.name.startswith(".env") or resolved.name.endswith(".env")):
|
|
151
|
+
raise ValueError("Env file must end with .env")
|
|
152
|
+
if resolved.exists() and not resolved.is_file():
|
|
153
|
+
raise ValueError(f"Env file must be a file: {resolved}")
|
|
154
|
+
return resolved
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def resolve_mock_data_path(path: str | Path, *, require_exists: bool = True) -> Path:
|
|
158
|
+
return resolve_input_file(
|
|
159
|
+
path,
|
|
160
|
+
extensions={".json"},
|
|
161
|
+
label="Mock data file",
|
|
162
|
+
require_exists=require_exists,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def resolve_theme_path(path: str | Path, *, require_exists: bool = True) -> Path:
|
|
167
|
+
return resolve_input_file(
|
|
168
|
+
path,
|
|
169
|
+
extensions={".yml", ".yaml"},
|
|
170
|
+
label="Theme file",
|
|
171
|
+
require_exists=require_exists,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def resolve_output_path(path: str | Path, *, format_name: str | None) -> Path:
|
|
176
|
+
extensions: set[str] | None
|
|
177
|
+
if format_name == "svg" or format_name == "svg-iso":
|
|
178
|
+
extensions = {".svg"}
|
|
179
|
+
elif format_name == "mock":
|
|
180
|
+
extensions = {".json"}
|
|
181
|
+
elif format_name == "mermaid":
|
|
182
|
+
extensions = {".md", ".mermaid", ".mmd"}
|
|
183
|
+
elif format_name == "lldp-md":
|
|
184
|
+
extensions = {".md"}
|
|
185
|
+
elif format_name == "mkdocs":
|
|
186
|
+
extensions = {".md"}
|
|
187
|
+
else:
|
|
188
|
+
extensions = None
|
|
189
|
+
return resolve_output_file(path, extensions=extensions, label="Output file")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def resolve_cache_dir(path: str | Path) -> Path:
|
|
193
|
+
resolved = _resolve_user_path(path)
|
|
194
|
+
_ensure_within_allowed(resolved, _allowed_roots(), label="Cache directory")
|
|
195
|
+
_ensure_no_symlink(resolved, label="Cache directory")
|
|
196
|
+
_ensure_no_symlink_in_parents(resolved, label="Cache directory")
|
|
197
|
+
return resolved
|