unifi-network-maps 1.3.1__tar.gz → 1.4.1__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.
Files changed (111) hide show
  1. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/CHANGELOG.md +19 -2
  2. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/CONTRIBUTING.md +2 -1
  3. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/PKG-INFO +89 -15
  4. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/README.md +78 -3
  5. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/RELEASING.md +2 -1
  6. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/pyproject.toml +20 -17
  7. unifi_network_maps-1.4.1/src/unifi_network_maps/__init__.py +1 -0
  8. unifi_network_maps-1.4.1/src/unifi_network_maps/__main__.py +8 -0
  9. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/adapters/unifi.py +90 -9
  10. unifi_network_maps-1.4.1/src/unifi_network_maps/cli/main.py +614 -0
  11. unifi_network_maps-1.4.1/src/unifi_network_maps/io/mock_data.py +23 -0
  12. unifi_network_maps-1.4.1/src/unifi_network_maps/io/mock_generate.py +299 -0
  13. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/model/lldp.py +26 -12
  14. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/model/topology.py +111 -3
  15. unifi_network_maps-1.4.1/src/unifi_network_maps/render/device_ports_md.py +462 -0
  16. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/render/lldp_md.py +33 -12
  17. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/render/mermaid.py +62 -3
  18. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps.egg-info/PKG-INFO +89 -15
  19. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps.egg-info/SOURCES.txt +6 -0
  20. unifi_network_maps-1.4.1/src/unifi_network_maps.egg-info/requires.txt +10 -0
  21. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_cli.py +189 -6
  22. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_clients.py +3 -1
  23. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_debug.py +2 -2
  24. unifi_network_maps-1.4.1/tests/test_device_ports_md.py +526 -0
  25. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_groups.py +6 -2
  26. unifi_network_maps-1.4.1/tests/test_lldp_md.py +296 -0
  27. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_mermaid.py +9 -2
  28. unifi_network_maps-1.4.1/tests/test_mock_generate.py +28 -0
  29. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_topology.py +36 -0
  30. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_unifi.py +72 -0
  31. unifi_network_maps-1.3.1/src/unifi_network_maps/__init__.py +0 -1
  32. unifi_network_maps-1.3.1/src/unifi_network_maps/cli/main.py +0 -317
  33. unifi_network_maps-1.3.1/src/unifi_network_maps.egg-info/requires.txt +0 -9
  34. unifi_network_maps-1.3.1/tests/test_lldp_md.py +0 -42
  35. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/LICENSE +0 -0
  36. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/LICENSES.md +0 -0
  37. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/MANIFEST.in +0 -0
  38. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/SECURITY.md +0 -0
  39. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/setup.cfg +0 -0
  40. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/adapters/__init__.py +0 -0
  41. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/adapters/config.py +0 -0
  42. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/__init__.py +0 -0
  43. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/__init__.py +0 -0
  44. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/access-point.svg +0 -0
  45. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/ISOPACKS_LICENSE +0 -0
  46. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/block.svg +0 -0
  47. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/cache.svg +0 -0
  48. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/cardterminal.svg +0 -0
  49. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/cloud.svg +0 -0
  50. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/cronjob.svg +0 -0
  51. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/cube.svg +0 -0
  52. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/desktop.svg +0 -0
  53. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/diamond.svg +0 -0
  54. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/dns.svg +0 -0
  55. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/document.svg +0 -0
  56. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/firewall.svg +0 -0
  57. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/function-module.svg +0 -0
  58. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/image.svg +0 -0
  59. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/laptop.svg +0 -0
  60. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/loadbalancer.svg +0 -0
  61. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/lock.svg +0 -0
  62. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/mail.svg +0 -0
  63. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/mailmultiple.svg +0 -0
  64. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/mobiledevice.svg +0 -0
  65. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/office.svg +0 -0
  66. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/package-module.svg +0 -0
  67. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/paymentcard.svg +0 -0
  68. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/plane.svg +0 -0
  69. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/printer.svg +0 -0
  70. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/pyramid.svg +0 -0
  71. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/queue.svg +0 -0
  72. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/router.svg +0 -0
  73. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/server.svg +0 -0
  74. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/speech.svg +0 -0
  75. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/sphere.svg +0 -0
  76. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/storage.svg +0 -0
  77. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/switch-module.svg +0 -0
  78. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/tower.svg +0 -0
  79. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/truck-2.svg +0 -0
  80. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/truck.svg +0 -0
  81. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/user.svg +0 -0
  82. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/isometric/vm.svg +0 -0
  83. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/laptop.svg +0 -0
  84. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/router-network.svg +0 -0
  85. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/server-network.svg +0 -0
  86. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/icons/server.svg +0 -0
  87. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/themes/dark.yaml +0 -0
  88. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/assets/themes/default.yaml +0 -0
  89. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/cli/__init__.py +0 -0
  90. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/cli/__main__.py +0 -0
  91. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/io/__init__.py +0 -0
  92. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/io/debug.py +0 -0
  93. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/io/export.py +0 -0
  94. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/model/__init__.py +0 -0
  95. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/model/labels.py +0 -0
  96. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/model/ports.py +0 -0
  97. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/render/__init__.py +0 -0
  98. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/render/mermaid_theme.py +0 -0
  99. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/render/svg.py +0 -0
  100. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/render/svg_theme.py +0 -0
  101. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps/render/theme.py +0 -0
  102. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps.egg-info/dependency_links.txt +0 -0
  103. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps.egg-info/entry_points.txt +0 -0
  104. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/src/unifi_network_maps.egg-info/top_level.txt +0 -0
  105. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_config.py +0 -0
  106. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_export.py +0 -0
  107. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_labels.py +0 -0
  108. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_lldp.py +0 -0
  109. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_svg.py +0 -0
  110. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_svg_iso.py +0 -0
  111. {unifi_network_maps-1.3.1 → unifi_network_maps-1.4.1}/tests/test_theme.py +0 -0
@@ -2,8 +2,25 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- ## Unreleased
6
- - TBD.
5
+ ## v1.4.0
6
+ - Added MkDocs output, which includes gateway/switch details and per-port tables.
7
+ - Port tables show speed, PoE status, power, and wired clients per port.
8
+ - Added compact legend with sidebar injection (`--mkdocs-sidebar-legend`).
9
+ - LLDP markdown includes the same device details and port tables when enabled.
10
+ - Improved uplink labeling (gateway shows Internet for WAN/unknown).
11
+ - Aggregated ports are combined into single LAG rows.
12
+ - Bumped minimum Python to 3.13 and aligned CI to 3.13.
13
+ - Pinned runtime/dev/build dependencies and added `requirements*.txt` + `constraints.txt`.
14
+ - Added `--mock-data` for safe, offline rendering from fixtures.
15
+ - Added Faker-powered `--generate-mock` for deterministic mock fixtures (dev-only).
16
+ - Added mock fixtures + SVG/Mermaid examples, with mock smoketest/CI steps.
17
+
18
+ ## v1.3.1
19
+ - Added `lldp-md` output with per-device details tables and optional client sections.
20
+ - Added `--client-scope wired|wireless|all` and dashed wireless client links in Mermaid/SVG.
21
+ - Expanded smoketest outputs for wireless/all client scopes and LLDP markdown.
22
+ - Fixed SVG icon loading paths after package reorg.
23
+ - Tuned isometric port label placement on front tiles.
7
24
 
8
25
  ## v1.3.0
9
26
  - Reorganized package into submodules (`adapters/`, `model/`, `render/`, `io/`, `cli/`).
@@ -7,7 +7,8 @@ Thanks for considering a contribution!
7
7
  ```bash
8
8
  python -m venv .venv
9
9
  source .venv/bin/activate
10
- pip install -e ".[dev]"
10
+ pip install -r requirements-build.txt
11
+ pip install -r requirements-dev.txt -c constraints.txt
11
12
  pre-commit install
12
13
  ```
13
14
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unifi-network-maps
3
- Version: 1.3.1
3
+ Version: 1.4.1
4
4
  Summary: Dynamic UniFi -> network maps in mermaid or svg
5
5
  Author: Merlijn
6
6
  License-Expression: MIT
@@ -14,22 +14,21 @@ Classifier: Intended Audience :: System Administrators
14
14
  Classifier: Operating System :: OS Independent
15
15
  Classifier: Programming Language :: Python :: 3
16
16
  Classifier: Programming Language :: Python :: 3 :: Only
17
- Classifier: Programming Language :: Python :: 3.10
18
- Classifier: Programming Language :: Python :: 3.11
19
- Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
20
18
  Classifier: Topic :: Documentation
21
19
  Classifier: Topic :: System :: Networking
22
- Requires-Python: >=3.10
20
+ Requires-Python: >=3.13
23
21
  Description-Content-Type: text/markdown
24
22
  License-File: LICENSE
25
- Requires-Dist: unifi-controller-api
26
- Requires-Dist: python-dotenv
27
- Requires-Dist: PyYAML
23
+ Requires-Dist: unifi-controller-api==0.3.2
24
+ Requires-Dist: python-dotenv==1.2.1
25
+ Requires-Dist: PyYAML==6.0.3
28
26
  Provides-Extra: dev
29
- Requires-Dist: pre-commit; extra == "dev"
30
- Requires-Dist: pytest; extra == "dev"
31
- Requires-Dist: pytest-cov; extra == "dev"
32
- Requires-Dist: ruff; extra == "dev"
27
+ Requires-Dist: Faker==40.1.0; extra == "dev"
28
+ Requires-Dist: pre-commit==4.5.1; extra == "dev"
29
+ Requires-Dist: pytest==9.0.2; extra == "dev"
30
+ Requires-Dist: pytest-cov==7.0.0; extra == "dev"
31
+ Requires-Dist: ruff==0.14.10; extra == "dev"
33
32
  Dynamic: license-file
34
33
 
35
34
  # unifi-network-maps
@@ -38,13 +37,14 @@ Dynamic UniFi -> Mermaid network maps generated from LLDP topology.
38
37
 
39
38
  ## Setup
40
39
 
41
- - Python >= 3.10
40
+ - Python >= 3.13
42
41
  - Virtualenv required
43
42
 
44
43
  ```bash
45
44
  python -m venv .venv
46
45
  source .venv/bin/activate
47
- pip install -e .
46
+ pip install -r requirements-build.txt
47
+ pip install -e . -c constraints.txt
48
48
  ```
49
49
 
50
50
  Local install (non-editable):
@@ -101,6 +101,15 @@ Isometric SVG output:
101
101
 
102
102
  ```bash
103
103
  unifi-network-maps --format svg-iso --output ./network.svg
104
+
105
+ # Single-page MkDocs output (ports included, no clients)
106
+ unifi-network-maps --format mkdocs --output ./docs/unifi-network.md
107
+
108
+ # MkDocs output (map + legend + gateway/switch port tables)
109
+ unifi-network-maps --format mkdocs --output ./docs/unifi-network.md
110
+
111
+ # Include wired clients in the port tables
112
+ unifi-network-maps --format mkdocs --include-clients --output ./docs/unifi-network.md
104
113
  ```
105
114
 
106
115
  SVG size overrides:
@@ -118,12 +127,61 @@ Legend only:
118
127
  unifi-network-maps --legend-only --stdout
119
128
  ```
120
129
 
130
+ ## Examples (mock data)
131
+
132
+ These examples are generated from `examples/mock_data.json` (safe, anonymized fixture).
133
+ Mock generation requires dev dependencies (`pip install -r requirements-dev.txt -c constraints.txt`).
134
+ Regenerate the fixture + SVG with `make mock-data`.
135
+
136
+ Generate mock data (dev-only, uses Faker):
137
+
138
+ ```bash
139
+ unifi-network-maps --generate-mock examples/mock_data.json --mock-seed 1337
140
+ ```
141
+
142
+ Generate the isometric SVG:
143
+
144
+ ```bash
145
+ unifi-network-maps --mock-data examples/mock_data.json \
146
+ --include-ports --include-clients --format svg-iso \
147
+ --output examples/output/network_ports_clients_iso.svg
148
+ ```
149
+
150
+ ![Isometric network example](examples/output/network_ports_clients_iso.svg)
151
+
152
+ Mermaid example with ports:
153
+
154
+ ```mermaid
155
+ graph TB
156
+ core_switch["Core Switch"] ---|"Core Switch: Port 7 (AP Attic) <-> AP Attic: Port 1 (Core Switch)"| ap_attic["AP Attic"];
157
+ core_switch["Core Switch"] ---|"Core Switch: Port 3 (AP Living Room) <-> AP Living Room: Port 1 (Core Switch)"| ap_living_room["AP Living Room"];
158
+ cloud_gateway["Cloud Gateway"] ---|"Cloud Gateway: Port 9 (Core Switch) <-> Core Switch: Port 1 (Cloud Gateway)"| core_switch["Core Switch"];
159
+ class cloud_gateway node_gateway;
160
+ class core_switch node_switch;
161
+ class ap_living_room node_ap;
162
+ class ap_attic node_ap;
163
+ classDef node_gateway fill:#ffe3b3,stroke:#d98300,stroke-width:1px;
164
+ classDef node_switch fill:#d6ecff,stroke:#3a7bd5,stroke-width:1px;
165
+ classDef node_ap fill:#d7f5e7,stroke:#27ae60,stroke-width:1px;
166
+ classDef node_client fill:#f2e5ff,stroke:#7f3fbf,stroke-width:1px;
167
+ classDef node_other fill:#eeeeee,stroke:#8f8f8f,stroke-width:1px;
168
+ classDef node_legend font-size:10px;
169
+ linkStyle 0 stroke:#1e88e5,stroke-width:2px,arrowhead:none;
170
+ linkStyle 1 stroke:#1e88e5,stroke-width:2px,arrowhead:none;
171
+ ```
172
+
121
173
  ## Local install check
122
174
 
123
175
  ```bash
124
176
  pip install .
125
177
  ```
126
178
 
179
+ ## Dev
180
+
181
+ ```bash
182
+ pip install -r requirements-dev.txt -c constraints.txt
183
+ ```
184
+
127
185
  ## Release
128
186
 
129
187
  Build and upload to PyPI:
@@ -164,6 +222,14 @@ The CLI groups options by category (`Source`, `Functional`, `Mermaid`, `SVG`, `O
164
222
  Source:
165
223
  - `--site`: override `UNIFI_SITE`.
166
224
  - `--env-file`: load environment variables from a specific `.env` file.
225
+ - `--mock-data`: use mock data JSON instead of the UniFi API.
226
+ Mock:
227
+ - `--generate-mock`: write mock data JSON and exit.
228
+ - `--mock-seed`: seed for deterministic mock generation.
229
+ - `--mock-switches`: number of switches to generate.
230
+ - `--mock-aps`: number of access points to generate.
231
+ - `--mock-wired-clients`: number of wired clients to generate.
232
+ - `--mock-wireless-clients`: number of wireless clients to generate.
167
233
 
168
234
  Functional:
169
235
  - `--include-ports`: show port labels (Mermaid shows both ends; SVG shows compact labels).
@@ -174,6 +240,8 @@ Functional:
174
240
  Mermaid:
175
241
  - `--direction LR|TB`: diagram direction for Mermaid (default TB).
176
242
  - `--group-by-type`: group nodes by gateway/switch/AP in Mermaid subgraphs.
243
+ - `--legend-scale`: scale legend font/link sizes for Mermaid outputs (default 1.0).
244
+ - `--legend-style auto|compact|diagram`: legend rendering mode (auto uses compact for mkdocs).
177
245
  - `--legend-only`: render just the legend as a separate Mermaid graph (Mermaid only).
178
246
 
179
247
  SVG:
@@ -181,9 +249,10 @@ SVG:
181
249
  - `--theme-file`: load a YAML theme for Mermaid + SVG colors (see `examples/theme.yaml` and `examples/theme-dark.yaml`).
182
250
 
183
251
  Output:
184
- - `--format mermaid|svg|svg-iso|lldp-md`: output format (default mermaid).
252
+ - `--format mermaid|svg|svg-iso|lldp-md|mkdocs`: output format (default mermaid).
185
253
  - `--stdout`: write output to stdout.
186
254
  - `--markdown`: wrap Mermaid output in a code fence.
255
+ - `--mkdocs-sidebar-legend`: write assets to place the compact legend in the MkDocs right sidebar.
187
256
 
188
257
  Debug:
189
258
  - `--debug-dump`: dump gateway + sample devices to stderr for debugging.
@@ -228,5 +297,10 @@ svg:
228
297
  to: "#b6dcff"
229
298
  ```
230
299
 
300
+ ## MkDocs Material example
301
+
302
+ See `examples/mkdocs/` for a ready-to-use setup that renders Mermaid diagrams
303
+ with Material for MkDocs, including a sample `unifi-network` page and legend.
304
+
231
305
  The built-in themes live at `src/unifi_network_maps/assets/themes/default.yaml` and
232
306
  `src/unifi_network_maps/assets/themes/dark.yaml`.
@@ -4,13 +4,14 @@ Dynamic UniFi -> Mermaid network maps generated from LLDP topology.
4
4
 
5
5
  ## Setup
6
6
 
7
- - Python >= 3.10
7
+ - Python >= 3.13
8
8
  - Virtualenv required
9
9
 
10
10
  ```bash
11
11
  python -m venv .venv
12
12
  source .venv/bin/activate
13
- pip install -e .
13
+ pip install -r requirements-build.txt
14
+ pip install -e . -c constraints.txt
14
15
  ```
15
16
 
16
17
  Local install (non-editable):
@@ -67,6 +68,15 @@ Isometric SVG output:
67
68
 
68
69
  ```bash
69
70
  unifi-network-maps --format svg-iso --output ./network.svg
71
+
72
+ # Single-page MkDocs output (ports included, no clients)
73
+ unifi-network-maps --format mkdocs --output ./docs/unifi-network.md
74
+
75
+ # MkDocs output (map + legend + gateway/switch port tables)
76
+ unifi-network-maps --format mkdocs --output ./docs/unifi-network.md
77
+
78
+ # Include wired clients in the port tables
79
+ unifi-network-maps --format mkdocs --include-clients --output ./docs/unifi-network.md
70
80
  ```
71
81
 
72
82
  SVG size overrides:
@@ -84,12 +94,61 @@ Legend only:
84
94
  unifi-network-maps --legend-only --stdout
85
95
  ```
86
96
 
97
+ ## Examples (mock data)
98
+
99
+ These examples are generated from `examples/mock_data.json` (safe, anonymized fixture).
100
+ Mock generation requires dev dependencies (`pip install -r requirements-dev.txt -c constraints.txt`).
101
+ Regenerate the fixture + SVG with `make mock-data`.
102
+
103
+ Generate mock data (dev-only, uses Faker):
104
+
105
+ ```bash
106
+ unifi-network-maps --generate-mock examples/mock_data.json --mock-seed 1337
107
+ ```
108
+
109
+ Generate the isometric SVG:
110
+
111
+ ```bash
112
+ unifi-network-maps --mock-data examples/mock_data.json \
113
+ --include-ports --include-clients --format svg-iso \
114
+ --output examples/output/network_ports_clients_iso.svg
115
+ ```
116
+
117
+ ![Isometric network example](examples/output/network_ports_clients_iso.svg)
118
+
119
+ Mermaid example with ports:
120
+
121
+ ```mermaid
122
+ graph TB
123
+ core_switch["Core Switch"] ---|"Core Switch: Port 7 (AP Attic) <-> AP Attic: Port 1 (Core Switch)"| ap_attic["AP Attic"];
124
+ core_switch["Core Switch"] ---|"Core Switch: Port 3 (AP Living Room) <-> AP Living Room: Port 1 (Core Switch)"| ap_living_room["AP Living Room"];
125
+ cloud_gateway["Cloud Gateway"] ---|"Cloud Gateway: Port 9 (Core Switch) <-> Core Switch: Port 1 (Cloud Gateway)"| core_switch["Core Switch"];
126
+ class cloud_gateway node_gateway;
127
+ class core_switch node_switch;
128
+ class ap_living_room node_ap;
129
+ class ap_attic node_ap;
130
+ classDef node_gateway fill:#ffe3b3,stroke:#d98300,stroke-width:1px;
131
+ classDef node_switch fill:#d6ecff,stroke:#3a7bd5,stroke-width:1px;
132
+ classDef node_ap fill:#d7f5e7,stroke:#27ae60,stroke-width:1px;
133
+ classDef node_client fill:#f2e5ff,stroke:#7f3fbf,stroke-width:1px;
134
+ classDef node_other fill:#eeeeee,stroke:#8f8f8f,stroke-width:1px;
135
+ classDef node_legend font-size:10px;
136
+ linkStyle 0 stroke:#1e88e5,stroke-width:2px,arrowhead:none;
137
+ linkStyle 1 stroke:#1e88e5,stroke-width:2px,arrowhead:none;
138
+ ```
139
+
87
140
  ## Local install check
88
141
 
89
142
  ```bash
90
143
  pip install .
91
144
  ```
92
145
 
146
+ ## Dev
147
+
148
+ ```bash
149
+ pip install -r requirements-dev.txt -c constraints.txt
150
+ ```
151
+
93
152
  ## Release
94
153
 
95
154
  Build and upload to PyPI:
@@ -130,6 +189,14 @@ The CLI groups options by category (`Source`, `Functional`, `Mermaid`, `SVG`, `O
130
189
  Source:
131
190
  - `--site`: override `UNIFI_SITE`.
132
191
  - `--env-file`: load environment variables from a specific `.env` file.
192
+ - `--mock-data`: use mock data JSON instead of the UniFi API.
193
+ Mock:
194
+ - `--generate-mock`: write mock data JSON and exit.
195
+ - `--mock-seed`: seed for deterministic mock generation.
196
+ - `--mock-switches`: number of switches to generate.
197
+ - `--mock-aps`: number of access points to generate.
198
+ - `--mock-wired-clients`: number of wired clients to generate.
199
+ - `--mock-wireless-clients`: number of wireless clients to generate.
133
200
 
134
201
  Functional:
135
202
  - `--include-ports`: show port labels (Mermaid shows both ends; SVG shows compact labels).
@@ -140,6 +207,8 @@ Functional:
140
207
  Mermaid:
141
208
  - `--direction LR|TB`: diagram direction for Mermaid (default TB).
142
209
  - `--group-by-type`: group nodes by gateway/switch/AP in Mermaid subgraphs.
210
+ - `--legend-scale`: scale legend font/link sizes for Mermaid outputs (default 1.0).
211
+ - `--legend-style auto|compact|diagram`: legend rendering mode (auto uses compact for mkdocs).
143
212
  - `--legend-only`: render just the legend as a separate Mermaid graph (Mermaid only).
144
213
 
145
214
  SVG:
@@ -147,9 +216,10 @@ SVG:
147
216
  - `--theme-file`: load a YAML theme for Mermaid + SVG colors (see `examples/theme.yaml` and `examples/theme-dark.yaml`).
148
217
 
149
218
  Output:
150
- - `--format mermaid|svg|svg-iso|lldp-md`: output format (default mermaid).
219
+ - `--format mermaid|svg|svg-iso|lldp-md|mkdocs`: output format (default mermaid).
151
220
  - `--stdout`: write output to stdout.
152
221
  - `--markdown`: wrap Mermaid output in a code fence.
222
+ - `--mkdocs-sidebar-legend`: write assets to place the compact legend in the MkDocs right sidebar.
153
223
 
154
224
  Debug:
155
225
  - `--debug-dump`: dump gateway + sample devices to stderr for debugging.
@@ -194,5 +264,10 @@ svg:
194
264
  to: "#b6dcff"
195
265
  ```
196
266
 
267
+ ## MkDocs Material example
268
+
269
+ See `examples/mkdocs/` for a ready-to-use setup that renders Mermaid diagrams
270
+ with Material for MkDocs, including a sample `unifi-network` page and legend.
271
+
197
272
  The built-in themes live at `src/unifi_network_maps/assets/themes/default.yaml` and
198
273
  `src/unifi_network_maps/assets/themes/dark.yaml`.
@@ -10,7 +10,8 @@
10
10
  ```
11
11
  3) Build the package:
12
12
  ```bash
13
- python -m pip install build twine
13
+ python -m pip install -r requirements-build.txt
14
+ python -m pip install build twine -c constraints.txt
14
15
  python -m build
15
16
  ```
16
17
  4) Inspect the artifacts:
@@ -1,13 +1,13 @@
1
1
  [build-system]
2
- requires = ["setuptools>=68", "wheel"]
2
+ requires = ["setuptools==80.9.0", "wheel==0.45.1"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "unifi-network-maps"
7
- version = "1.3.1"
7
+ version = "1.4.1"
8
8
  description = "Dynamic UniFi -> network maps in mermaid or svg"
9
9
  readme = "README.md"
10
- requires-python = ">=3.10"
10
+ requires-python = ">=3.13"
11
11
  license = "MIT"
12
12
  license-files = ["LICENSE"]
13
13
  authors = [{ name = "Merlijn" }]
@@ -18,16 +18,14 @@ classifiers = [
18
18
  "Operating System :: OS Independent",
19
19
  "Programming Language :: Python :: 3",
20
20
  "Programming Language :: Python :: 3 :: Only",
21
- "Programming Language :: Python :: 3.10",
22
- "Programming Language :: Python :: 3.11",
23
- "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
24
22
  "Topic :: Documentation",
25
23
  "Topic :: System :: Networking",
26
24
  ]
27
25
  dependencies = [
28
- "unifi-controller-api",
29
- "python-dotenv",
30
- "PyYAML",
26
+ "unifi-controller-api==0.3.2",
27
+ "python-dotenv==1.2.1",
28
+ "PyYAML==6.0.3",
31
29
  ]
32
30
 
33
31
  [project.urls]
@@ -38,10 +36,11 @@ Changelog = "https://github.com/merlijntishauser/unifi-network-maps/blob/main/CH
38
36
 
39
37
  [project.optional-dependencies]
40
38
  dev = [
41
- "pre-commit",
42
- "pytest",
43
- "pytest-cov",
44
- "ruff",
39
+ "Faker==40.1.0",
40
+ "pre-commit==4.5.1",
41
+ "pytest==9.0.2",
42
+ "pytest-cov==7.0.0",
43
+ "ruff==0.14.10",
45
44
  ]
46
45
 
47
46
  [project.scripts]
@@ -49,7 +48,7 @@ unifi-network-maps = "unifi_network_maps.cli:main"
49
48
 
50
49
  [tool.ruff]
51
50
  line-length = 100
52
- target-version = "py310"
51
+ target-version = "py313"
53
52
 
54
53
  [tool.ruff.lint]
55
54
  select = ["E", "F", "I", "B", "UP"]
@@ -61,13 +60,17 @@ line-ending = "lf"
61
60
  [tool.pytest.ini_options]
62
61
  testpaths = ["tests"]
63
62
  norecursedirs = ["src/unifi_network_maps/assets"]
63
+ # Remove --cov from here so it doesn't conflict with PyCharm's runner
64
+ addopts = "-ra"
64
65
 
65
66
  [tool.coverage.run]
66
67
  branch = true
67
68
  source = ["unifi_network_maps"]
68
- omit = [
69
- "src/unifi_network_maps/assets/*",
70
- "src/unifi_network_maps/assets/**",
69
+
70
+ [tool.coverage.paths]
71
+ source = [
72
+ "src/unifi_network_maps",
73
+ "*/site-packages/unifi_network_maps",
71
74
  ]
72
75
 
73
76
  [tool.coverage.report]
@@ -0,0 +1 @@
1
+ __version__ = "1.4.1"
@@ -0,0 +1,8 @@
1
+ """Module entrypoint for python -m unifi_network_maps."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .cli.main import main
6
+
7
+ if __name__ == "__main__":
8
+ raise SystemExit(main())
@@ -39,30 +39,83 @@ def _cache_key(*parts: str) -> str:
39
39
 
40
40
 
41
41
  def _load_cache(path: Path, ttl_seconds: int) -> object | None:
42
- if ttl_seconds <= 0 or not path.exists():
42
+ data, age = _load_cache_with_age(path)
43
+ if data is None:
43
44
  return None
45
+ if ttl_seconds <= 0:
46
+ return None
47
+ if age is None or age > ttl_seconds:
48
+ return None
49
+ return data
50
+
51
+
52
+ def _load_cache_with_age(path: Path) -> tuple[object | None, float | None]:
53
+ if not path.exists():
54
+ return None, None
44
55
  try:
45
56
  payload = pickle.loads(path.read_bytes())
46
57
  except Exception as exc:
47
58
  logger.debug("Failed to read cache %s: %s", path, exc)
48
- return None
59
+ return None, None
49
60
  timestamp = payload.get("timestamp")
50
61
  if not isinstance(timestamp, int | float):
51
- return None
52
- if time.time() - timestamp > ttl_seconds:
53
- return None
54
- return payload.get("data")
62
+ return None, None
63
+ data = payload.get("data")
64
+ if not isinstance(data, list):
65
+ logger.debug("Cached payload at %s is not a list", path)
66
+ return None, None
67
+ return data, time.time() - timestamp
55
68
 
56
69
 
57
70
  def _save_cache(path: Path, data: object) -> None:
58
71
  try:
59
72
  path.parent.mkdir(parents=True, exist_ok=True)
60
73
  payload = {"timestamp": time.time(), "data": data}
61
- path.write_bytes(pickle.dumps(payload))
74
+ tmp_path = path.with_suffix(path.suffix + ".tmp")
75
+ tmp_path.write_bytes(pickle.dumps(payload))
76
+ tmp_path.replace(path)
62
77
  except Exception as exc:
63
78
  logger.debug("Failed to write cache %s: %s", path, exc)
64
79
 
65
80
 
81
+ def _retry_attempts() -> int:
82
+ value = os.environ.get("UNIFI_RETRY_ATTEMPTS", "").strip()
83
+ if not value:
84
+ return 2
85
+ if value.isdigit():
86
+ return max(1, int(value))
87
+ logger.warning("Invalid UNIFI_RETRY_ATTEMPTS value: %s", value)
88
+ return 2
89
+
90
+
91
+ def _retry_backoff_seconds() -> float:
92
+ value = os.environ.get("UNIFI_RETRY_BACKOFF_SECONDS", "").strip()
93
+ if not value:
94
+ return 0.5
95
+ try:
96
+ return max(0.0, float(value))
97
+ except ValueError:
98
+ logger.warning("Invalid UNIFI_RETRY_BACKOFF_SECONDS value: %s", value)
99
+ return 0.5
100
+
101
+
102
+ def _call_with_retries(operation: str, func) -> object:
103
+ attempts = _retry_attempts()
104
+ backoff = _retry_backoff_seconds()
105
+ last_exc: Exception | None = None
106
+ for attempt in range(1, attempts + 1):
107
+ try:
108
+ return func()
109
+ except Exception as exc: # noqa: BLE001 - surface full error after retries
110
+ last_exc = exc
111
+ logger.warning("Failed %s attempt %d/%d: %s", operation, attempt, attempts, exc)
112
+ if attempt < attempts and backoff > 0:
113
+ time.sleep(backoff * attempt)
114
+ if last_exc:
115
+ raise last_exc
116
+ raise RuntimeError(f"Failed {operation}")
117
+
118
+
66
119
  def _init_controller(config: Config, *, is_udm_pro: bool) -> UnifiController:
67
120
  from unifi_controller_api import UnifiController
68
121
 
@@ -91,6 +144,7 @@ def fetch_devices(
91
144
  ttl_seconds = _cache_ttl_seconds()
92
145
  cache_path = _cache_dir() / f"devices_{_cache_key(config.url, site_name, str(detailed))}.pkl"
93
146
  cached = _load_cache(cache_path, ttl_seconds)
147
+ stale_cached, cache_age = _load_cache_with_age(cache_path)
94
148
  if cached is not None:
95
149
  logger.info("Using cached devices (%d)", len(cached))
96
150
  return cached
@@ -101,7 +155,20 @@ def fetch_devices(
101
155
  logger.info("UDM Pro authentication failed, retrying legacy auth")
102
156
  controller = _init_controller(config, is_udm_pro=False)
103
157
 
104
- devices = controller.get_unifi_site_device(site_name=site_name, detailed=detailed, raw=False)
158
+ def _fetch() -> list[object]:
159
+ return controller.get_unifi_site_device(site_name=site_name, detailed=detailed, raw=False)
160
+
161
+ try:
162
+ devices = _call_with_retries("device fetch", _fetch)
163
+ except Exception as exc: # noqa: BLE001 - fallback to cache
164
+ if stale_cached is not None:
165
+ logger.warning(
166
+ "Device fetch failed; using stale cache (%ds old): %s",
167
+ int(cache_age or 0),
168
+ exc,
169
+ )
170
+ return stale_cached
171
+ raise
105
172
  _save_cache(cache_path, devices)
106
173
  logger.info("Fetched %d devices", len(devices))
107
174
  return devices
@@ -118,6 +185,7 @@ def fetch_clients(config: Config, *, site: str | None = None) -> Iterable[object
118
185
  ttl_seconds = _cache_ttl_seconds()
119
186
  cache_path = _cache_dir() / f"clients_{_cache_key(config.url, site_name)}.pkl"
120
187
  cached = _load_cache(cache_path, ttl_seconds)
188
+ stale_cached, cache_age = _load_cache_with_age(cache_path)
121
189
  if cached is not None:
122
190
  logger.info("Using cached clients (%d)", len(cached))
123
191
  return cached
@@ -128,7 +196,20 @@ def fetch_clients(config: Config, *, site: str | None = None) -> Iterable[object
128
196
  logger.info("UDM Pro authentication failed, retrying legacy auth")
129
197
  controller = _init_controller(config, is_udm_pro=False)
130
198
 
131
- clients = controller.get_unifi_site_client(site_name=site_name, raw=True)
199
+ def _fetch() -> list[object]:
200
+ return controller.get_unifi_site_client(site_name=site_name, raw=True)
201
+
202
+ try:
203
+ clients = _call_with_retries("client fetch", _fetch)
204
+ except Exception as exc: # noqa: BLE001 - fallback to cache
205
+ if stale_cached is not None:
206
+ logger.warning(
207
+ "Client fetch failed; using stale cache (%ds old): %s",
208
+ int(cache_age or 0),
209
+ exc,
210
+ )
211
+ return stale_cached
212
+ raise
132
213
  _save_cache(cache_path, clients)
133
214
  logger.info("Fetched %d clients", len(clients))
134
215
  return clients