unifi-network-maps 1.4.12__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.12 → unifi_network_maps-1.4.13}/CHANGELOG.md +11 -1
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/PKG-INFO +2 -2
- {unifi_network_maps-1.4.12 → 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.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/adapters/config.py +6 -2
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/adapters/unifi.py +17 -7
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/cli/main.py +46 -3
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/cli/render.py +4 -2
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/io/export.py +11 -2
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/io/mkdocs_assets.py +4 -2
- {unifi_network_maps-1.4.12 → 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.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/model/topology.py +3 -3
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/device_ports_md.py +27 -18
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/lldp_md.py +4 -1
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/theme.py +2 -1
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps.egg-info/PKG-INFO +2 -2
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps.egg-info/SOURCES.txt +2 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps.egg-info/requires.txt +1 -1
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/tests/test_device_ports_md.py +36 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/tests/test_lldp_md.py +112 -1
- unifi_network_maps-1.4.13/tests/test_mkdocs.py +113 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/tests/test_topology.py +130 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/tests/test_unifi.py +102 -0
- unifi_network_maps-1.4.12/src/unifi_network_maps/__init__.py +0 -1
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/CONTRIBUTING.md +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/LICENSE +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/LICENSES.md +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/MANIFEST.in +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/README.md +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/RELEASING.md +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/SECURITY.md +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/setup.cfg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/__main__.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/adapters/__init__.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/__init__.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/__init__.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/access-point.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/ISOPACKS_LICENSE +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/block.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/cache.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/cardterminal.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/cloud.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/cronjob.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/cube.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/desktop.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/diamond.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/dns.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/document.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/firewall.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/function-module.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/image.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/laptop.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/loadbalancer.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/lock.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/mail.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/mailmultiple.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/mobiledevice.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/office.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/package-module.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/paymentcard.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/plane.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/printer.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/pyramid.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/queue.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/router.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/server.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/speech.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/sphere.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/storage.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/switch-module.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/tower.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/truck-2.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/truck.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/user.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/isometric/vm.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/laptop.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/router-network.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/server-network.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/icons/server.svg +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/themes/dark.yaml +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/assets/themes/default.yaml +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/cli/__init__.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/cli/__main__.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/cli/args.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/cli/runtime.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/io/__init__.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/io/debug.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/io/mock_generate.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/model/__init__.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/model/labels.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/model/lldp.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/model/mock.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/model/ports.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/__init__.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/legend.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/markdown_tables.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/mermaid.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/mermaid_theme.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/mkdocs.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/svg.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/svg_theme.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/templates/device_port_block.md.j2 +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/templates/legend_compact.html.j2 +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/templates/lldp_device_section.md.j2 +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/templates/markdown_section.md.j2 +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/templates/mermaid_legend.mmd.j2 +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/templates/mkdocs_document.md.j2 +0 -0
- {unifi_network_maps-1.4.12 → 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.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/templates/mkdocs_html_block.html.j2 +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/templates/mkdocs_legend.css.j2 +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/templates/mkdocs_legend.js.j2 +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/templates/mkdocs_mermaid_block.md.j2 +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/templating.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps.egg-info/dependency_links.txt +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps.egg-info/entry_points.txt +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps.egg-info/top_level.txt +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/tests/test_cli.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/tests/test_clients.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/tests/test_config.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/tests/test_contract_unifi.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/tests/test_contract_unifi_live.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/tests/test_debug.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/tests/test_export.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/tests/test_groups.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/tests/test_labels.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/tests/test_lldp.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/tests/test_mermaid.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/tests/test_mock_generate.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/tests/test_svg.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/tests/test_svg_iso.py +0 -0
- {unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/tests/test_theme.py +0 -0
|
@@ -5,6 +5,15 @@ 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
|
+
|
|
8
17
|
## [1.4.12] - 2026-01-21
|
|
9
18
|
### Added
|
|
10
19
|
- Filter UniFi clients with --only-unifi, and not only neighbors
|
|
@@ -183,7 +192,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
183
192
|
- Introduced SVG renderer and tree layout fixes.
|
|
184
193
|
- Increased test coverage and added coverage tooling.
|
|
185
194
|
|
|
186
|
-
[Unreleased]: https://github.com/merlijntishauser/unifi-network-maps/compare/v1.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
|
|
187
197
|
[1.4.12]:https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.11...v1.4.12
|
|
188
198
|
[1.4.11]:https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.10...v1.4.11
|
|
189
199
|
[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.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
|
|
@@ -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.12 → 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.12 → 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.12 → unifi_network_maps-1.4.13}/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.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.12 → 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
|
{unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/model/topology.py
RENAMED
|
@@ -723,7 +723,7 @@ def build_edges(
|
|
|
723
723
|
)
|
|
724
724
|
|
|
725
725
|
poe_edges = sum(1 for edge in edges if edge.poe)
|
|
726
|
-
logger.
|
|
726
|
+
logger.debug("Built %d unique edges (%d PoE)", len(edges), poe_edges)
|
|
727
727
|
return edges
|
|
728
728
|
|
|
729
729
|
|
|
@@ -978,14 +978,14 @@ def build_topology(
|
|
|
978
978
|
) -> TopologyResult:
|
|
979
979
|
normalized_devices = list(devices)
|
|
980
980
|
lldp_entries = sum(len(device.lldp_info) for device in normalized_devices)
|
|
981
|
-
logger.
|
|
981
|
+
logger.debug(
|
|
982
982
|
"Normalized %d devices (%d LLDP entries)",
|
|
983
983
|
len(normalized_devices),
|
|
984
984
|
lldp_entries,
|
|
985
985
|
)
|
|
986
986
|
raw_edges = build_edges(normalized_devices, include_ports=include_ports, only_unifi=only_unifi)
|
|
987
987
|
tree_edges = build_tree_edges_by_topology(raw_edges, gateways)
|
|
988
|
-
logger.
|
|
988
|
+
logger.debug(
|
|
989
989
|
"Built %d hierarchy edges (gateways=%d)",
|
|
990
990
|
len(tree_edges),
|
|
991
991
|
len(gateways),
|
|
@@ -83,11 +83,11 @@ def _render_device_ports(
|
|
|
83
83
|
rows = _build_port_rows(device, port_map, client_ports)
|
|
84
84
|
table_rows = [
|
|
85
85
|
[
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
86
|
+
_escape_markdown_text(port_label),
|
|
87
|
+
_escape_connected_cell(connected or "-"),
|
|
88
|
+
_escape_markdown_text(speed),
|
|
89
|
+
_escape_markdown_text(poe_state),
|
|
90
|
+
_escape_markdown_text(power),
|
|
91
91
|
]
|
|
92
92
|
for port_label, connected, speed, poe_state, power in rows
|
|
93
93
|
]
|
|
@@ -224,9 +224,11 @@ def _format_connections(
|
|
|
224
224
|
for peer in sorted(peers, key=str.lower):
|
|
225
225
|
peer_label = port_map.get((peer, device_name))
|
|
226
226
|
if peer_label:
|
|
227
|
-
peer_entries.append(
|
|
227
|
+
peer_entries.append(
|
|
228
|
+
f"{_escape_markdown_text(peer)} ({_escape_markdown_text(peer_label)})"
|
|
229
|
+
)
|
|
228
230
|
else:
|
|
229
|
-
peer_entries.append(peer)
|
|
231
|
+
peer_entries.append(_escape_markdown_text(peer))
|
|
230
232
|
peer_text = ", ".join(peer_entries)
|
|
231
233
|
client_text = _format_client_connections(clients)
|
|
232
234
|
if peer_text and client_text:
|
|
@@ -292,8 +294,15 @@ def _port_sort_key(port: object) -> tuple[int, str]:
|
|
|
292
294
|
return (1, name.lower())
|
|
293
295
|
|
|
294
296
|
|
|
295
|
-
def
|
|
296
|
-
|
|
297
|
+
def _escape_markdown_text(value: str) -> str:
|
|
298
|
+
escaped = value.replace("\\", "\\\\")
|
|
299
|
+
for char in ("|", "[", "]", "*", "_", "`", "<", ">"):
|
|
300
|
+
escaped = escaped.replace(char, f"\\{char}")
|
|
301
|
+
return escaped
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _escape_connected_cell(value: str) -> str:
|
|
305
|
+
return value
|
|
297
306
|
|
|
298
307
|
|
|
299
308
|
def _render_device_details(device: Device) -> list[str]:
|
|
@@ -302,14 +311,14 @@ def _render_device_details(device: Device) -> list[str]:
|
|
|
302
311
|
"",
|
|
303
312
|
"| Field | Value |",
|
|
304
313
|
"| --- | --- |",
|
|
305
|
-
f"| Model | {
|
|
306
|
-
f"| Type | {
|
|
307
|
-
f"| IP | {
|
|
308
|
-
f"| MAC | {
|
|
309
|
-
f"| Firmware | {
|
|
310
|
-
f"| Uplink | {
|
|
311
|
-
f"| Ports | {
|
|
312
|
-
f"| PoE | {
|
|
314
|
+
f"| Model | {_escape_markdown_text(_device_model_label(device))} |",
|
|
315
|
+
f"| Type | {_escape_markdown_text(device.type or '-')} |",
|
|
316
|
+
f"| IP | {_escape_markdown_text(device.ip or '-')} |",
|
|
317
|
+
f"| MAC | {_escape_markdown_text(device.mac or '-')} |",
|
|
318
|
+
f"| Firmware | {_escape_markdown_text(device.version or '-')} |",
|
|
319
|
+
f"| Uplink | {_escape_markdown_text(_uplink_summary(device))} |",
|
|
320
|
+
f"| Ports | {_escape_markdown_text(_port_summary(device))} |",
|
|
321
|
+
f"| PoE | {_escape_markdown_text(_poe_summary(device))} |",
|
|
313
322
|
"",
|
|
314
323
|
]
|
|
315
324
|
return lines
|
|
@@ -367,7 +376,7 @@ def _format_client_connections(clients: list[str]) -> str:
|
|
|
367
376
|
if not clients:
|
|
368
377
|
return ""
|
|
369
378
|
if len(clients) == 1:
|
|
370
|
-
return f"{clients[0]} (client)"
|
|
379
|
+
return f"{_escape_markdown_text(clients[0])} (client)"
|
|
371
380
|
items = "".join(f"<li>{_escape_html(name)}</li>" for name in clients)
|
|
372
381
|
return f'<ul class="unifi-port-clients">{items}</ul>'
|
|
373
382
|
|
{unifi_network_maps-1.4.12 → unifi_network_maps-1.4.13}/src/unifi_network_maps/render/lldp_md.py
RENAMED
|
@@ -271,7 +271,10 @@ def _lldp_rows(
|
|
|
271
271
|
|
|
272
272
|
|
|
273
273
|
def _escape_cell(value: str) -> str:
|
|
274
|
-
|
|
274
|
+
escaped = value.replace("\\", "\\\\")
|
|
275
|
+
for char in ("|", "[", "]", "*", "_", "`", "<", ">"):
|
|
276
|
+
escaped = escaped.replace(char, f"\\{char}")
|
|
277
|
+
return escaped
|
|
275
278
|
|
|
276
279
|
|
|
277
280
|
def _client_rows(
|