unifi-network-maps 1.4.3__tar.gz → 1.4.5__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 (132) hide show
  1. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/CHANGELOG.md +14 -2
  2. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/CONTRIBUTING.md +2 -1
  3. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/PKG-INFO +2 -1
  4. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/pyproject.toml +3 -1
  5. unifi_network_maps-1.4.5/src/unifi_network_maps/__init__.py +1 -0
  6. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/themes/dark.yaml +2 -2
  7. unifi_network_maps-1.4.5/src/unifi_network_maps/cli/__init__.py +5 -0
  8. unifi_network_maps-1.4.5/src/unifi_network_maps/cli/args.py +166 -0
  9. unifi_network_maps-1.4.5/src/unifi_network_maps/cli/main.py +130 -0
  10. unifi_network_maps-1.4.5/src/unifi_network_maps/cli/render.py +255 -0
  11. unifi_network_maps-1.4.5/src/unifi_network_maps/cli/runtime.py +157 -0
  12. unifi_network_maps-1.4.5/src/unifi_network_maps/io/mkdocs_assets.py +21 -0
  13. unifi_network_maps-1.4.5/src/unifi_network_maps/io/mock_generate.py +7 -0
  14. unifi_network_maps-1.4.3/src/unifi_network_maps/io/mock_generate.py → unifi_network_maps-1.4.5/src/unifi_network_maps/model/mock.py +57 -49
  15. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/render/device_ports_md.py +44 -27
  16. unifi_network_maps-1.4.5/src/unifi_network_maps/render/legend.py +30 -0
  17. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/render/lldp_md.py +81 -60
  18. unifi_network_maps-1.4.5/src/unifi_network_maps/render/markdown_tables.py +21 -0
  19. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/render/mermaid.py +72 -85
  20. unifi_network_maps-1.4.5/src/unifi_network_maps/render/mkdocs.py +167 -0
  21. unifi_network_maps-1.4.5/src/unifi_network_maps/render/templates/device_port_block.md.j2 +5 -0
  22. unifi_network_maps-1.4.5/src/unifi_network_maps/render/templates/legend_compact.html.j2 +14 -0
  23. unifi_network_maps-1.4.5/src/unifi_network_maps/render/templates/lldp_device_section.md.j2 +15 -0
  24. unifi_network_maps-1.4.5/src/unifi_network_maps/render/templates/markdown_section.md.j2 +3 -0
  25. unifi_network_maps-1.4.5/src/unifi_network_maps/render/templates/mermaid_legend.mmd.j2 +30 -0
  26. unifi_network_maps-1.4.5/src/unifi_network_maps/render/templates/mkdocs_document.md.j2 +23 -0
  27. unifi_network_maps-1.4.5/src/unifi_network_maps/render/templates/mkdocs_dual_theme_style.html.j2 +8 -0
  28. unifi_network_maps-1.4.5/src/unifi_network_maps/render/templates/mkdocs_html_block.html.j2 +2 -0
  29. unifi_network_maps-1.4.5/src/unifi_network_maps/render/templates/mkdocs_legend.css.j2 +29 -0
  30. unifi_network_maps-1.4.5/src/unifi_network_maps/render/templates/mkdocs_legend.js.j2 +18 -0
  31. unifi_network_maps-1.4.5/src/unifi_network_maps/render/templates/mkdocs_mermaid_block.md.j2 +4 -0
  32. unifi_network_maps-1.4.5/src/unifi_network_maps/render/templating.py +19 -0
  33. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps.egg-info/PKG-INFO +2 -1
  34. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps.egg-info/SOURCES.txt +20 -0
  35. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps.egg-info/requires.txt +1 -0
  36. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/tests/test_cli.py +95 -77
  37. unifi_network_maps-1.4.3/src/unifi_network_maps/__init__.py +0 -1
  38. unifi_network_maps-1.4.3/src/unifi_network_maps/cli/__init__.py +0 -41
  39. unifi_network_maps-1.4.3/src/unifi_network_maps/cli/main.py +0 -859
  40. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/LICENSE +0 -0
  41. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/LICENSES.md +0 -0
  42. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/MANIFEST.in +0 -0
  43. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/README.md +0 -0
  44. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/RELEASING.md +0 -0
  45. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/SECURITY.md +0 -0
  46. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/setup.cfg +0 -0
  47. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/__main__.py +0 -0
  48. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/adapters/__init__.py +0 -0
  49. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/adapters/config.py +0 -0
  50. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/adapters/unifi.py +0 -0
  51. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/__init__.py +0 -0
  52. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/__init__.py +0 -0
  53. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/access-point.svg +0 -0
  54. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/ISOPACKS_LICENSE +0 -0
  55. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/block.svg +0 -0
  56. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/cache.svg +0 -0
  57. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/cardterminal.svg +0 -0
  58. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/cloud.svg +0 -0
  59. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/cronjob.svg +0 -0
  60. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/cube.svg +0 -0
  61. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/desktop.svg +0 -0
  62. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/diamond.svg +0 -0
  63. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/dns.svg +0 -0
  64. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/document.svg +0 -0
  65. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/firewall.svg +0 -0
  66. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/function-module.svg +0 -0
  67. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/image.svg +0 -0
  68. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/laptop.svg +0 -0
  69. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/loadbalancer.svg +0 -0
  70. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/lock.svg +0 -0
  71. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/mail.svg +0 -0
  72. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/mailmultiple.svg +0 -0
  73. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/mobiledevice.svg +0 -0
  74. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/office.svg +0 -0
  75. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/package-module.svg +0 -0
  76. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/paymentcard.svg +0 -0
  77. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/plane.svg +0 -0
  78. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/printer.svg +0 -0
  79. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/pyramid.svg +0 -0
  80. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/queue.svg +0 -0
  81. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/router.svg +0 -0
  82. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/server.svg +0 -0
  83. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/speech.svg +0 -0
  84. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/sphere.svg +0 -0
  85. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/storage.svg +0 -0
  86. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/switch-module.svg +0 -0
  87. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/tower.svg +0 -0
  88. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/truck-2.svg +0 -0
  89. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/truck.svg +0 -0
  90. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/user.svg +0 -0
  91. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/isometric/vm.svg +0 -0
  92. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/laptop.svg +0 -0
  93. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/router-network.svg +0 -0
  94. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/server-network.svg +0 -0
  95. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/icons/server.svg +0 -0
  96. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/assets/themes/default.yaml +0 -0
  97. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/cli/__main__.py +0 -0
  98. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/io/__init__.py +0 -0
  99. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/io/debug.py +0 -0
  100. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/io/export.py +0 -0
  101. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/io/mock_data.py +0 -0
  102. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/model/__init__.py +0 -0
  103. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/model/labels.py +0 -0
  104. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/model/lldp.py +0 -0
  105. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/model/ports.py +0 -0
  106. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/model/topology.py +0 -0
  107. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/render/__init__.py +0 -0
  108. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/render/mermaid_theme.py +0 -0
  109. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/render/svg.py +0 -0
  110. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/render/svg_theme.py +0 -0
  111. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps/render/theme.py +0 -0
  112. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps.egg-info/dependency_links.txt +0 -0
  113. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps.egg-info/entry_points.txt +0 -0
  114. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/src/unifi_network_maps.egg-info/top_level.txt +0 -0
  115. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/tests/test_clients.py +0 -0
  116. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/tests/test_config.py +0 -0
  117. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/tests/test_contract_unifi.py +0 -0
  118. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/tests/test_contract_unifi_live.py +0 -0
  119. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/tests/test_debug.py +0 -0
  120. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/tests/test_device_ports_md.py +0 -0
  121. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/tests/test_export.py +0 -0
  122. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/tests/test_groups.py +0 -0
  123. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/tests/test_labels.py +0 -0
  124. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/tests/test_lldp.py +0 -0
  125. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/tests/test_lldp_md.py +0 -0
  126. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/tests/test_mermaid.py +0 -0
  127. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/tests/test_mock_generate.py +0 -0
  128. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/tests/test_svg.py +0 -0
  129. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/tests/test_svg_iso.py +0 -0
  130. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/tests/test_theme.py +0 -0
  131. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/tests/test_topology.py +0 -0
  132. {unifi_network_maps-1.4.3 → unifi_network_maps-1.4.5}/tests/test_unifi.py +0 -0
@@ -2,8 +2,20 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- ## v1.4.3 - (unreleased)
6
- - planned: POC for HA integration
5
+
6
+ ## 1.4.5 - (2026-01-11)
7
+ - Refactored CLI orchestration into focused CLI/render/runtime modules.
8
+ - Extracted MkDocs rendering and sidebar asset output into dedicated modules.
9
+ - Moved mock generation into the model layer with a thin IO facade.
10
+ - Centralized legend rendering helpers and shared markdown table utilities.
11
+ - Added Jinja2 templating for MkDocs output, Mermaid legend blocks, and Markdown sections.
12
+ - Moved MkDocs sidebar assets and legend HTML blocks into reusable templates.
13
+ - Enabled Jinja2 autoescaping for HTML templates and marked trusted HTML blocks safe.
14
+
15
+ ## v1.4.4 - (2026-01-10)
16
+ - Improved dark theme Mermaid readability (labels + link borders).
17
+ - Fixed MkDocs sidebar legend duplication with dual-theme output.
18
+ - Added smoke tests for dual-theme MkDocs sidebar legend output.
7
19
 
8
20
  ## v1.4.2 - 2026-01-10
9
21
  - Added static code analysis and stricter type-checking
@@ -43,6 +43,7 @@ Notes:
43
43
  - Contract tests use fixtures in `tests/test_contract_unifi.py` and run in CI.
44
44
  - Live contract tests require `UNIFI_CONTRACT_LIVE=1` plus UniFi env vars.
45
45
  - BDD tests live in `features/` and run via `behave` (included in `make ci`).
46
+ - Render helper structure is documented in `src/unifi_network_maps/render/README.md`.
46
47
 
47
48
  ## Release
48
49
 
@@ -86,4 +87,4 @@ See `LICENSES.md` for third-party license info.
86
87
  - A clean render pipeline that can power multiple export targets.
87
88
 
88
89
  So we’re not duplicating a packaged workflow; we’re combining data + topology modeling + diagram output + documentation/export in a way I haven’t seen in one place.
89
-
90
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unifi-network-maps
3
- Version: 1.4.3
3
+ Version: 1.4.5
4
4
  Summary: Dynamic UniFi -> network maps in mermaid or svg
5
5
  Author: Merlijn
6
6
  License-Expression: MIT
@@ -23,6 +23,7 @@ License-File: LICENSE
23
23
  Requires-Dist: unifi-controller-api==0.3.2
24
24
  Requires-Dist: python-dotenv==1.2.1
25
25
  Requires-Dist: PyYAML==6.0.3
26
+ Requires-Dist: Jinja2==3.1.6
26
27
  Provides-Extra: dev
27
28
  Requires-Dist: Faker==40.1.0; extra == "dev"
28
29
  Requires-Dist: behave==1.3.3; extra == "dev"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "unifi-network-maps"
7
- version = "1.4.3"
7
+ version = "1.4.5"
8
8
  description = "Dynamic UniFi -> network maps in mermaid or svg"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -26,6 +26,7 @@ dependencies = [
26
26
  "unifi-controller-api==0.3.2",
27
27
  "python-dotenv==1.2.1",
28
28
  "PyYAML==6.0.3",
29
+ "Jinja2==3.1.6",
29
30
  ]
30
31
 
31
32
  [project.urls]
@@ -96,6 +97,7 @@ package-dir = {"" = "src"}
96
97
  "assets/icons/*.svg",
97
98
  "assets/icons/isometric/*.svg",
98
99
  "assets/icons/isometric/ISOPACKS_LICENSE",
100
+ "render/templates/*.j2",
99
101
  ]
100
102
 
101
103
  [tool.setuptools.packages.find]
@@ -0,0 +1 @@
1
+ __version__ = "1.4.5"
@@ -15,8 +15,8 @@ mermaid:
15
15
  other:
16
16
  fill: "#2a2a2a"
17
17
  stroke: "#9e9e9e"
18
- node_text: "#111111"
19
- edge_label_border: "#111111"
18
+ node_text: "#f5f5f5"
19
+ edge_label_border: "#e6e6e6"
20
20
  edge_label_border_width: 2
21
21
  poe_link: "#64b5f6"
22
22
  poe_link_width: 2
@@ -0,0 +1,5 @@
1
+ """CLI package facade."""
2
+
3
+ from .main import main
4
+
5
+ __all__ = ["main"]
@@ -0,0 +1,166 @@
1
+ """CLI argument definitions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+
8
+ def build_parser() -> argparse.ArgumentParser:
9
+ parser = argparse.ArgumentParser(
10
+ description="Generate network maps from UniFi LLDP data, as mermaid or SVG"
11
+ )
12
+ add_source_args(parser.add_argument_group("Source"))
13
+ add_mock_args(parser.add_argument_group("Mock"))
14
+ add_functional_args(parser.add_argument_group("Functional"))
15
+ add_mermaid_args(parser.add_argument_group("Mermaid"))
16
+ add_svg_args(parser.add_argument_group("SVG"))
17
+ add_general_render_args(parser.add_argument_group("Output"))
18
+ add_debug_args(parser.add_argument_group("Debug"))
19
+ return parser
20
+
21
+
22
+ def add_source_args(parser: argparse._ArgumentGroup) -> None:
23
+ parser.add_argument("--site", default=None, help="UniFi site name (overrides UNIFI_SITE)")
24
+ parser.add_argument(
25
+ "--env-file",
26
+ default=None,
27
+ help="Path to .env file (overrides default .env discovery)",
28
+ )
29
+ parser.add_argument(
30
+ "--mock-data",
31
+ default=None,
32
+ help="Path to mock data JSON (skips UniFi API calls)",
33
+ )
34
+
35
+
36
+ def add_mock_args(parser: argparse._ArgumentGroup) -> None:
37
+ parser.add_argument(
38
+ "--generate-mock",
39
+ default=None,
40
+ help="Write mock data JSON to the given path and exit",
41
+ )
42
+ parser.add_argument("--mock-seed", type=int, default=1337, help="Seed for mock generation")
43
+ parser.add_argument(
44
+ "--mock-switches",
45
+ type=int,
46
+ default=1,
47
+ help="Number of switches to generate (default: 1)",
48
+ )
49
+ parser.add_argument(
50
+ "--mock-aps",
51
+ type=int,
52
+ default=2,
53
+ help="Number of access points to generate (default: 2)",
54
+ )
55
+ parser.add_argument(
56
+ "--mock-wired-clients",
57
+ type=int,
58
+ default=2,
59
+ help="Number of wired clients to generate (default: 2)",
60
+ )
61
+ parser.add_argument(
62
+ "--mock-wireless-clients",
63
+ type=int,
64
+ default=2,
65
+ help="Number of wireless clients to generate (default: 2)",
66
+ )
67
+
68
+
69
+ def add_functional_args(parser: argparse._ArgumentGroup) -> None:
70
+ parser.add_argument("--include-ports", action="store_true", help="Include port labels in edges")
71
+ parser.add_argument(
72
+ "--include-clients",
73
+ action="store_true",
74
+ help="Include active clients as leaf nodes",
75
+ )
76
+ parser.add_argument(
77
+ "--client-scope",
78
+ choices=["wired", "wireless", "all"],
79
+ default="wired",
80
+ help="Client types to include (default: wired)",
81
+ )
82
+ parser.add_argument(
83
+ "--only-unifi", action="store_true", help="Only include neighbors that are UniFi devices"
84
+ )
85
+ parser.add_argument(
86
+ "--no-cache",
87
+ action="store_true",
88
+ help="Disable UniFi API cache reads and writes",
89
+ )
90
+
91
+
92
+ def add_mermaid_args(parser: argparse._ArgumentGroup) -> None:
93
+ parser.add_argument("--direction", default="TB", choices=["LR", "TB"], help="Mermaid direction")
94
+ parser.add_argument(
95
+ "--group-by-type",
96
+ action="store_true",
97
+ help="Group nodes by gateway/switch/ap in Mermaid subgraphs",
98
+ )
99
+ parser.add_argument(
100
+ "--legend-scale",
101
+ type=float,
102
+ default=1.0,
103
+ help="Scale legend font/link sizes for Mermaid output (default: 1.0)",
104
+ )
105
+ parser.add_argument(
106
+ "--legend-style",
107
+ default="auto",
108
+ choices=["auto", "compact", "diagram"],
109
+ help="Legend style (auto uses compact for mkdocs, diagram otherwise)",
110
+ )
111
+ parser.add_argument(
112
+ "--legend-only",
113
+ action="store_true",
114
+ help="Render only the legend as a separate Mermaid graph",
115
+ )
116
+
117
+
118
+ def add_svg_args(parser: argparse._ArgumentGroup) -> None:
119
+ parser.add_argument("--svg-width", type=int, default=None, help="SVG width override")
120
+ parser.add_argument("--svg-height", type=int, default=None, help="SVG height override")
121
+ parser.add_argument("--theme-file", default=None, help="Path to theme YAML file")
122
+
123
+
124
+ def add_general_render_args(parser: argparse._ArgumentGroup) -> None:
125
+ parser.add_argument(
126
+ "--format",
127
+ default="mermaid",
128
+ choices=["mermaid", "svg", "svg-iso", "lldp-md", "mkdocs"],
129
+ help="Output format",
130
+ )
131
+ parser.add_argument(
132
+ "--markdown",
133
+ action="store_true",
134
+ help="Wrap output in a Markdown mermaid code fence for notes tools like Obsidian",
135
+ )
136
+ parser.add_argument("--output", default=None, help="Output file path")
137
+ parser.add_argument("--stdout", action="store_true", help="Write output to stdout")
138
+ parser.add_argument(
139
+ "--mkdocs-sidebar-legend",
140
+ action="store_true",
141
+ help="For mkdocs output, write sidebar legend assets next to the output file",
142
+ )
143
+ parser.add_argument(
144
+ "--mkdocs-dual-theme",
145
+ action="store_true",
146
+ help="Render light/dark Mermaid blocks for MkDocs Material theme switching",
147
+ )
148
+ parser.add_argument(
149
+ "--mkdocs-timestamp-zone",
150
+ default="Europe/Amsterdam",
151
+ help="Timezone for mkdocs generated timestamp (use 'off' to disable)",
152
+ )
153
+
154
+
155
+ def add_debug_args(parser: argparse._ArgumentGroup) -> None:
156
+ parser.add_argument(
157
+ "--debug-dump",
158
+ action="store_true",
159
+ help="Dump gateway and sample device data to stderr for debugging",
160
+ )
161
+ parser.add_argument(
162
+ "--debug-sample",
163
+ type=int,
164
+ default=2,
165
+ help="Number of non-gateway devices to include in debug dump (default: 2)",
166
+ )
@@ -0,0 +1,130 @@
1
+ """CLI entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import logging
7
+
8
+ from ..adapters.config import Config
9
+ from ..io.export import write_output
10
+ from ..io.mock_data import load_mock_data
11
+ from ..render.legend import render_legend_only, resolve_legend_style
12
+ from ..render.theme import resolve_themes
13
+ from .args import build_parser
14
+ from .render import render_lldp_format, render_standard_format
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def _load_dotenv(env_file: str | None = None) -> None:
20
+ try:
21
+ from dotenv import load_dotenv
22
+ except ImportError:
23
+ logger.info("python-dotenv not installed; skipping .env loading")
24
+ return
25
+ load_dotenv(dotenv_path=env_file) if env_file else load_dotenv()
26
+
27
+
28
+ def _parse_args(argv: list[str] | None) -> argparse.Namespace:
29
+ parser = build_parser()
30
+ return parser.parse_args(argv)
31
+
32
+
33
+ def _load_config(args: argparse.Namespace) -> Config | None:
34
+ try:
35
+ _load_dotenv(args.env_file)
36
+ return Config.from_env(env_file=args.env_file)
37
+ except ValueError as exc:
38
+ logging.error(str(exc))
39
+ return None
40
+
41
+
42
+ def _resolve_site(args: argparse.Namespace, config: Config) -> str:
43
+ return args.site or config.site
44
+
45
+
46
+ def _handle_generate_mock(args: argparse.Namespace) -> int | None:
47
+ if not args.generate_mock:
48
+ return None
49
+ try:
50
+ from ..model.mock import MockOptions, mock_payload_json
51
+ except ImportError as exc:
52
+ logging.error("Faker is required for --generate-mock: %s", exc)
53
+ return 2
54
+ options = MockOptions(
55
+ seed=args.mock_seed,
56
+ switch_count=max(1, args.mock_switches),
57
+ ap_count=max(0, args.mock_aps),
58
+ wired_client_count=max(0, args.mock_wired_clients),
59
+ wireless_client_count=max(0, args.mock_wireless_clients),
60
+ )
61
+ content = mock_payload_json(options)
62
+ write_output(content, output_path=args.generate_mock, stdout=args.stdout)
63
+ return 0
64
+
65
+
66
+ def _load_runtime_context(
67
+ args: argparse.Namespace,
68
+ ) -> tuple[Config | None, str, list[object] | None, list[object] | None]:
69
+ if args.mock_data:
70
+ try:
71
+ mock_devices, mock_clients = load_mock_data(args.mock_data)
72
+ except Exception as exc: # noqa: BLE001
73
+ raise ValueError(f"Failed to load mock data: {exc}") from exc
74
+ return None, "mock", mock_devices, mock_clients
75
+ config = _load_config(args)
76
+ if config is None:
77
+ raise ValueError("Config required to run")
78
+ site = _resolve_site(args, config)
79
+ return config, site, None, None
80
+
81
+
82
+ def main(argv: list[str] | None = None) -> int:
83
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
84
+ args = _parse_args(argv)
85
+ mock_result = _handle_generate_mock(args)
86
+ if mock_result is not None:
87
+ return mock_result
88
+ try:
89
+ config, site, mock_devices, mock_clients = _load_runtime_context(args)
90
+ except ValueError as exc:
91
+ logging.error(str(exc))
92
+ return 2
93
+ mermaid_theme, svg_theme = resolve_themes(args.theme_file)
94
+
95
+ if args.legend_only:
96
+ legend_style = resolve_legend_style(
97
+ format_name=args.format,
98
+ legend_style=args.legend_style,
99
+ )
100
+ content = render_legend_only(
101
+ legend_style=legend_style,
102
+ legend_scale=args.legend_scale,
103
+ markdown=args.markdown,
104
+ theme=mermaid_theme,
105
+ )
106
+ write_output(content, output_path=args.output, stdout=args.stdout)
107
+ return 0
108
+
109
+ if args.format == "lldp-md":
110
+ return render_lldp_format(
111
+ args,
112
+ config=config,
113
+ site=site,
114
+ mock_devices=mock_devices,
115
+ mock_clients=mock_clients,
116
+ )
117
+
118
+ return render_standard_format(
119
+ args,
120
+ config=config,
121
+ site=site,
122
+ mock_devices=mock_devices,
123
+ mock_clients=mock_clients,
124
+ mermaid_theme=mermaid_theme,
125
+ svg_theme=svg_theme,
126
+ )
127
+
128
+
129
+ if __name__ == "__main__":
130
+ raise SystemExit(main())
@@ -0,0 +1,255 @@
1
+ """CLI rendering orchestration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import logging
7
+
8
+ from ..adapters.config import Config
9
+ from ..adapters.unifi import fetch_clients
10
+ from ..io.export import write_output
11
+ from ..io.mkdocs_assets import write_mkdocs_sidebar_assets
12
+ from ..model.topology import (
13
+ Device,
14
+ TopologyResult,
15
+ build_node_type_map,
16
+ build_port_map,
17
+ group_devices_by_type,
18
+ )
19
+ from ..render.legend import resolve_legend_style
20
+ from ..render.lldp_md import render_lldp_md
21
+ from ..render.mermaid import render_mermaid
22
+ from ..render.mermaid_theme import MermaidTheme
23
+ from ..render.mkdocs import MkdocsRenderOptions, render_mkdocs
24
+ from ..render.svg import SvgOptions, render_svg
25
+ from ..render.svg_theme import SvgTheme
26
+ from .runtime import (
27
+ build_edges_with_clients,
28
+ load_dark_mermaid_theme,
29
+ load_devices_data,
30
+ load_topology_for_render,
31
+ resolve_mkdocs_client_ports,
32
+ select_edges,
33
+ )
34
+
35
+
36
+ def render_mermaid_output(
37
+ args: argparse.Namespace,
38
+ devices: list[Device],
39
+ topology: TopologyResult,
40
+ config: Config | None,
41
+ site: str,
42
+ mermaid_theme: MermaidTheme,
43
+ *,
44
+ clients_override: list[object] | None = None,
45
+ ) -> str:
46
+ edges, _has_tree = select_edges(topology)
47
+ edges, clients = build_edges_with_clients(
48
+ args,
49
+ edges,
50
+ devices,
51
+ config,
52
+ site,
53
+ clients_override=clients_override,
54
+ )
55
+ groups = None
56
+ group_order = None
57
+ if args.group_by_type:
58
+ groups = group_devices_by_type(devices)
59
+ group_order = ["gateway", "switch", "ap", "other"]
60
+ content = render_mermaid(
61
+ edges,
62
+ direction=args.direction,
63
+ groups=groups,
64
+ group_order=group_order,
65
+ node_types=build_node_type_map(devices, clients, client_mode=args.client_scope),
66
+ theme=mermaid_theme,
67
+ )
68
+ if args.markdown:
69
+ content = f"""```mermaid
70
+ {content}```
71
+ """
72
+ return content
73
+
74
+
75
+ def render_svg_output(
76
+ args: argparse.Namespace,
77
+ devices: list[Device],
78
+ topology: TopologyResult,
79
+ config: Config | None,
80
+ site: str,
81
+ svg_theme: SvgTheme,
82
+ *,
83
+ clients_override: list[object] | None = None,
84
+ ) -> str:
85
+ edges, _has_tree = select_edges(topology)
86
+ edges, clients = build_edges_with_clients(
87
+ args,
88
+ edges,
89
+ devices,
90
+ config,
91
+ site,
92
+ clients_override=clients_override,
93
+ )
94
+ options = SvgOptions(width=args.svg_width, height=args.svg_height)
95
+ if args.format == "svg-iso":
96
+ from ..render.svg import render_svg_isometric
97
+
98
+ return render_svg_isometric(
99
+ edges,
100
+ node_types=build_node_type_map(devices, clients, client_mode=args.client_scope),
101
+ options=options,
102
+ theme=svg_theme,
103
+ )
104
+ return render_svg(
105
+ edges,
106
+ node_types=build_node_type_map(devices, clients, client_mode=args.client_scope),
107
+ options=options,
108
+ theme=svg_theme,
109
+ )
110
+
111
+
112
+ def render_mkdocs_format(
113
+ args: argparse.Namespace,
114
+ *,
115
+ devices: list[Device],
116
+ topology: TopologyResult,
117
+ config: Config | None,
118
+ site: str,
119
+ mermaid_theme: MermaidTheme,
120
+ mock_clients: list[object] | None,
121
+ ) -> str | None:
122
+ if args.mkdocs_sidebar_legend and not args.output:
123
+ logging.error("--mkdocs-sidebar-legend requires --output")
124
+ return None
125
+ if args.mkdocs_sidebar_legend:
126
+ write_mkdocs_sidebar_assets(args.output)
127
+ port_map = build_port_map(devices, only_unifi=args.only_unifi)
128
+ client_ports, error_code = resolve_mkdocs_client_ports(
129
+ args,
130
+ devices,
131
+ config,
132
+ site,
133
+ mock_clients,
134
+ )
135
+ if error_code is not None:
136
+ logging.error("Mock data required for client rendering")
137
+ return None
138
+ dark_mermaid_theme = load_dark_mermaid_theme() if args.mkdocs_dual_theme else None
139
+ edges, _has_tree = select_edges(topology)
140
+ options = MkdocsRenderOptions(
141
+ direction=args.direction,
142
+ legend_style=resolve_legend_style(
143
+ format_name=args.format,
144
+ legend_style=args.legend_style,
145
+ ),
146
+ legend_scale=args.legend_scale,
147
+ timestamp_zone=args.mkdocs_timestamp_zone,
148
+ client_scope=args.client_scope,
149
+ dual_theme=args.mkdocs_dual_theme,
150
+ )
151
+ return render_mkdocs(
152
+ edges,
153
+ devices,
154
+ mermaid_theme=mermaid_theme,
155
+ port_map=port_map,
156
+ client_ports=client_ports,
157
+ options=options,
158
+ dark_mermaid_theme=dark_mermaid_theme,
159
+ )
160
+
161
+
162
+ def render_lldp_format(
163
+ args: argparse.Namespace,
164
+ *,
165
+ config: Config | None,
166
+ site: str,
167
+ mock_devices: list[object] | None,
168
+ mock_clients: list[object] | None,
169
+ ) -> int:
170
+ try:
171
+ _raw_devices, devices = load_devices_data(
172
+ args,
173
+ config,
174
+ site,
175
+ raw_devices_override=mock_devices,
176
+ )
177
+ except Exception as exc:
178
+ logging.error("Failed to load devices: %s", exc)
179
+ return 1
180
+ if mock_clients is None:
181
+ if config is None:
182
+ logging.error("Mock data required for client rendering")
183
+ return 2
184
+ clients = list(fetch_clients(config, site=site))
185
+ else:
186
+ clients = mock_clients
187
+ content = render_lldp_md(
188
+ devices,
189
+ clients=clients,
190
+ include_ports=args.include_ports,
191
+ show_clients=args.include_clients,
192
+ client_mode=args.client_scope,
193
+ )
194
+ write_output(content, output_path=args.output, stdout=args.stdout)
195
+ return 0
196
+
197
+
198
+ def render_standard_format(
199
+ args: argparse.Namespace,
200
+ *,
201
+ config: Config | None,
202
+ site: str,
203
+ mock_devices: list[object] | None,
204
+ mock_clients: list[object] | None,
205
+ mermaid_theme: MermaidTheme,
206
+ svg_theme: SvgTheme,
207
+ ) -> int:
208
+ topology_result = load_topology_for_render(
209
+ args,
210
+ config=config,
211
+ site=site,
212
+ mock_devices=mock_devices,
213
+ )
214
+ if topology_result is None:
215
+ return 1
216
+ devices, topology = topology_result
217
+
218
+ if args.format == "mermaid":
219
+ content = render_mermaid_output(
220
+ args,
221
+ devices,
222
+ topology,
223
+ config,
224
+ site,
225
+ mermaid_theme,
226
+ clients_override=mock_clients,
227
+ )
228
+ elif args.format == "mkdocs":
229
+ content = render_mkdocs_format(
230
+ args,
231
+ devices=devices,
232
+ topology=topology,
233
+ config=config,
234
+ site=site,
235
+ mermaid_theme=mermaid_theme,
236
+ mock_clients=mock_clients,
237
+ )
238
+ if content is None:
239
+ return 2
240
+ elif args.format in {"svg", "svg-iso"}:
241
+ content = render_svg_output(
242
+ args,
243
+ devices,
244
+ topology,
245
+ config,
246
+ site,
247
+ svg_theme,
248
+ clients_override=mock_clients,
249
+ )
250
+ else:
251
+ logging.error("Unsupported format: %s", args.format)
252
+ return 2
253
+
254
+ write_output(content, output_path=args.output, stdout=args.stdout)
255
+ return 0