netbox-data-import 2.4.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 (247) hide show
  1. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/PKG-INFO +3 -3
  2. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/README.md +2 -2
  3. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/__init__.py +1 -1
  4. netbox_data_import-2.6.0/netbox_data_import/_dns_worker.py +31 -0
  5. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/adapter_forms.py +1 -1
  6. netbox_data_import-2.6.0/netbox_data_import/cable_disclosure.py +448 -0
  7. netbox_data_import-2.6.0/netbox_data_import/cable_policy.py +157 -0
  8. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/cable_target.py +334 -57
  9. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/catalog.py +1 -0
  10. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/device_field_review.py +45 -3
  11. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/forms.py +53 -5
  12. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/inference_adapter.py +1 -5
  13. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/inference_backend.py +16 -1
  14. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/inference_credentials.py +2 -9
  15. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/inference_transport.py +1 -27
  16. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/inference_trust.py +109 -2
  17. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/jobs.py +3 -1
  18. netbox_data_import-2.6.0/netbox_data_import/migrations/0036_resolutionproposal_job.py +27 -0
  19. netbox_data_import-2.6.0/netbox_data_import/migrations/0037_cablesegmentoverride.py +42 -0
  20. netbox_data_import-2.6.0/netbox_data_import/migrations/0038_cable_tag_integrity.py +104 -0
  21. netbox_data_import-2.6.0/netbox_data_import/migrations/0039_remove_job_plan_copies.py +22 -0
  22. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/models.py +77 -76
  23. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/plan.py +24 -4
  24. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/preview_row_actions.py +1 -6
  25. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/proposal_decisions.py +3 -3
  26. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/proposal_jobs.py +8 -6
  27. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/proposal_presentation.py +92 -19
  28. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/proposal_tasks.py +46 -6
  29. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/resolution_proposals.py +64 -1
  30. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/review_workspace.py +191 -10
  31. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/conflict_modal.js +3 -1
  32. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/contact_candidate_modal.js +15 -11
  33. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/preview_row_actions.js +2 -0
  34. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/preview_row_controls.js +126 -4
  35. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/sync_row_modal.js +147 -53
  36. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/trace_picker_search.js +10 -1
  37. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/trace_proposals.js +7 -8
  38. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tables.py +30 -1
  39. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/target_modules.py +21 -6
  40. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/target_runtime.py +2 -7
  41. netbox_data_import-2.6.0/netbox_data_import/templates/netbox_data_import/_proposal_card.html +33 -0
  42. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/import_preview.html +28 -3
  43. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/import_profile_yaml.html +1 -1
  44. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/trace_workspace.html +208 -47
  45. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/helpers.py +63 -13
  46. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/mock_discipline.py +79 -21
  47. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_adapter_config_migration.py +2 -2
  48. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_atomic_exits.py +15 -3
  49. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_cable_module.py +1243 -28
  50. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_ci_configuration.py +14 -28
  51. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_comment_style.py +2 -2
  52. netbox_data_import-2.6.0/netbox_data_import/tests/test_configuration_docs.py +147 -0
  53. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_conflict_row_actions.py +1 -1
  54. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_contact_resolution_ajax.py +4 -5
  55. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_contact_sync.py +2 -2
  56. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_execution_audit.py +1 -1
  57. netbox_data_import-2.6.0/netbox_data_import/tests/test_helpers.py +43 -0
  58. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_import_cutover.py +110 -5
  59. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_import_source_migration.py +2 -2
  60. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_inference_adapter.py +1 -110
  61. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_inference_connection_test.py +77 -0
  62. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_inference_trust.py +228 -0
  63. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_installation_docs.py +1 -1
  64. netbox_data_import-2.6.0/netbox_data_import/tests/test_job_plan_cleanup_migration.py +57 -0
  65. netbox_data_import-2.6.0/netbox_data_import/tests/test_markdown_tables.py +124 -0
  66. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_migrations.py +103 -1
  67. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_misc_coverage.py +5 -1
  68. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_mock_discipline.py +65 -6
  69. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_module_boundaries.py +24 -15
  70. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_placement_reference_locks.py +1 -1
  71. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_policy_write_refusals.py +1 -1
  72. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_preview_assets.py +483 -10
  73. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_pristine_preview_rows.py +1 -1
  74. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_proposal_decisions.py +26 -3
  75. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_proposal_jobs.py +96 -3
  76. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_proposal_tasks.py +48 -0
  77. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_proposal_workspace.py +442 -8
  78. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_quick_action_input_bounds.py +4 -4
  79. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_release_metadata.py +3 -3
  80. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_resolution_proposals.py +19 -1
  81. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_retire_superseded_proposals_migration.py +1 -1
  82. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_review_actions.py +28 -1
  83. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_review_resolutions.py +1 -1
  84. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_review_workspace.py +8 -2
  85. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_target_field_catalog.py +2 -2
  86. netbox_data_import-2.6.0/netbox_data_import/tests/test_text_encoding.py +116 -0
  87. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_trace_decisions.py +3 -3
  88. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_trace_device_resolution.py +186 -4
  89. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_trace_workbook.py +62 -16
  90. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_trace_workspace.py +1475 -18
  91. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_views.py +5 -5
  92. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/trace_workbook.py +5 -14
  93. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/urls.py +10 -0
  94. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/views.py +526 -37
  95. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import.egg-info/PKG-INFO +3 -3
  96. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import.egg-info/SOURCES.txt +12 -1
  97. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/pyproject.toml +13 -5
  98. netbox_data_import-2.4.0/netbox_data_import/templates/netbox_data_import/_proposal_card.html +0 -31
  99. netbox_data_import-2.4.0/netbox_data_import/tests/test_configuration_docs.py +0 -55
  100. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/LICENSE +0 -0
  101. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/adapter_config.py +0 -0
  102. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/adapters.py +0 -0
  103. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/api/__init__.py +0 -0
  104. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/api/serializers.py +0 -0
  105. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/api/urls.py +0 -0
  106. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/api/views.py +0 -0
  107. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/contact_resolution.py +0 -0
  108. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/device_identity.py +0 -0
  109. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/field_keys.py +0 -0
  110. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/filters.py +0 -0
  111. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/flat_workbook.py +0 -0
  112. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/graphql/__init__.py +0 -0
  113. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/graphql/filters.py +0 -0
  114. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/graphql/schema.py +0 -0
  115. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/graphql/types.py +0 -0
  116. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/import_engine.py +0 -0
  117. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/inference_connection_test.py +0 -0
  118. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/inference_settings.py +0 -0
  119. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/ip_assignment.py +0 -0
  120. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0001_initial.py +0 -0
  121. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0002_importjob.py +0 -0
  122. {netbox_data_import-2.4.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
  123. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0004_features.py +0 -0
  124. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0005_deviceexistingmatch.py +0 -0
  125. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0006_deviceexistingmatch_asset_tag.py +0 -0
  126. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0007_manufacturermapping.py +0 -0
  127. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0008_unique_constraints.py +0 -0
  128. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0009_add_rack_type_to_classrolemapping.py +0 -0
  129. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0010_alter_manufacturermapping_id.py +0 -0
  130. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0011_importprofile_capture_extra_data.py +0 -0
  131. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0012_classrolemapping_rack_type_related_name.py +0 -0
  132. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0013_remove_columnmapping_unique_target.py +0 -0
  133. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0014_alter_columnmapping_target_field_and_more.py +0 -0
  134. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0015_cleanup_duplicate_device_matches.py +0 -0
  135. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0016_deviceexistingmatch_ndi_devicematch_profile_device.py +0 -0
  136. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0017_native_contact_sync.py +0 -0
  137. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0018_ignoredfielddifference.py +0 -0
  138. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0019_deviceimportsource.py +0 -0
  139. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0020_migrate_import_source_custom_field.py +0 -0
  140. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0021_importprofile_adapter_config.py +0 -0
  141. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0022_migrate_profile_adapter_config.py +0 -0
  142. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0023_drop_moved_profile_columns.py +0 -0
  143. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0024_rename_importjob_importexecution.py +0 -0
  144. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0025_remove_importexecution_dry_run_and_more.py +0 -0
  145. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0026_alter_sourcedocument_profile.py +0 -0
  146. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0027_cableclassmapping_terminationresolution.py +0 -0
  147. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0028_cableimportsource.py +0 -0
  148. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0029_alter_cableimportsource_from_text_and_more.py +0 -0
  149. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0030_remove_device_type_creation_config.py +0 -0
  150. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0031_inferencebackend.py +0 -0
  151. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0032_inference_timeout_floor.py +0 -0
  152. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0033_resolutionproposal.py +0 -0
  153. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0034_tracedeviceresolution.py +0 -0
  154. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0035_retire_superseded_proposals.py +0 -0
  155. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/__init__.py +0 -0
  156. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/navigation.py +0 -0
  157. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/netbox_reader.py +0 -0
  158. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/object_permissions.py +0 -0
  159. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/profile_yaml.py +0 -0
  160. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/proposal_contract.py +0 -0
  161. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/proposal_response.py +0 -0
  162. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/signals.py +0 -0
  163. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/source_resolution.py +0 -0
  164. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/source_trace.py +0 -0
  165. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/split_name_modal.js +0 -0
  166. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/trace_device_picker.js +0 -0
  167. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/trace_termination_picker.js +0 -0
  168. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/template_content.py +0 -0
  169. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/_import_progress.html +0 -0
  170. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/_proposal_action.html +0 -0
  171. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/analysis.html +0 -0
  172. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/bulk_yaml_import.html +0 -0
  173. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/cableclassmapping_edit.html +0 -0
  174. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/classrolemapping_edit.html +0 -0
  175. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/columnmapping_edit.html +0 -0
  176. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/columntransformrule_edit.html +0 -0
  177. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/device_import_data.html +0 -0
  178. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/devicetypemapping_edit.html +0 -0
  179. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/import_progress.html +0 -0
  180. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/import_results.html +0 -0
  181. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/import_setup.html +0 -0
  182. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/importexecution_list.html +0 -0
  183. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/importprofile.html +0 -0
  184. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/importprofile_edit.html +0 -0
  185. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/importprofile_list.html +0 -0
  186. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/inferencebackend.html +0 -0
  187. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/source_resolution_list.html +0 -0
  188. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/termination_proposal.py +0 -0
  189. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/__init__.py +0 -0
  190. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/conftest.py +0 -0
  191. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/fixtures/combined_name.xlsx +0 -0
  192. /netbox_data_import-2.4.0/netbox_data_import/tests/fixtures/sample_cans.xlsx → /netbox_data_import-2.6.0/netbox_data_import/tests/fixtures/sample_workbook.xlsx +0 -0
  193. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/fixtures/trace_copper.xlsx +0 -0
  194. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/fixtures/trace_fiber.xlsx +0 -0
  195. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/inference_http.py +0 -0
  196. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/isolated_settings.py +0 -0
  197. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/mixins.py +0 -0
  198. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/netbox_configuration.py +0 -0
  199. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/parallel.py +0 -0
  200. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/redaction.py +0 -0
  201. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_adapter_config.py +0 -0
  202. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_api.py +0 -0
  203. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_configuration_surfaces.py +0 -0
  204. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_contact_role_suggestion.py +0 -0
  205. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_cutover_edge_contracts.py +0 -0
  206. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_devcontainer_e2e.py +0 -0
  207. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_device_module.py +0 -0
  208. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_digest_indexed_models.py +0 -0
  209. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_graphql_filters.py +0 -0
  210. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_import_engine.py +0 -0
  211. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_import_execution.py +0 -0
  212. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_import_plan.py +0 -0
  213. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_inference_backend.py +0 -0
  214. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_inference_credentials.py +0 -0
  215. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_inference_secret_containment.py +0 -0
  216. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_inference_settings.py +0 -0
  217. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_ip_target.py +0 -0
  218. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_model_base_conventions.py +0 -0
  219. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_models.py +0 -0
  220. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_navigation.py +0 -0
  221. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_netbox_reader.py +0 -0
  222. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_object_permission_writer.py +0 -0
  223. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_parallel_test_setup.py +0 -0
  224. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_preview_state_ownership.py +0 -0
  225. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_profile_policy_crud_scoping.py +0 -0
  226. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_proposal_response.py +0 -0
  227. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_quick_action_object_permissions.py +0 -0
  228. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_quick_action_profile_id.py +0 -0
  229. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_rack_module.py +0 -0
  230. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_remove_device_type_creation_config_migration.py +0 -0
  231. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_resolution_proposal_api.py +0 -0
  232. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_serial_display.py +0 -0
  233. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_source_adapter.py +0 -0
  234. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_source_trace_contract.py +0 -0
  235. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_standard_views.py +0 -0
  236. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_target_module_edge_contracts.py +0 -0
  237. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_views_coverage2.py +0 -0
  238. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_views_extra.py +0 -0
  239. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/trace_device_resolution.py +0 -0
  240. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/trace_schema.py +0 -0
  241. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/transform_regex.py +0 -0
  242. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/utils.py +0 -0
  243. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import/values.py +0 -0
  244. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import.egg-info/dependency_links.txt +0 -0
  245. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import.egg-info/requires.txt +0 -0
  246. {netbox_data_import-2.4.0 → netbox_data_import-2.6.0}/netbox_data_import.egg-info/top_level.txt +0 -0
  247. {netbox_data_import-2.4.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.4.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
@@ -39,14 +39,14 @@ A NetBox plugin for importing device inventory and rack layouts from external DC
39
39
 
40
40
  This plugin allows you to:
41
41
 
42
- - Define **import profiles** that configure how your source data (Excel/CSV) maps to NetBox objects
42
+ - Define **import profiles** that configure how your source data (Excel) maps to NetBox objects
43
43
  - Configure **field mappings** per column with transformation rules
44
44
  - Define **class/role mappings** to translate your source system's device categories to NetBox roles
45
45
  - Synchronize a mapped **primary contact** into native NetBox Contacts and Contact Assignments
46
46
  - **Preview** the import before applying changes
47
47
  - **Run imports in the background**, track processed rows, and resume the progress page
48
48
 
49
- Currently supports importing from Excel (`.xlsx`) files in the CANS format, with the mapping configuration stored in NetBox so it can be reused and adapted for other source formats.
49
+ Currently supports importing from Excel (`.xlsx`) workbooks with a flat, one-row-per-device layout. The mapping configuration is stored in NetBox, so it can be reused and adapted for other source formats.
50
50
 
51
51
  ## Requirements
52
52
 
@@ -16,14 +16,14 @@ A NetBox plugin for importing device inventory and rack layouts from external DC
16
16
 
17
17
  This plugin allows you to:
18
18
 
19
- - Define **import profiles** that configure how your source data (Excel/CSV) maps to NetBox objects
19
+ - Define **import profiles** that configure how your source data (Excel) maps to NetBox objects
20
20
  - Configure **field mappings** per column with transformation rules
21
21
  - Define **class/role mappings** to translate your source system's device categories to NetBox roles
22
22
  - Synchronize a mapped **primary contact** into native NetBox Contacts and Contact Assignments
23
23
  - **Preview** the import before applying changes
24
24
  - **Run imports in the background**, track processed rows, and resume the progress page
25
25
 
26
- Currently supports importing from Excel (`.xlsx`) files in the CANS format, with the mapping configuration stored in NetBox so it can be reused and adapted for other source formats.
26
+ Currently supports importing from Excel (`.xlsx`) workbooks with a flat, one-row-per-device layout. The mapping configuration is stored in NetBox, so it can be reused and adapted for other source formats.
27
27
 
28
28
  ## Requirements
29
29
 
@@ -4,7 +4,7 @@ from typing import Any
4
4
 
5
5
  from netbox.plugins import PluginConfig
6
6
 
7
- __version__ = "2.4.0"
7
+ __version__ = "2.6.0"
8
8
 
9
9
 
10
10
  class NetBoxDataImportConfig(PluginConfig):
@@ -0,0 +1,31 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: 2026 Marcin Zieba <marcinpsk@gmail.com>
3
+ """Resolve one host with libc in a deadline-bounded child process."""
4
+
5
+ import json
6
+ import signal
7
+ import socket
8
+ import sys
9
+
10
+ from typing import TextIO
11
+
12
+
13
+ def main(stdin: TextIO, stdout: TextIO) -> int:
14
+ """Read one request from stdin and write its ordered addresses to stdout."""
15
+ try:
16
+ host, port, timeout = json.load(stdin)
17
+ except Exception: # noqa: BLE001 - the parent exposes one typed resolution failure
18
+ return 1
19
+ # The alarm is the deadline. An unarmed worker must crash, not read as a failed lookup.
20
+ signal.setitimer(signal.ITIMER_REAL, timeout)
21
+ try:
22
+ answers = socket.getaddrinfo(host, port, proto=socket.IPPROTO_TCP)
23
+ addresses = list(dict.fromkeys(str(answer[4][0]) for answer in answers))
24
+ except Exception: # noqa: BLE001 - the parent exposes one typed resolution failure
25
+ return 1
26
+ json.dump(addresses, stdout)
27
+ return 0
28
+
29
+
30
+ if __name__ == "__main__": # pragma: no cover - child entry point; main() is tested in process
31
+ raise SystemExit(main(sys.stdin, sys.stdout))
@@ -91,7 +91,7 @@ class FlatWorkbookConfigForm(AdapterConfigForm):
91
91
  max_length=100,
92
92
  required=False,
93
93
  initial="",
94
- help_text="NetBox custom field name to store the source ID in (e.g. 'cans_id')",
94
+ help_text="NetBox custom field name to store the source ID in (e.g. 'external_id')",
95
95
  )
96
96
  update_existing = forms.BooleanField(
97
97
  required=False,
@@ -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
+ )