netbox-data-import 2.5.0__tar.gz → 2.6.0__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 (245) hide show
  1. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/PKG-INFO +1 -1
  2. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/__init__.py +1 -1
  3. netbox_data_import-2.6.0/netbox_data_import/cable_disclosure.py +448 -0
  4. netbox_data_import-2.6.0/netbox_data_import/cable_policy.py +157 -0
  5. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/cable_target.py +325 -55
  6. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/catalog.py +1 -0
  7. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/forms.py +53 -5
  8. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/inference_backend.py +16 -1
  9. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/jobs.py +3 -1
  10. netbox_data_import-2.6.0/netbox_data_import/migrations/0036_resolutionproposal_job.py +27 -0
  11. netbox_data_import-2.6.0/netbox_data_import/migrations/0037_cablesegmentoverride.py +42 -0
  12. netbox_data_import-2.6.0/netbox_data_import/migrations/0038_cable_tag_integrity.py +104 -0
  13. netbox_data_import-2.6.0/netbox_data_import/migrations/0039_remove_job_plan_copies.py +22 -0
  14. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/models.py +77 -76
  15. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/plan.py +24 -4
  16. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/preview_row_actions.py +1 -6
  17. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/proposal_decisions.py +3 -3
  18. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/proposal_jobs.py +8 -6
  19. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/proposal_presentation.py +92 -19
  20. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/proposal_tasks.py +46 -6
  21. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/resolution_proposals.py +64 -1
  22. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/review_workspace.py +191 -10
  23. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/trace_picker_search.js +10 -1
  24. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/trace_proposals.js +7 -8
  25. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tables.py +30 -1
  26. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/target_runtime.py +2 -7
  27. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/_proposal_card.html +6 -4
  28. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/trace_workspace.html +105 -7
  29. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/helpers.py +60 -10
  30. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/mock_discipline.py +79 -21
  31. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_atomic_exits.py +12 -0
  32. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_cable_module.py +1236 -25
  33. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_conflict_row_actions.py +1 -1
  34. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_contact_resolution_ajax.py +4 -5
  35. netbox_data_import-2.6.0/netbox_data_import/tests/test_helpers.py +43 -0
  36. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_import_cutover.py +11 -5
  37. netbox_data_import-2.6.0/netbox_data_import/tests/test_job_plan_cleanup_migration.py +57 -0
  38. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_migrations.py +102 -0
  39. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_misc_coverage.py +5 -1
  40. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_mock_discipline.py +54 -0
  41. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_proposal_decisions.py +26 -3
  42. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_proposal_jobs.py +96 -3
  43. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_proposal_tasks.py +48 -0
  44. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_proposal_workspace.py +441 -7
  45. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_quick_action_input_bounds.py +1 -1
  46. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_resolution_proposals.py +19 -1
  47. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_review_actions.py +14 -1
  48. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_review_resolutions.py +1 -1
  49. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_review_workspace.py +8 -2
  50. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_target_field_catalog.py +1 -1
  51. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_trace_decisions.py +2 -2
  52. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_trace_device_resolution.py +32 -2
  53. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_trace_workbook.py +62 -16
  54. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_trace_workspace.py +1414 -18
  55. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_views.py +1 -1
  56. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/trace_workbook.py +5 -14
  57. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/urls.py +10 -0
  58. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/views.py +444 -32
  59. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import.egg-info/PKG-INFO +1 -1
  60. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import.egg-info/SOURCES.txt +8 -0
  61. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/pyproject.toml +1 -1
  62. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/LICENSE +0 -0
  63. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/README.md +0 -0
  64. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/_dns_worker.py +0 -0
  65. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/adapter_config.py +0 -0
  66. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/adapter_forms.py +0 -0
  67. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/adapters.py +0 -0
  68. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/api/__init__.py +0 -0
  69. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/api/serializers.py +0 -0
  70. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/api/urls.py +0 -0
  71. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/api/views.py +0 -0
  72. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/contact_resolution.py +0 -0
  73. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/device_field_review.py +0 -0
  74. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/device_identity.py +0 -0
  75. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/field_keys.py +0 -0
  76. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/filters.py +0 -0
  77. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/flat_workbook.py +0 -0
  78. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/graphql/__init__.py +0 -0
  79. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/graphql/filters.py +0 -0
  80. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/graphql/schema.py +0 -0
  81. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/graphql/types.py +0 -0
  82. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/import_engine.py +0 -0
  83. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/inference_adapter.py +0 -0
  84. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/inference_connection_test.py +0 -0
  85. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/inference_credentials.py +0 -0
  86. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/inference_settings.py +0 -0
  87. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/inference_transport.py +0 -0
  88. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/inference_trust.py +0 -0
  89. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/ip_assignment.py +0 -0
  90. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0001_initial.py +0 -0
  91. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0002_importjob.py +0 -0
  92. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0003_remove_classrolemapping_unique_class_role_mapping_per_profile_and_more.py +0 -0
  93. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0004_features.py +0 -0
  94. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0005_deviceexistingmatch.py +0 -0
  95. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0006_deviceexistingmatch_asset_tag.py +0 -0
  96. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0007_manufacturermapping.py +0 -0
  97. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0008_unique_constraints.py +0 -0
  98. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0009_add_rack_type_to_classrolemapping.py +0 -0
  99. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0010_alter_manufacturermapping_id.py +0 -0
  100. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0011_importprofile_capture_extra_data.py +0 -0
  101. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0012_classrolemapping_rack_type_related_name.py +0 -0
  102. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0013_remove_columnmapping_unique_target.py +0 -0
  103. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0014_alter_columnmapping_target_field_and_more.py +0 -0
  104. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0015_cleanup_duplicate_device_matches.py +0 -0
  105. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0016_deviceexistingmatch_ndi_devicematch_profile_device.py +0 -0
  106. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0017_native_contact_sync.py +0 -0
  107. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0018_ignoredfielddifference.py +0 -0
  108. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0019_deviceimportsource.py +0 -0
  109. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0020_migrate_import_source_custom_field.py +0 -0
  110. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0021_importprofile_adapter_config.py +0 -0
  111. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0022_migrate_profile_adapter_config.py +0 -0
  112. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0023_drop_moved_profile_columns.py +0 -0
  113. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0024_rename_importjob_importexecution.py +0 -0
  114. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0025_remove_importexecution_dry_run_and_more.py +0 -0
  115. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0026_alter_sourcedocument_profile.py +0 -0
  116. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0027_cableclassmapping_terminationresolution.py +0 -0
  117. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0028_cableimportsource.py +0 -0
  118. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0029_alter_cableimportsource_from_text_and_more.py +0 -0
  119. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0030_remove_device_type_creation_config.py +0 -0
  120. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0031_inferencebackend.py +0 -0
  121. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0032_inference_timeout_floor.py +0 -0
  122. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0033_resolutionproposal.py +0 -0
  123. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0034_tracedeviceresolution.py +0 -0
  124. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0035_retire_superseded_proposals.py +0 -0
  125. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/__init__.py +0 -0
  126. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/navigation.py +0 -0
  127. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/netbox_reader.py +0 -0
  128. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/object_permissions.py +0 -0
  129. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/profile_yaml.py +0 -0
  130. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/proposal_contract.py +0 -0
  131. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/proposal_response.py +0 -0
  132. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/signals.py +0 -0
  133. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/source_resolution.py +0 -0
  134. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/source_trace.py +0 -0
  135. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/conflict_modal.js +0 -0
  136. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/contact_candidate_modal.js +0 -0
  137. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/preview_row_actions.js +0 -0
  138. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/preview_row_controls.js +0 -0
  139. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/split_name_modal.js +0 -0
  140. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/sync_row_modal.js +0 -0
  141. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/trace_device_picker.js +0 -0
  142. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/trace_termination_picker.js +0 -0
  143. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/target_modules.py +0 -0
  144. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/template_content.py +0 -0
  145. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/_import_progress.html +0 -0
  146. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/_proposal_action.html +0 -0
  147. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/analysis.html +0 -0
  148. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/bulk_yaml_import.html +0 -0
  149. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/cableclassmapping_edit.html +0 -0
  150. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/classrolemapping_edit.html +0 -0
  151. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/columnmapping_edit.html +0 -0
  152. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/columntransformrule_edit.html +0 -0
  153. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/device_import_data.html +0 -0
  154. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/devicetypemapping_edit.html +0 -0
  155. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/import_preview.html +0 -0
  156. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/import_profile_yaml.html +0 -0
  157. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/import_progress.html +0 -0
  158. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/import_results.html +0 -0
  159. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/import_setup.html +0 -0
  160. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/importexecution_list.html +0 -0
  161. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/importprofile.html +0 -0
  162. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/importprofile_edit.html +0 -0
  163. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/importprofile_list.html +0 -0
  164. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/inferencebackend.html +0 -0
  165. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/source_resolution_list.html +0 -0
  166. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/termination_proposal.py +0 -0
  167. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/__init__.py +0 -0
  168. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/conftest.py +0 -0
  169. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/fixtures/combined_name.xlsx +0 -0
  170. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/fixtures/sample_workbook.xlsx +0 -0
  171. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/fixtures/trace_copper.xlsx +0 -0
  172. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/fixtures/trace_fiber.xlsx +0 -0
  173. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/inference_http.py +0 -0
  174. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/isolated_settings.py +0 -0
  175. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/mixins.py +0 -0
  176. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/netbox_configuration.py +0 -0
  177. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/parallel.py +0 -0
  178. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/redaction.py +0 -0
  179. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_adapter_config.py +0 -0
  180. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_adapter_config_migration.py +0 -0
  181. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_api.py +0 -0
  182. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_ci_configuration.py +0 -0
  183. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_comment_style.py +0 -0
  184. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_configuration_docs.py +0 -0
  185. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_configuration_surfaces.py +0 -0
  186. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_contact_role_suggestion.py +0 -0
  187. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_contact_sync.py +0 -0
  188. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_cutover_edge_contracts.py +0 -0
  189. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_devcontainer_e2e.py +0 -0
  190. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_device_module.py +0 -0
  191. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_digest_indexed_models.py +0 -0
  192. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_execution_audit.py +0 -0
  193. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_graphql_filters.py +0 -0
  194. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_import_engine.py +0 -0
  195. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_import_execution.py +0 -0
  196. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_import_plan.py +0 -0
  197. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_import_source_migration.py +0 -0
  198. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_inference_adapter.py +0 -0
  199. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_inference_backend.py +0 -0
  200. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_inference_connection_test.py +0 -0
  201. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_inference_credentials.py +0 -0
  202. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_inference_secret_containment.py +0 -0
  203. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_inference_settings.py +0 -0
  204. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_inference_trust.py +0 -0
  205. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_installation_docs.py +0 -0
  206. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_ip_target.py +0 -0
  207. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_markdown_tables.py +0 -0
  208. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_model_base_conventions.py +0 -0
  209. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_models.py +0 -0
  210. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_module_boundaries.py +0 -0
  211. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_navigation.py +0 -0
  212. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_netbox_reader.py +0 -0
  213. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_object_permission_writer.py +0 -0
  214. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_parallel_test_setup.py +0 -0
  215. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_placement_reference_locks.py +0 -0
  216. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_policy_write_refusals.py +0 -0
  217. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_preview_assets.py +0 -0
  218. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_preview_state_ownership.py +0 -0
  219. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_pristine_preview_rows.py +0 -0
  220. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_profile_policy_crud_scoping.py +0 -0
  221. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_proposal_response.py +0 -0
  222. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_quick_action_object_permissions.py +0 -0
  223. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_quick_action_profile_id.py +0 -0
  224. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_rack_module.py +0 -0
  225. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_release_metadata.py +0 -0
  226. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_remove_device_type_creation_config_migration.py +0 -0
  227. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_resolution_proposal_api.py +0 -0
  228. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_retire_superseded_proposals_migration.py +0 -0
  229. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_serial_display.py +0 -0
  230. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_source_adapter.py +0 -0
  231. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_source_trace_contract.py +0 -0
  232. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_standard_views.py +0 -0
  233. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_target_module_edge_contracts.py +0 -0
  234. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_text_encoding.py +0 -0
  235. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_views_coverage2.py +0 -0
  236. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_views_extra.py +0 -0
  237. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/trace_device_resolution.py +0 -0
  238. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/trace_schema.py +0 -0
  239. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/transform_regex.py +0 -0
  240. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/utils.py +0 -0
  241. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/values.py +0 -0
  242. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import.egg-info/dependency_links.txt +0 -0
  243. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import.egg-info/requires.txt +0 -0
  244. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import.egg-info/top_level.txt +0 -0
  245. {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netbox-data-import
3
- Version: 2.5.0
3
+ Version: 2.6.0
4
4
  Summary: NetBox plugin for importing data from external DCIM systems
5
5
  Author: Marcin Zieba
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ from typing import Any
4
4
 
5
5
  from netbox.plugins import PluginConfig
6
6
 
7
- __version__ = "2.5.0"
7
+ __version__ = "2.6.0"
8
8
 
9
9
 
10
10
  class NetBoxDataImportConfig(PluginConfig):
@@ -0,0 +1,448 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: 2026 Marcin Zieba <marcinpsk@gmail.com>
3
+ """Apply live row permissions to cached Cable presentation data."""
4
+
5
+ from dataclasses import replace
6
+ from types import MappingProxyType
7
+
8
+ CABLE_ROW = "dcim.cable"
9
+ CABLE_CLASS_MAPPING_ROW = "netbox_data_import.cableclassmapping"
10
+ CABLE_SEGMENT_OVERRIDE_ROW = "netbox_data_import.cablesegmentoverride"
11
+ DISCLOSURE_SOURCE = "disclosure_source"
12
+ POLICY_HIDDEN = "a policy you cannot view"
13
+ POLICY_VISIBLE = "policy_visible"
14
+ POLICY_WRITE_REFUSED = "You cannot change a policy you cannot view."
15
+
16
+ _REFERENCE_FIELDS = frozenset({"device", "cards", "port", "port_class"})
17
+
18
+ _DIAGNOSTIC_DISCLOSURE_FIELDS = MappingProxyType(
19
+ {
20
+ "cable.ambiguous_mapped_peer": (_REFERENCE_FIELDS | {"peers"}, frozenset(), frozenset()),
21
+ "cable.attribute_drift": (
22
+ frozenset({"segment_index"}),
23
+ frozenset({"cable", "status", "type", "profile", "label"}),
24
+ frozenset(),
25
+ ),
26
+ "cable.cableclass_unmapped": (frozenset({"segment_index", "cable_class"}), frozenset(), frozenset()),
27
+ "cable.media_family_mismatch": (
28
+ frozenset(),
29
+ frozenset({"segments"}),
30
+ frozenset({"segments"}),
31
+ ),
32
+ "cable.multi_termination_conflict": (
33
+ frozenset({"segment_index", "port"}),
34
+ frozenset({"cable"}),
35
+ frozenset(),
36
+ ),
37
+ "cable.pass_through_not_mapped": (
38
+ _REFERENCE_FIELDS | {"entry", "exit", "mapped"},
39
+ frozenset(),
40
+ frozenset(),
41
+ ),
42
+ "cable.pass_through_verified": (
43
+ _REFERENCE_FIELDS | {"entry", "exit"},
44
+ frozenset(),
45
+ frozenset(),
46
+ ),
47
+ "cable.permission_denied": (
48
+ _REFERENCE_FIELDS | {"permission"},
49
+ frozenset({"cable"}),
50
+ frozenset(),
51
+ ),
52
+ "cable.planned_termination_conflict": (
53
+ frozenset({"segment_index", "termination", "competing_trace"}),
54
+ frozenset(),
55
+ frozenset(),
56
+ ),
57
+ "cable.policy_stale": (frozenset({"segment_index", "cable_class"}), frozenset(), frozenset()),
58
+ "cable.profile_incompatible": (frozenset({"segment_index", "cable_class"}), frozenset(), frozenset()),
59
+ "cable.resolved_segment_conflict": (
60
+ frozenset({"segment_index", "terminations"}),
61
+ frozenset(),
62
+ frozenset({"cable_type", "cable_profile"}),
63
+ ),
64
+ "cable.same_port_continuation": (_REFERENCE_FIELDS | {"peer"}, frozenset(), frozenset()),
65
+ "cable.segment_override_lost": (
66
+ frozenset({"segment_index"}),
67
+ frozenset(),
68
+ frozenset({"cable_type", "cable_profile"}),
69
+ ),
70
+ "cable.segment_reused": (
71
+ frozenset({"segment_index"}),
72
+ frozenset({"cable"}),
73
+ frozenset(),
74
+ ),
75
+ "cable.segment_self_connection": (
76
+ frozenset({"segment_index", "cable_class", "termination"}),
77
+ frozenset(),
78
+ frozenset(),
79
+ ),
80
+ "cable.termination_kind_mismatch": (
81
+ _REFERENCE_FIELDS | {"selected_display_name", "claimed_kind", "selected_kind"},
82
+ frozenset(),
83
+ frozenset(),
84
+ ),
85
+ "cable.termination_occupied": (
86
+ frozenset({"segment_index", "port"}),
87
+ frozenset({"cable"}),
88
+ frozenset(),
89
+ ),
90
+ "cable.termination_unresolved": (
91
+ _REFERENCE_FIELDS | {"matches", "selected_display_name"},
92
+ frozenset(),
93
+ frozenset(),
94
+ ),
95
+ "cable.unsupported_termination_kind": (
96
+ _REFERENCE_FIELDS | {"selected_object_type"},
97
+ frozenset(),
98
+ frozenset(),
99
+ ),
100
+ }
101
+ )
102
+
103
+ CABLE_DIAGNOSTIC_FIELDS = MappingProxyType(
104
+ {code: frozenset().union(*fields) for code, fields in _DIAGNOSTIC_DISCLOSURE_FIELDS.items()}
105
+ )
106
+
107
+ CABLE_DIAGNOSTIC_DISCLOSURES = MappingProxyType(
108
+ {code: fields[1] for code, fields in _DIAGNOSTIC_DISCLOSURE_FIELDS.items() if fields[1]}
109
+ )
110
+
111
+ POLICY_DIAGNOSTIC_DISCLOSURES = MappingProxyType(
112
+ {code: fields[2] for code, fields in _DIAGNOSTIC_DISCLOSURE_FIELDS.items() if fields[2]}
113
+ )
114
+
115
+
116
+ def disclosure_source(row_kind: str, row_pk: int) -> dict:
117
+ """Return the plan-side identity for one row that supplied display text."""
118
+ return {"kind": row_kind, "pk": row_pk}
119
+
120
+
121
+ def disclosed_cable(cable) -> dict:
122
+ """Return the fields a planner may record for one visible Cable."""
123
+ return {
124
+ "cable_visible": True,
125
+ "cable": str(cable),
126
+ DISCLOSURE_SOURCE: disclosure_source(CABLE_ROW, cable.pk),
127
+ }
128
+
129
+
130
+ def policy_row_kind(row) -> str:
131
+ """Return the disclosure kind for one supported Cable policy row."""
132
+ from .models import CableClassMapping, CableSegmentOverride
133
+
134
+ if isinstance(row, CableClassMapping):
135
+ return CABLE_CLASS_MAPPING_ROW
136
+ if isinstance(row, CableSegmentOverride):
137
+ return CABLE_SEGMENT_OVERRIDE_ROW
138
+ raise TypeError(f"Unsupported Cable policy row: {type(row).__name__}")
139
+
140
+
141
+ def disclosed_policy(row, visible: bool, display: dict) -> dict:
142
+ """Return policy display values only when the planning viewer may read their row."""
143
+ if not visible:
144
+ return _redact_policy(display)
145
+ return {
146
+ **display,
147
+ POLICY_VISIBLE: True,
148
+ DISCLOSURE_SOURCE: disclosure_source(policy_row_kind(row), row.pk),
149
+ }
150
+
151
+
152
+ def policy_row_is_disclosed(display: dict, row) -> bool:
153
+ """Return whether a presented value names this policy row as its visible source."""
154
+ return (
155
+ display.get(POLICY_VISIBLE) is True
156
+ and _source_pk(display.get(DISCLOSURE_SOURCE), policy_row_kind(row)) == row.pk
157
+ )
158
+
159
+
160
+ def validate_diagnostic_disclosures(code: str, display: dict) -> None:
161
+ """Reject diagnostic display fields that bypass the shared disclosure vocabulary."""
162
+ if not code.startswith("cable."):
163
+ return
164
+ fields = _DIAGNOSTIC_DISCLOSURE_FIELDS.get(code)
165
+ if fields is None:
166
+ raise ValueError(f"Diagnostic '{code}' has no registered display schema.")
167
+ public, cable, policy = fields
168
+ if code == "cable.media_family_mismatch":
169
+ _validate_media_segments(display)
170
+ return
171
+ metadata = set()
172
+ if cable:
173
+ metadata.update({DISCLOSURE_SOURCE, "cable_visible"})
174
+ if policy:
175
+ metadata.update({DISCLOSURE_SOURCE, POLICY_VISIBLE})
176
+ unknown = set(display) - set(public) - set(cable) - set(policy) - metadata
177
+ if unknown:
178
+ names = ", ".join(sorted(unknown))
179
+ raise ValueError(f"Diagnostic '{code}' has unregistered display fields: {names}.")
180
+ source = display.get(DISCLOSURE_SOURCE)
181
+ if set(display) & set(cable) or "cable_visible" in display:
182
+ visible = display.get("cable_visible")
183
+ if not isinstance(visible, bool):
184
+ raise ValueError(f"Diagnostic '{code}' has Cable fields without a visibility flag.")
185
+ if visible and _source_pk(source, CABLE_ROW) is None:
186
+ raise ValueError(f"Diagnostic '{code}' has visible Cable fields without a Cable source.")
187
+ if not visible and source is not None:
188
+ raise ValueError(f"Diagnostic '{code}' has a source for a hidden Cable.")
189
+ if set(display) & set(policy) or POLICY_VISIBLE in display:
190
+ visible = display.get(POLICY_VISIBLE)
191
+ if not isinstance(visible, bool):
192
+ raise ValueError(f"Diagnostic '{code}' has policy fields without a visibility flag.")
193
+ row_kinds = (CABLE_CLASS_MAPPING_ROW, CABLE_SEGMENT_OVERRIDE_ROW)
194
+ if visible and not any(_source_pk(source, kind) is not None for kind in row_kinds):
195
+ raise ValueError(f"Diagnostic '{code}' has visible policy fields without a policy source.")
196
+ if not visible and source is not None:
197
+ raise ValueError(f"Diagnostic '{code}' has a source for a hidden policy.")
198
+
199
+
200
+ def _validate_media_segments(display: dict) -> None:
201
+ """Reject media observations that omit provenance or add unregistered nested fields."""
202
+ if set(display) != {"segments"} or not isinstance(display["segments"], list):
203
+ raise ValueError("Diagnostic 'cable.media_family_mismatch' has an invalid display schema.")
204
+ base = {"segment_index", "retained", "visible", "origin"}
205
+ disclosed = {"cable_type", "family", DISCLOSURE_SOURCE}
206
+ for segment in display["segments"]:
207
+ if not isinstance(segment, dict) or frozenset(segment) not in {frozenset(base), frozenset(base | disclosed)}:
208
+ raise ValueError("Diagnostic 'cable.media_family_mismatch' has invalid segment fields.")
209
+ origin = segment.get("origin")
210
+ if origin not in {"cable", "policy"} or not isinstance(segment.get("visible"), bool):
211
+ raise ValueError("Diagnostic 'cable.media_family_mismatch' has an invalid segment origin.")
212
+ if segment["visible"] is False:
213
+ if set(segment) != base:
214
+ raise ValueError("Diagnostic 'cable.media_family_mismatch' discloses a hidden segment.")
215
+ continue
216
+ row_kinds = (CABLE_ROW,) if origin == "cable" else (CABLE_CLASS_MAPPING_ROW, CABLE_SEGMENT_OVERRIDE_ROW)
217
+ if not any(_source_pk(segment.get(DISCLOSURE_SOURCE), kind) is not None for kind in row_kinds):
218
+ raise ValueError("Diagnostic 'cable.media_family_mismatch' has a visible segment without its source.")
219
+
220
+
221
+ def _source_pk(value, row_kind: str) -> int | None:
222
+ if not isinstance(value, dict) or value.get("kind") != row_kind:
223
+ return None
224
+ row_pk = value.get("pk")
225
+ return row_pk if isinstance(row_pk, int) and not isinstance(row_pk, bool) else None
226
+
227
+
228
+ def _source_is_visible(value, row_kinds: tuple[str, ...], visible_row_ids: dict[str, set[int]]) -> bool:
229
+ """Return whether one well-formed source names a row visible in its expected kind."""
230
+ return any(
231
+ (row_pk := _source_pk(value, row_kind)) is not None and row_pk in visible_row_ids[row_kind]
232
+ for row_kind in row_kinds
233
+ )
234
+
235
+
236
+ def _row_ids(units) -> dict[str, set[int]]:
237
+ row_ids: dict[str, set[int]] = {
238
+ CABLE_ROW: set(),
239
+ CABLE_CLASS_MAPPING_ROW: set(),
240
+ CABLE_SEGMENT_OVERRIDE_ROW: set(),
241
+ }
242
+
243
+ def collect(value) -> None:
244
+ if isinstance(value, dict):
245
+ source = value.get(DISCLOSURE_SOURCE)
246
+ for row_kind, ids in row_ids.items():
247
+ if row_pk := _source_pk(source, row_kind):
248
+ ids.add(row_pk)
249
+ for child in value.values():
250
+ collect(child)
251
+ elif isinstance(value, (list, tuple)):
252
+ for child in value:
253
+ collect(child)
254
+
255
+ for unit in units:
256
+ collect(unit.display)
257
+ for diagnostic in unit.diagnostics:
258
+ collect(diagnostic.display)
259
+ return row_ids
260
+
261
+
262
+ def _redact_cable(display: dict, keys: frozenset[str]) -> dict:
263
+ for key in keys:
264
+ display.pop(key, None)
265
+ display.pop(DISCLOSURE_SOURCE, None)
266
+ display["cable_visible"] = False
267
+ return display
268
+
269
+
270
+ def _redact_policy(display: dict) -> dict:
271
+ display = dict(display)
272
+ for key in ("cable_type", "cable_profile"):
273
+ if key in display:
274
+ display[key] = POLICY_HIDDEN
275
+ if "policy" in display:
276
+ display["policy"] = {}
277
+ display.pop(DISCLOSURE_SOURCE, None)
278
+ display[POLICY_VISIBLE] = False
279
+ return display
280
+
281
+
282
+ def _media_segment(segment: dict, visible_row_ids: dict[str, set[int]]) -> dict:
283
+ source = segment.get(DISCLOSURE_SOURCE)
284
+ origin = segment.get("origin")
285
+ row_kinds = (
286
+ (CABLE_ROW,)
287
+ if origin == "cable"
288
+ else (CABLE_CLASS_MAPPING_ROW, CABLE_SEGMENT_OVERRIDE_ROW)
289
+ if origin == "policy"
290
+ else ()
291
+ )
292
+ if segment.get("visible") is True and _source_is_visible(source, row_kinds, visible_row_ids):
293
+ return segment
294
+ return {
295
+ "segment_index": segment["segment_index"],
296
+ "retained": segment["retained"],
297
+ "visible": False,
298
+ "origin": origin if origin in {"cable", "policy"} else "cable",
299
+ }
300
+
301
+
302
+ def _media_message(segments: list[dict]) -> str:
303
+ from .cable_policy import cable_media_family_label, cable_type_label
304
+
305
+ statements = []
306
+ for segment in segments:
307
+ position = segment["segment_index"] + 1
308
+ if not segment["visible"]:
309
+ hidden = POLICY_HIDDEN if segment.get("origin") == "policy" else "a Cable you cannot view"
310
+ statements.append(f"segment {position} uses {hidden}")
311
+ continue
312
+ family = cable_media_family_label(segment["family"])
313
+ statement = f"segment {position} is {cable_type_label(segment['cable_type'])} ({family})"
314
+ if segment["retained"]:
315
+ statement += ", on the Cable this import keeps"
316
+ statements.append(statement)
317
+ remedy = (
318
+ "Correct those Cables in NetBox, then re-read."
319
+ if all(segment["retained"] for segment in segments)
320
+ else "Force the segment that states the wrong medium, or correct the source."
321
+ )
322
+ return f"Verified pass-throughs join these segments, and {'; '.join(statements)}. {remedy}"
323
+
324
+
325
+ def _diagnostic(diagnostic, visible_row_ids: dict[str, set[int]]):
326
+ display = diagnostic.to_dict()["display"]
327
+ if diagnostic.code == "cable.media_family_mismatch":
328
+ segments = [_media_segment(segment, visible_row_ids) for segment in display["segments"]]
329
+ display["segments"] = segments
330
+ display["families"] = sorted(
331
+ {_family_label(segment["family"]) for segment in segments if segment["visible"] and segment.get("family")}
332
+ )
333
+ display["message"] = _media_message(segments)
334
+ elif keys := CABLE_DIAGNOSTIC_DISCLOSURES.get(diagnostic.code):
335
+ if display.get("cable_visible") is True and not _source_is_visible(
336
+ display.get(DISCLOSURE_SOURCE), (CABLE_ROW,), visible_row_ids
337
+ ):
338
+ display = _redact_cable(display, keys)
339
+ if (
340
+ diagnostic.code in POLICY_DIAGNOSTIC_DISCLOSURES
341
+ and display.get(POLICY_VISIBLE) is True
342
+ and not _source_is_visible(
343
+ display.get(DISCLOSURE_SOURCE),
344
+ (CABLE_CLASS_MAPPING_ROW, CABLE_SEGMENT_OVERRIDE_ROW),
345
+ visible_row_ids,
346
+ )
347
+ ):
348
+ display = _redact_policy(display)
349
+ return replace(diagnostic, display=display)
350
+
351
+
352
+ def _family_label(family: str) -> str:
353
+ from .cable_policy import cable_media_family_label
354
+
355
+ return cable_media_family_label(family)
356
+
357
+
358
+ def _unit(unit, visible_row_ids: dict[str, set[int]]):
359
+ display = unit.to_dict()["display"]
360
+ trace = display.get("trace")
361
+ if trace is not None:
362
+ logical = trace.get("logical_cable")
363
+ if (
364
+ logical is not None
365
+ and logical.get("visible") is True
366
+ and not _source_is_visible(logical.get(DISCLOSURE_SOURCE), (CABLE_ROW,), visible_row_ids)
367
+ ):
368
+ trace["logical_cable"] = {"visible": False, "display": "", "description": "", "tags": []}
369
+ for segment in trace.get("segments") or ():
370
+ if segment.get(POLICY_VISIBLE) is True and not _source_is_visible(
371
+ segment.get(DISCLOSURE_SOURCE),
372
+ (CABLE_CLASS_MAPPING_ROW, CABLE_SEGMENT_OVERRIDE_ROW),
373
+ visible_row_ids,
374
+ ):
375
+ segment.pop(DISCLOSURE_SOURCE, None)
376
+ segment.update(_redact_policy(segment))
377
+ for policy in trace.get("cable_policies") or ():
378
+ if policy.get(POLICY_VISIBLE) is True and not _source_is_visible(
379
+ policy.get(DISCLOSURE_SOURCE), (CABLE_CLASS_MAPPING_ROW,), visible_row_ids
380
+ ):
381
+ policy.pop(DISCLOSURE_SOURCE, None)
382
+ policy.update(_redact_policy(policy))
383
+ diagnostics = tuple(_diagnostic(item, visible_row_ids) for item in unit.diagnostics)
384
+ return replace(unit, diagnostics=diagnostics, display=display)
385
+
386
+
387
+ def present_units(units, viewer) -> tuple:
388
+ """Return presentation copies after one live Cable visibility query."""
389
+ if viewer is None:
390
+ raise TypeError("ReviewWorkspace requires a live viewer.")
391
+ units = tuple(units)
392
+ from dcim.models import Cable
393
+
394
+ from .models import CableClassMapping, CableSegmentOverride
395
+
396
+ row_ids = _row_ids(units)
397
+ models = {
398
+ CABLE_ROW: Cable,
399
+ CABLE_CLASS_MAPPING_ROW: CableClassMapping,
400
+ CABLE_SEGMENT_OVERRIDE_ROW: CableSegmentOverride,
401
+ }
402
+ visible = {}
403
+ for row_kind, model in models.items():
404
+ ids = row_ids[row_kind]
405
+ visible[row_kind] = (
406
+ set(model.objects.restrict(viewer, "view").filter(pk__in=sorted(ids)).values_list("pk", flat=True))
407
+ if ids
408
+ else set()
409
+ )
410
+ return tuple(_unit(unit, visible) for unit in units)
411
+
412
+
413
+ def redact_deleted_cables(plan_data: dict) -> dict:
414
+ """Return an execution copy whose deleted Cable rows authorize no stored display text."""
415
+ from .plan import ImportPlan
416
+
417
+ plan = ImportPlan.from_dict(plan_data)
418
+ visible = _row_ids(plan.units)
419
+ deleted_ids = {
420
+ change.payload["cable_id"]
421
+ for unit in plan.units
422
+ for change in unit.changes
423
+ if change.target_module == "cable" and change.operation == "delete"
424
+ }
425
+ visible[CABLE_ROW].difference_update(deleted_ids)
426
+ units = tuple(_unit(unit, visible) for unit in plan.units)
427
+ return replace(plan, units=units).to_dict()
428
+
429
+
430
+ __all__ = (
431
+ "CABLE_CLASS_MAPPING_ROW",
432
+ "CABLE_DIAGNOSTIC_DISCLOSURES",
433
+ "CABLE_DIAGNOSTIC_FIELDS",
434
+ "CABLE_ROW",
435
+ "CABLE_SEGMENT_OVERRIDE_ROW",
436
+ "DISCLOSURE_SOURCE",
437
+ "POLICY_DIAGNOSTIC_DISCLOSURES",
438
+ "POLICY_HIDDEN",
439
+ "POLICY_VISIBLE",
440
+ "POLICY_WRITE_REFUSED",
441
+ "disclosed_cable",
442
+ "disclosed_policy",
443
+ "disclosure_source",
444
+ "policy_row_is_disclosed",
445
+ "present_units",
446
+ "redact_deleted_cables",
447
+ "validate_diagnostic_disclosures",
448
+ )
@@ -0,0 +1,157 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: 2026 Marcin Zieba <marcinpsk@gmail.com>
3
+ """Own the Cable policy question: the runtime choices, and which stored decision is in force."""
4
+
5
+ from django.core.exceptions import ValidationError
6
+
7
+
8
+ def _flatten_choice_groups(choices):
9
+ """Return value and label pairs from flat or grouped NetBox choices."""
10
+ flattened = []
11
+ for value, label in choices:
12
+ if isinstance(label, (tuple, list)):
13
+ flattened.extend(label)
14
+ else:
15
+ flattened.append((value, label))
16
+ return tuple(flattened)
17
+
18
+
19
+ def cable_type_choices():
20
+ """Return the Cable Type values offered by the running NetBox instance."""
21
+ from dcim.choices import CableTypeChoices
22
+
23
+ return _flatten_choice_groups(CableTypeChoices.CHOICES)
24
+
25
+
26
+ def cable_profile_choices():
27
+ """Return the Cable Profile values offered by the running NetBox instance."""
28
+ from dcim.choices import CableProfileChoices
29
+
30
+ return _flatten_choice_groups(CableProfileChoices.CHOICES)
31
+
32
+
33
+ def cable_profile_accepts_one_termination_per_side(value) -> bool:
34
+ """Return whether NetBox reports one connector on each side of the Cable Profile."""
35
+ from dcim.models import Cable
36
+
37
+ profile_class = Cable(profile=value).profile_class
38
+ return profile_class is not None and len(profile_class.a_connectors) == 1 and len(profile_class.b_connectors) == 1
39
+
40
+
41
+ def compatible_cable_profile_choices():
42
+ """Return running Cable Profiles that permit one termination on each side."""
43
+ return tuple(
44
+ (value, label)
45
+ for value, label in cable_profile_choices()
46
+ if cable_profile_accepts_one_termination_per_side(value)
47
+ )
48
+
49
+
50
+ def policy_choice_errors(cable_type, cable_profile):
51
+ """Return runtime-choice and profile-cardinality errors by model field."""
52
+ errors = {}
53
+ type_values = {value for value, _label in cable_type_choices()}
54
+ profile_values = {value for value, _label in cable_profile_choices()}
55
+ if cable_type is not None and cable_type not in type_values:
56
+ errors["cable_type"] = ValidationError(
57
+ "The selected Cable Type is no longer offered by this NetBox instance.",
58
+ code="cable.policy_stale",
59
+ )
60
+ if cable_profile is not None and cable_profile not in profile_values:
61
+ errors["cable_profile"] = ValidationError(
62
+ "The selected Cable Profile is no longer offered by this NetBox instance.",
63
+ code="cable.policy_stale",
64
+ )
65
+ elif cable_profile is not None and not cable_profile_accepts_one_termination_per_side(cable_profile):
66
+ errors["cable_profile"] = ValidationError(
67
+ "The selected Cable Profile does not permit one termination on each side.",
68
+ code="cable.profile_incompatible",
69
+ )
70
+ return errors
71
+
72
+
73
+ # An active optical assembly states no terminated medium, so its whole group decides nothing.
74
+ INDETERMINATE_MEDIA_TYPE = "aoc"
75
+
76
+
77
+ def _media_groups():
78
+ """Return the Cable Type values of each group the running instance offers, with its label."""
79
+ from dcim.choices import CableTypeChoices
80
+
81
+ return [
82
+ (label, [item_value for item_value, _item_label in group])
83
+ for label, group in CableTypeChoices.CHOICES
84
+ if isinstance(group, (tuple, list))
85
+ ]
86
+
87
+
88
+ def cable_media_families():
89
+ """Return the family each Cable Type belongs to, keyed by a value rather than by a group label.
90
+
91
+ A group label is a lazy translation, so keying on it would make classification depend on the
92
+ reader's language and would put translated text in a fingerprint (spec section 4.3).
93
+ """
94
+ families = {}
95
+ for _label, values in _media_groups():
96
+ families.update(dict.fromkeys(values, min(values)))
97
+ return families
98
+
99
+
100
+ def cable_media_family_label(family) -> str:
101
+ """Return the operator-facing name of one media family, in the reader's own language."""
102
+ for label, values in _media_groups():
103
+ if min(values) == family:
104
+ return str(label)
105
+ return family
106
+
107
+
108
+ def decisive_media_family(cable_type) -> str:
109
+ """Return the family that decides one Cable Type's medium, or "" when nothing decides it.
110
+
111
+ A value the instance does not group, and one it groups as indeterminate, are incomplete coverage.
112
+ They never read as agreement with another segment and never contradict one.
113
+ """
114
+ families = cable_media_families()
115
+ family = families.get(cable_type or "", "")
116
+ return "" if family and family == families.get(INDETERMINATE_MEDIA_TYPE, "") else family
117
+
118
+
119
+ def cable_profile_splits_a_span(cable_profile) -> bool:
120
+ """Return whether a Cable with this profile cannot be read as one medium along one path.
121
+
122
+ A profile with several connectors on a side, a breakout or a trunk, fans one Cable out into runs
123
+ the path does not state, so its type says nothing about the medium of the segment beside it.
124
+ """
125
+ return bool(cable_profile) and not cable_profile_accepts_one_termination_per_side(cable_profile)
126
+
127
+
128
+ def policy_in_force(override, mapping):
129
+ """Return the stored decision that governs one segment: its override, else the CableClass row."""
130
+ return mapping if override is None else override
131
+
132
+
133
+ def cable_type_label(value) -> str:
134
+ """Return the operator-facing name of one stored Cable Type value."""
135
+ return "None" if value is None else str(dict(cable_type_choices()).get(value, value))
136
+
137
+
138
+ def cable_profile_label(value) -> str:
139
+ """Return the operator-facing name of one stored Cable Profile value."""
140
+ return "None" if value is None else str(dict(cable_profile_choices()).get(value, value))
141
+
142
+
143
+ __all__ = (
144
+ "INDETERMINATE_MEDIA_TYPE",
145
+ "cable_media_families",
146
+ "cable_media_family_label",
147
+ "cable_profile_accepts_one_termination_per_side",
148
+ "cable_profile_choices",
149
+ "cable_profile_label",
150
+ "cable_profile_splits_a_span",
151
+ "cable_type_choices",
152
+ "cable_type_label",
153
+ "compatible_cable_profile_choices",
154
+ "decisive_media_family",
155
+ "policy_choice_errors",
156
+ "policy_in_force",
157
+ )