unifi-network-maps 1.4.10__tar.gz → 1.4.11__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 (130) hide show
  1. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/CHANGELOG.md +9 -1
  2. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/PKG-INFO +1 -1
  3. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/pyproject.toml +1 -1
  4. unifi_network_maps-1.4.11/src/unifi_network_maps/__init__.py +1 -0
  5. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/model/topology.py +22 -11
  6. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/render/lldp_md.py +23 -11
  7. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/render/svg.py +8 -2
  8. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps.egg-info/PKG-INFO +1 -1
  9. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/tests/test_svg.py +27 -0
  10. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/tests/test_topology.py +5 -0
  11. unifi_network_maps-1.4.10/src/unifi_network_maps/__init__.py +0 -1
  12. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/CONTRIBUTING.md +0 -0
  13. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/LICENSE +0 -0
  14. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/LICENSES.md +0 -0
  15. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/MANIFEST.in +0 -0
  16. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/README.md +0 -0
  17. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/RELEASING.md +0 -0
  18. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/SECURITY.md +0 -0
  19. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/setup.cfg +0 -0
  20. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/__main__.py +0 -0
  21. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/adapters/__init__.py +0 -0
  22. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/adapters/config.py +0 -0
  23. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/adapters/unifi.py +0 -0
  24. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/__init__.py +0 -0
  25. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/__init__.py +0 -0
  26. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/access-point.svg +0 -0
  27. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/ISOPACKS_LICENSE +0 -0
  28. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/block.svg +0 -0
  29. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/cache.svg +0 -0
  30. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/cardterminal.svg +0 -0
  31. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/cloud.svg +0 -0
  32. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/cronjob.svg +0 -0
  33. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/cube.svg +0 -0
  34. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/desktop.svg +0 -0
  35. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/diamond.svg +0 -0
  36. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/dns.svg +0 -0
  37. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/document.svg +0 -0
  38. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/firewall.svg +0 -0
  39. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/function-module.svg +0 -0
  40. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/image.svg +0 -0
  41. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/laptop.svg +0 -0
  42. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/loadbalancer.svg +0 -0
  43. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/lock.svg +0 -0
  44. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/mail.svg +0 -0
  45. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/mailmultiple.svg +0 -0
  46. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/mobiledevice.svg +0 -0
  47. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/office.svg +0 -0
  48. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/package-module.svg +0 -0
  49. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/paymentcard.svg +0 -0
  50. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/plane.svg +0 -0
  51. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/printer.svg +0 -0
  52. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/pyramid.svg +0 -0
  53. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/queue.svg +0 -0
  54. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/router.svg +0 -0
  55. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/server.svg +0 -0
  56. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/speech.svg +0 -0
  57. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/sphere.svg +0 -0
  58. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/storage.svg +0 -0
  59. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/switch-module.svg +0 -0
  60. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/tower.svg +0 -0
  61. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/truck-2.svg +0 -0
  62. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/truck.svg +0 -0
  63. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/user.svg +0 -0
  64. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/isometric/vm.svg +0 -0
  65. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/laptop.svg +0 -0
  66. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/router-network.svg +0 -0
  67. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/server-network.svg +0 -0
  68. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/icons/server.svg +0 -0
  69. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/themes/dark.yaml +0 -0
  70. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/assets/themes/default.yaml +0 -0
  71. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/cli/__init__.py +0 -0
  72. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/cli/__main__.py +0 -0
  73. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/cli/args.py +0 -0
  74. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/cli/main.py +0 -0
  75. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/cli/render.py +0 -0
  76. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/cli/runtime.py +0 -0
  77. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/io/__init__.py +0 -0
  78. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/io/debug.py +0 -0
  79. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/io/export.py +0 -0
  80. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/io/mkdocs_assets.py +0 -0
  81. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/io/mock_data.py +0 -0
  82. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/io/mock_generate.py +0 -0
  83. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/model/__init__.py +0 -0
  84. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/model/labels.py +0 -0
  85. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/model/lldp.py +0 -0
  86. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/model/mock.py +0 -0
  87. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/model/ports.py +0 -0
  88. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/render/__init__.py +0 -0
  89. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/render/device_ports_md.py +0 -0
  90. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/render/legend.py +0 -0
  91. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/render/markdown_tables.py +0 -0
  92. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/render/mermaid.py +0 -0
  93. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/render/mermaid_theme.py +0 -0
  94. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/render/mkdocs.py +0 -0
  95. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/render/svg_theme.py +0 -0
  96. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/render/templates/device_port_block.md.j2 +0 -0
  97. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/render/templates/legend_compact.html.j2 +0 -0
  98. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/render/templates/lldp_device_section.md.j2 +0 -0
  99. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/render/templates/markdown_section.md.j2 +0 -0
  100. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/render/templates/mermaid_legend.mmd.j2 +0 -0
  101. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/render/templates/mkdocs_document.md.j2 +0 -0
  102. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/render/templates/mkdocs_dual_theme_style.html.j2 +0 -0
  103. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/render/templates/mkdocs_html_block.html.j2 +0 -0
  104. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/render/templates/mkdocs_legend.css.j2 +0 -0
  105. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/render/templates/mkdocs_legend.js.j2 +0 -0
  106. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/render/templates/mkdocs_mermaid_block.md.j2 +0 -0
  107. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/render/templating.py +0 -0
  108. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps/render/theme.py +0 -0
  109. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps.egg-info/SOURCES.txt +0 -0
  110. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps.egg-info/dependency_links.txt +0 -0
  111. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps.egg-info/entry_points.txt +0 -0
  112. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps.egg-info/requires.txt +0 -0
  113. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/src/unifi_network_maps.egg-info/top_level.txt +0 -0
  114. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/tests/test_cli.py +0 -0
  115. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/tests/test_clients.py +0 -0
  116. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/tests/test_config.py +0 -0
  117. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/tests/test_contract_unifi.py +0 -0
  118. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/tests/test_contract_unifi_live.py +0 -0
  119. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/tests/test_debug.py +0 -0
  120. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/tests/test_device_ports_md.py +0 -0
  121. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/tests/test_export.py +0 -0
  122. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/tests/test_groups.py +0 -0
  123. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/tests/test_labels.py +0 -0
  124. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/tests/test_lldp.py +0 -0
  125. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/tests/test_lldp_md.py +0 -0
  126. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/tests/test_mermaid.py +0 -0
  127. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/tests/test_mock_generate.py +0 -0
  128. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/tests/test_svg_iso.py +0 -0
  129. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/tests/test_theme.py +0 -0
  130. {unifi_network_maps-1.4.10 → unifi_network_maps-1.4.11}/tests/test_unifi.py +0 -0
@@ -5,6 +5,13 @@ 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.11] - 2026-01-19
9
+ ### Added
10
+ - Add data-edge-left/right attributes to SVG paths
11
+
12
+ ### Fixed
13
+ - Regression in identifying wireless/wired clients
14
+
8
15
  ## [1.4.10] - 2026-01-18
9
16
  ### Added
10
17
  - Add speed and channel fields to Edge dataclass
@@ -169,7 +176,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
169
176
  - Introduced SVG renderer and tree layout fixes.
170
177
  - Increased test coverage and added coverage tooling.
171
178
 
172
- [Unreleased]: https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.10...HEAD
179
+ [Unreleased]: https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.11...HEAD
180
+ [1.4.11]:https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.10...v1.4.111
173
181
  [1.4.10]:https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.9...v1.4.10
174
182
  [1.4.9]: https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.8...v1.4.9
175
183
  [1.4.8]: https://github.com/merlijntishauser/unifi-network-maps/compare/v1.4.7...v1.4.8
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unifi-network-maps
3
- Version: 1.4.10
3
+ Version: 1.4.11
4
4
  Summary: Dynamic UniFi -> network maps in mermaid or svg
5
5
  Author: Merlijn
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "unifi-network-maps"
7
- version = "1.4.10"
7
+ version = "1.4.11"
8
8
  description = "Dynamic UniFi -> network maps in mermaid or svg"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -0,0 +1 @@
1
+ __version__ = "1.4.11"
@@ -482,20 +482,31 @@ def _client_uplink_mac(client: object) -> str | None:
482
482
 
483
483
 
484
484
  def _client_uplink_port(client: object) -> int | None:
485
- for key in ("uplink_remote_port", "sw_port", "ap_port"):
486
- value = _client_field(client, key)
487
- if isinstance(value, int):
488
- return value
489
- if isinstance(value, str) and value.isdigit():
490
- return int(value)
485
+ for value in _client_port_values(client):
486
+ parsed = _parse_port_value(value)
487
+ if parsed is not None:
488
+ return parsed
489
+ return None
490
+
491
+
492
+ def _client_port_values(client: object) -> Iterable[object | None]:
493
+ for key in ("uplink_remote_port", "sw_port", "ap_port", "port_idx"):
494
+ yield _client_field(client, key)
491
495
  for key in ("uplink", "last_uplink"):
492
496
  nested = _client_field(client, key)
493
497
  if isinstance(nested, dict):
494
- value = nested.get("uplink_remote_port")
495
- if isinstance(value, int):
496
- return value
497
- if isinstance(value, str) and value.isdigit():
498
- return int(value)
498
+ for nested_key in ("uplink_remote_port", "port_idx"):
499
+ yield nested.get(nested_key)
500
+
501
+
502
+ def _parse_port_value(value: object | None) -> int | None:
503
+ if isinstance(value, int):
504
+ return value
505
+ if isinstance(value, str):
506
+ stripped = value.strip()
507
+ if stripped.isdigit():
508
+ return int(stripped)
509
+ return extract_port_number(stripped)
499
510
  return None
500
511
 
501
512
 
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  from collections.abc import Iterable
6
6
 
7
7
  from ..model.lldp import LLDPEntry, local_port_label
8
+ from ..model.ports import extract_port_number
8
9
  from ..model.topology import Device, build_client_port_map, build_device_index, build_port_map
9
10
  from .device_ports_md import render_device_port_details
10
11
  from .markdown_tables import markdown_table_lines
@@ -44,20 +45,31 @@ def _client_uplink_mac(client: object) -> str | None:
44
45
 
45
46
 
46
47
  def _client_uplink_port(client: object) -> int | None:
47
- for key in ("uplink_remote_port", "sw_port", "ap_port"):
48
- value = _client_field(client, key)
49
- if isinstance(value, int):
50
- return value
51
- if isinstance(value, str) and value.isdigit():
52
- return int(value)
48
+ for value in _client_port_values(client):
49
+ parsed = _parse_port_value(value)
50
+ if parsed is not None:
51
+ return parsed
52
+ return None
53
+
54
+
55
+ def _client_port_values(client: object) -> Iterable[object | None]:
56
+ for key in ("uplink_remote_port", "sw_port", "ap_port", "port_idx"):
57
+ yield _client_field(client, key)
53
58
  for key in ("uplink", "last_uplink"):
54
59
  nested = _client_field(client, key)
55
60
  if isinstance(nested, dict):
56
- value = nested.get("uplink_remote_port")
57
- if isinstance(value, int):
58
- return value
59
- if isinstance(value, str) and value.isdigit():
60
- return int(value)
61
+ for nested_key in ("uplink_remote_port", "port_idx"):
62
+ yield nested.get(nested_key)
63
+
64
+
65
+ def _parse_port_value(value: object | None) -> int | None:
66
+ if isinstance(value, int):
67
+ return value
68
+ if isinstance(value, str):
69
+ stripped = value.strip()
70
+ if stripped.isdigit():
71
+ return int(stripped)
72
+ return extract_port_number(stripped)
61
73
  return None
62
74
 
63
75
 
@@ -547,8 +547,11 @@ def _render_svg_edges(
547
547
  f"L {dst_cx} {mid_y} L {dst_cx} {dst_top}"
548
548
  )
549
549
  dash = ' stroke-dasharray="6 4"' if edge.wireless else ""
550
+ left_attr = _escape_attr(edge.left, quote=True)
551
+ right_attr = _escape_attr(edge.right, quote=True)
550
552
  lines.append(
551
- f'<path d="{path}" stroke="{color}" stroke-width="{width_px}" fill="none"{dash}/>'
553
+ f'<path d="{path}" stroke="{color}" stroke-width="{width_px}" fill="none"{dash} '
554
+ f'data-edge-left="{left_attr}" data-edge-right="{right_attr}"/>'
552
555
  )
553
556
  if edge.poe:
554
557
  icon_x = dst_cx
@@ -834,9 +837,12 @@ def _render_iso_edges(
834
837
  dst_cy,
835
838
  )
836
839
  dash = ' stroke-dasharray="8 6"' if edge.wireless else ""
840
+ left_attr = _escape_attr(edge.left, quote=True)
841
+ right_attr = _escape_attr(edge.right, quote=True)
837
842
  lines.append(
838
843
  f'<path d="{" ".join(path_cmds)}" stroke="{color}" stroke-width="{width_px}" '
839
- f'fill="none" stroke-linecap="round" stroke-linejoin="round"{dash}/>'
844
+ f'fill="none" stroke-linecap="round" stroke-linejoin="round"{dash} '
845
+ f'data-edge-left="{left_attr}" data-edge-right="{right_attr}"/>'
840
846
  )
841
847
  if edge.poe:
842
848
  icon_x = dst_cx
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unifi-network-maps
3
- Version: 1.4.10
3
+ Version: 1.4.11
4
4
  Summary: Dynamic UniFi -> network maps in mermaid or svg
5
5
  Author: Merlijn
6
6
  License-Expression: MIT
@@ -263,3 +263,30 @@ def test_render_svg_isometric_nodes_reference_iso_node_prefix():
263
263
  [Edge("A", "B")], node_types={"A": "switch", "B": "switch"}
264
264
  )
265
265
  assert 'fill="url(#iso-node-switch)"' in output
266
+
267
+
268
+ def test_render_svg_adds_edge_data_attributes():
269
+ output = svg_module.render_svg(
270
+ [Edge("Gateway", "Switch")],
271
+ node_types={"Gateway": "gateway", "Switch": "switch"},
272
+ )
273
+ assert 'data-edge-left="Gateway"' in output
274
+ assert 'data-edge-right="Switch"' in output
275
+
276
+
277
+ def test_render_svg_escapes_edge_data_attributes():
278
+ output = svg_module.render_svg(
279
+ [Edge('Node "A"', "Node <B>")],
280
+ node_types={'Node "A"': "gateway", "Node <B>": "switch"},
281
+ )
282
+ assert 'data-edge-left="Node &quot;A&quot;"' in output
283
+ assert 'data-edge-right="Node &lt;B&gt;"' in output
284
+
285
+
286
+ def test_render_svg_isometric_adds_edge_data_attributes():
287
+ output = svg_module.render_svg_isometric(
288
+ [Edge("Gateway", "Switch")],
289
+ node_types={"Gateway": "gateway", "Switch": "switch"},
290
+ )
291
+ assert 'data-edge-left="Gateway"' in output
292
+ assert 'data-edge-right="Switch"' in output
@@ -502,6 +502,11 @@ def test_client_uplink_port_direct_str_digit():
502
502
  assert _client_uplink_port(client) == 7
503
503
 
504
504
 
505
+ def test_client_uplink_port_parses_port_label():
506
+ client = {"uplink_remote_port": "Port 9"}
507
+ assert _client_uplink_port(client) == 9
508
+
509
+
505
510
  def test_client_uplink_port_nested_int():
506
511
  client = {"uplink": {"uplink_remote_port": 8}}
507
512
  assert _client_uplink_port(client) == 8
@@ -1 +0,0 @@
1
- __version__ = "1.4.10"