netbox-data-import 2.2.0__tar.gz → 2.4.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 (243) hide show
  1. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/PKG-INFO +1 -1
  2. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/__init__.py +1 -1
  3. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/adapters.py +53 -2
  4. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/api/serializers.py +60 -12
  5. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/api/urls.py +4 -0
  6. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/api/views.py +107 -50
  7. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/cable_target.py +39 -43
  8. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/catalog.py +1 -0
  9. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/forms.py +24 -3
  10. netbox_data_import-2.4.0/netbox_data_import/graphql/schema.py +30 -0
  11. netbox_data_import-2.4.0/netbox_data_import/graphql/types.py +42 -0
  12. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/import_engine.py +3 -1
  13. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/inference_adapter.py +147 -48
  14. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/inference_backend.py +18 -0
  15. netbox_data_import-2.4.0/netbox_data_import/inference_connection_test.py +146 -0
  16. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/inference_credentials.py +71 -20
  17. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/inference_settings.py +3 -4
  18. netbox_data_import-2.4.0/netbox_data_import/inference_transport.py +349 -0
  19. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/ip_assignment.py +72 -0
  20. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/jobs.py +0 -23
  21. netbox_data_import-2.4.0/netbox_data_import/migrations/0034_tracedeviceresolution.py +41 -0
  22. netbox_data_import-2.4.0/netbox_data_import/migrations/0035_retire_superseded_proposals.py +28 -0
  23. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/models.py +71 -11
  24. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/netbox_reader.py +6 -0
  25. netbox_data_import-2.4.0/netbox_data_import/object_permissions.py +677 -0
  26. netbox_data_import-2.4.0/netbox_data_import/profile_yaml.py +458 -0
  27. netbox_data_import-2.4.0/netbox_data_import/proposal_contract.py +30 -0
  28. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/proposal_jobs.py +12 -17
  29. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/proposal_response.py +36 -16
  30. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/review_workspace.py +74 -12
  31. netbox_data_import-2.4.0/netbox_data_import/source_trace.py +124 -0
  32. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/static/netbox_data_import/js/sync_row_modal.js +2 -1
  33. netbox_data_import-2.4.0/netbox_data_import/static/netbox_data_import/js/trace_device_picker.js +130 -0
  34. netbox_data_import-2.4.0/netbox_data_import/static/netbox_data_import/js/trace_picker_search.js +54 -0
  35. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/static/netbox_data_import/js/trace_termination_picker.js +16 -37
  36. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/target_modules.py +244 -78
  37. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/import_preview.html +43 -3
  38. netbox_data_import-2.4.0/netbox_data_import/templates/netbox_data_import/importprofile_edit.html +45 -0
  39. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/inferencebackend.html +20 -0
  40. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/trace_workspace.html +118 -2
  41. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/termination_proposal.py +14 -6
  42. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/helpers.py +54 -7
  43. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/inference_http.py +7 -2
  44. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/mock_discipline.py +2 -1
  45. netbox_data_import-2.4.0/netbox_data_import/tests/redaction.py +54 -0
  46. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_atomic_exits.py +7 -0
  47. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_cable_module.py +68 -3
  48. netbox_data_import-2.4.0/netbox_data_import/tests/test_configuration_docs.py +55 -0
  49. netbox_data_import-2.4.0/netbox_data_import/tests/test_configuration_surfaces.py +1250 -0
  50. netbox_data_import-2.4.0/netbox_data_import/tests/test_conflict_row_actions.py +241 -0
  51. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_contact_sync.py +2 -43
  52. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_cutover_edge_contracts.py +10 -60
  53. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_device_module.py +190 -15
  54. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_digest_indexed_models.py +18 -1
  55. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_import_cutover.py +95 -23
  56. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_import_engine.py +109 -2
  57. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_import_execution.py +105 -8
  58. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_inference_adapter.py +431 -3
  59. netbox_data_import-2.4.0/netbox_data_import/tests/test_inference_connection_test.py +666 -0
  60. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_inference_credentials.py +80 -50
  61. netbox_data_import-2.4.0/netbox_data_import/tests/test_inference_secret_containment.py +338 -0
  62. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_inference_settings.py +6 -3
  63. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_migrations.py +1 -0
  64. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_mock_discipline.py +11 -0
  65. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_module_boundaries.py +55 -0
  66. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_object_permission_writer.py +609 -3
  67. netbox_data_import-2.4.0/netbox_data_import/tests/test_policy_write_refusals.py +155 -0
  68. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_pristine_preview_rows.py +2 -18
  69. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_proposal_jobs.py +82 -4
  70. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_proposal_response.py +36 -16
  71. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_proposal_tasks.py +12 -2
  72. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_proposal_workspace.py +19 -2
  73. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_quick_action_input_bounds.py +18 -0
  74. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_rack_module.py +53 -1
  75. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_resolution_proposal_api.py +4 -3
  76. netbox_data_import-2.4.0/netbox_data_import/tests/test_retire_superseded_proposals_migration.py +173 -0
  77. netbox_data_import-2.4.0/netbox_data_import/tests/test_source_trace_contract.py +66 -0
  78. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_standard_views.py +24 -2
  79. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_target_field_catalog.py +4 -4
  80. netbox_data_import-2.4.0/netbox_data_import/tests/test_target_module_edge_contracts.py +1069 -0
  81. netbox_data_import-2.4.0/netbox_data_import/tests/test_trace_device_resolution.py +843 -0
  82. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_trace_workbook.py +0 -29
  83. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_trace_workspace.py +161 -14
  84. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_views.py +113 -56
  85. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_views_coverage2.py +12 -21
  86. netbox_data_import-2.4.0/netbox_data_import/trace_device_resolution.py +482 -0
  87. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/trace_workbook.py +19 -104
  88. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/urls.py +11 -0
  89. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/views.py +459 -453
  90. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import.egg-info/PKG-INFO +1 -1
  91. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import.egg-info/SOURCES.txt +17 -1
  92. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/pyproject.toml +6 -2
  93. netbox_data_import-2.2.0/netbox_data_import/graphql/schema.py +0 -22
  94. netbox_data_import-2.2.0/netbox_data_import/graphql/types.py +0 -23
  95. netbox_data_import-2.2.0/netbox_data_import/inference_connection_test.py +0 -73
  96. netbox_data_import-2.2.0/netbox_data_import/inference_transport.py +0 -130
  97. netbox_data_import-2.2.0/netbox_data_import/object_permissions.py +0 -340
  98. netbox_data_import-2.2.0/netbox_data_import/tests/test_inference_connection_test.py +0 -452
  99. netbox_data_import-2.2.0/netbox_data_import/tests/test_inference_secret_containment.py +0 -216
  100. netbox_data_import-2.2.0/netbox_data_import/tests/test_legacy_profile_yaml.py +0 -171
  101. netbox_data_import-2.2.0/netbox_data_import/tests/test_target_module_edge_contracts.py +0 -449
  102. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/LICENSE +0 -0
  103. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/README.md +0 -0
  104. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/adapter_config.py +0 -0
  105. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/adapter_forms.py +0 -0
  106. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/api/__init__.py +0 -0
  107. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/contact_resolution.py +0 -0
  108. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/device_field_review.py +0 -0
  109. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/device_identity.py +0 -0
  110. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/field_keys.py +0 -0
  111. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/filters.py +0 -0
  112. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/flat_workbook.py +0 -0
  113. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/graphql/__init__.py +0 -0
  114. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/graphql/filters.py +0 -0
  115. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/inference_trust.py +0 -0
  116. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0001_initial.py +0 -0
  117. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0002_importjob.py +0 -0
  118. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0003_remove_classrolemapping_unique_class_role_mapping_per_profile_and_more.py +0 -0
  119. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0004_features.py +0 -0
  120. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0005_deviceexistingmatch.py +0 -0
  121. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0006_deviceexistingmatch_asset_tag.py +0 -0
  122. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0007_manufacturermapping.py +0 -0
  123. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0008_unique_constraints.py +0 -0
  124. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0009_add_rack_type_to_classrolemapping.py +0 -0
  125. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0010_alter_manufacturermapping_id.py +0 -0
  126. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0011_importprofile_capture_extra_data.py +0 -0
  127. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0012_classrolemapping_rack_type_related_name.py +0 -0
  128. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0013_remove_columnmapping_unique_target.py +0 -0
  129. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0014_alter_columnmapping_target_field_and_more.py +0 -0
  130. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0015_cleanup_duplicate_device_matches.py +0 -0
  131. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0016_deviceexistingmatch_ndi_devicematch_profile_device.py +0 -0
  132. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0017_native_contact_sync.py +0 -0
  133. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0018_ignoredfielddifference.py +0 -0
  134. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0019_deviceimportsource.py +0 -0
  135. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0020_migrate_import_source_custom_field.py +0 -0
  136. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0021_importprofile_adapter_config.py +0 -0
  137. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0022_migrate_profile_adapter_config.py +0 -0
  138. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0023_drop_moved_profile_columns.py +0 -0
  139. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0024_rename_importjob_importexecution.py +0 -0
  140. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0025_remove_importexecution_dry_run_and_more.py +0 -0
  141. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0026_alter_sourcedocument_profile.py +0 -0
  142. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0027_cableclassmapping_terminationresolution.py +0 -0
  143. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0028_cableimportsource.py +0 -0
  144. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0029_alter_cableimportsource_from_text_and_more.py +0 -0
  145. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0030_remove_device_type_creation_config.py +0 -0
  146. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0031_inferencebackend.py +0 -0
  147. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0032_inference_timeout_floor.py +0 -0
  148. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0033_resolutionproposal.py +0 -0
  149. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/__init__.py +0 -0
  150. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/navigation.py +0 -0
  151. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/plan.py +0 -0
  152. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/preview_row_actions.py +0 -0
  153. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/proposal_decisions.py +0 -0
  154. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/proposal_presentation.py +0 -0
  155. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/proposal_tasks.py +0 -0
  156. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/resolution_proposals.py +0 -0
  157. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/signals.py +0 -0
  158. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/source_resolution.py +0 -0
  159. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/static/netbox_data_import/js/conflict_modal.js +0 -0
  160. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/static/netbox_data_import/js/contact_candidate_modal.js +0 -0
  161. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/static/netbox_data_import/js/preview_row_actions.js +0 -0
  162. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/static/netbox_data_import/js/preview_row_controls.js +0 -0
  163. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/static/netbox_data_import/js/split_name_modal.js +0 -0
  164. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/static/netbox_data_import/js/trace_proposals.js +0 -0
  165. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tables.py +0 -0
  166. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/target_runtime.py +0 -0
  167. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/template_content.py +0 -0
  168. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/_import_progress.html +0 -0
  169. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/_proposal_action.html +0 -0
  170. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/_proposal_card.html +0 -0
  171. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/analysis.html +0 -0
  172. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/bulk_yaml_import.html +0 -0
  173. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/cableclassmapping_edit.html +0 -0
  174. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/classrolemapping_edit.html +0 -0
  175. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/columnmapping_edit.html +0 -0
  176. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/columntransformrule_edit.html +0 -0
  177. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/device_import_data.html +0 -0
  178. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/devicetypemapping_edit.html +0 -0
  179. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/import_profile_yaml.html +0 -0
  180. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/import_progress.html +0 -0
  181. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/import_results.html +0 -0
  182. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/import_setup.html +0 -0
  183. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/importexecution_list.html +0 -0
  184. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/importprofile.html +0 -0
  185. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/importprofile_list.html +0 -0
  186. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/source_resolution_list.html +0 -0
  187. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/__init__.py +0 -0
  188. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/conftest.py +0 -0
  189. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/fixtures/combined_name.xlsx +0 -0
  190. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/fixtures/sample_cans.xlsx +0 -0
  191. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/fixtures/trace_copper.xlsx +0 -0
  192. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/fixtures/trace_fiber.xlsx +0 -0
  193. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/isolated_settings.py +0 -0
  194. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/mixins.py +0 -0
  195. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/netbox_configuration.py +0 -0
  196. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/parallel.py +0 -0
  197. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_adapter_config.py +0 -0
  198. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_adapter_config_migration.py +0 -0
  199. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_api.py +0 -0
  200. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_ci_configuration.py +0 -0
  201. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_comment_style.py +0 -0
  202. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_contact_resolution_ajax.py +0 -0
  203. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_contact_role_suggestion.py +0 -0
  204. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_devcontainer_e2e.py +0 -0
  205. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_execution_audit.py +0 -0
  206. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_graphql_filters.py +0 -0
  207. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_import_plan.py +0 -0
  208. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_import_source_migration.py +0 -0
  209. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_inference_backend.py +0 -0
  210. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_inference_trust.py +0 -0
  211. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_installation_docs.py +0 -0
  212. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_ip_target.py +0 -0
  213. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_misc_coverage.py +0 -0
  214. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_model_base_conventions.py +0 -0
  215. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_models.py +0 -0
  216. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_navigation.py +0 -0
  217. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_netbox_reader.py +0 -0
  218. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_parallel_test_setup.py +0 -0
  219. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_placement_reference_locks.py +0 -0
  220. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_preview_assets.py +0 -0
  221. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_preview_state_ownership.py +0 -0
  222. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_profile_policy_crud_scoping.py +0 -0
  223. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_proposal_decisions.py +0 -0
  224. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_quick_action_object_permissions.py +0 -0
  225. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_quick_action_profile_id.py +0 -0
  226. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_release_metadata.py +0 -0
  227. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_remove_device_type_creation_config_migration.py +0 -0
  228. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_resolution_proposals.py +0 -0
  229. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_review_actions.py +0 -0
  230. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_review_resolutions.py +0 -0
  231. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_review_workspace.py +0 -0
  232. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_serial_display.py +0 -0
  233. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_source_adapter.py +0 -0
  234. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_trace_decisions.py +0 -0
  235. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_views_extra.py +0 -0
  236. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/trace_schema.py +0 -0
  237. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/transform_regex.py +0 -0
  238. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/utils.py +0 -0
  239. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import/values.py +0 -0
  240. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import.egg-info/dependency_links.txt +0 -0
  241. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import.egg-info/requires.txt +0 -0
  242. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/netbox_data_import.egg-info/top_level.txt +0 -0
  243. {netbox_data_import-2.2.0 → netbox_data_import-2.4.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netbox-data-import
3
- Version: 2.2.0
3
+ Version: 2.4.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.2.0"
7
+ __version__ = "2.4.0"
8
8
 
9
9
 
10
10
  class NetBoxDataImportConfig(PluginConfig):
@@ -13,13 +13,14 @@ the NetBox boundary.
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
- from dataclasses import dataclass, field
16
+ from dataclasses import dataclass, field, replace
17
17
  from typing import TYPE_CHECKING
18
18
 
19
19
  from .catalog import OutputKind, has_implemented_module
20
+ from .values import identity_text
20
21
 
21
22
  if TYPE_CHECKING:
22
- from .trace_workbook import SourceTrace
23
+ from .source_trace import SourceTrace
23
24
 
24
25
 
25
26
  class UnknownSourceAdapter(Exception):
@@ -39,6 +40,16 @@ class SourceDiagnostic:
39
40
  row_number: int | None = None
40
41
 
41
42
 
43
+ def _foreign_row_number(row) -> int | None:
44
+ """Return the source row a foreign row states, so its diagnostic can name a location."""
45
+ provenance = getattr(row, "provenance", ())
46
+ if provenance:
47
+ return provenance[0].row_start
48
+ if isinstance(row, dict):
49
+ return row.get("_row_number")
50
+ return None
51
+
52
+
42
53
  @dataclass(frozen=True)
43
54
  class SourceBatch:
44
55
  """The typed source items and source diagnostics from one file (section 1)."""
@@ -48,6 +59,46 @@ class SourceBatch:
48
59
  diagnostics: tuple[SourceDiagnostic, ...] = ()
49
60
  unused_columns: dict[str, dict] = field(default_factory=dict)
50
61
 
62
+ def __post_init__(self):
63
+ """Exclude a row the output kinds cannot carry, and mark a Trace with empty Device references."""
64
+ from .source_trace import SourceTrace
65
+
66
+ trace_batch = OutputKind.SOURCE_TRACE in self.output_kinds
67
+ expected = SourceTrace if trace_batch else dict
68
+ carries = "Source Traces" if trace_batch else "source rows"
69
+ rows = []
70
+ diagnostics = list(self.diagnostics)
71
+ for row in self.rows:
72
+ if not isinstance(row, expected):
73
+ # Excluded here, because every reader below reads the row the output kinds declare.
74
+ diagnostics.append(
75
+ SourceDiagnostic(
76
+ code="source.row_type_unexpected",
77
+ message=f"This batch carries {carries} only; got {type(row).__name__}.",
78
+ row_number=_foreign_row_number(row),
79
+ )
80
+ )
81
+ continue
82
+ if not trace_batch:
83
+ rows.append(row)
84
+ continue
85
+ summary = row.endpoint_summary
86
+ references = [summary.from_termination, summary.to_termination, *row.corroboration]
87
+ for segment in row.segments:
88
+ references.extend((segment.left, segment.right))
89
+ if all(identity_text(reference.device) for reference in references):
90
+ rows.append(row)
91
+ continue
92
+ diagnostic = SourceDiagnostic(
93
+ code="trace.device_required",
94
+ message="A Source Trace Termination Reference must name a Device.",
95
+ row_number=row.provenance[0].row_start,
96
+ )
97
+ diagnostics.append(diagnostic)
98
+ rows.append(replace(row, errors=(*row.errors, diagnostic)))
99
+ object.__setattr__(self, "rows", tuple(rows))
100
+ object.__setattr__(self, "diagnostics", tuple(diagnostics))
101
+
51
102
 
52
103
  class SourceAdapter:
53
104
  """Base class for a Source Adapter declaration."""
@@ -10,8 +10,11 @@ from rest_framework import serializers
10
10
 
11
11
  from ..adapters import DEFAULT_ADAPTER_KEY, get_adapter, selectable_adapter_choices
12
12
  from ..catalog import CATALOG
13
+ from ..forms import InferenceBackendForm
13
14
  from ..models import (
14
15
  ImportProfile,
16
+ CableClassMapping,
17
+ CableImportSource,
15
18
  ColumnMapping,
16
19
  ClassRoleMapping,
17
20
  DeviceTypeMapping,
@@ -63,6 +66,17 @@ class PolicySectionSerializer(PolicySectionApplicabilityMixin, ValidatedModelSer
63
66
  def validate_policy_row(self, attrs):
64
67
  """Check the fields this model resolves through the catalog. Subclasses override."""
65
68
 
69
+ def model_cleaned_values(self):
70
+ """Return request values after applying the model's canonical normalization."""
71
+ values = dict(self.validated_data)
72
+ if self.instance is None:
73
+ instance = self.Meta.model(**values)
74
+ instance.full_clean(validate_unique=False)
75
+ else:
76
+ # ValidatedModelSerializer.validate() applies request values and full_clean() to this instance.
77
+ instance = self.instance
78
+ return {name: getattr(instance, name) for name in values}
79
+
66
80
 
67
81
  def _validate_target_keys(instance, attrs, names, *, allow_candidates=True, required=False):
68
82
  """Reject a target key the profile's Source Adapter cannot supply."""
@@ -145,6 +159,24 @@ class ColumnMappingSerializer(PolicySectionSerializer):
145
159
  _validate_target_keys(self.instance, attrs, ("target_field",), required=True)
146
160
 
147
161
 
162
+ class CableClassMappingSerializer(PolicySectionSerializer):
163
+ """Serializer for CableClassMapping policy decisions."""
164
+
165
+ class Meta:
166
+ model = CableClassMapping
167
+ fields = [
168
+ "id",
169
+ "url",
170
+ "display",
171
+ "profile",
172
+ "cable_class",
173
+ "cable_type_resolved",
174
+ "cable_type",
175
+ "cable_profile_resolved",
176
+ "cable_profile",
177
+ ]
178
+
179
+
148
180
  class _RackTypeSlugField(serializers.SlugRelatedField):
149
181
  """SlugRelatedField for RackType that defers the queryset import."""
150
182
 
@@ -282,6 +314,30 @@ class ImportExecutionSerializer(serializers.ModelSerializer):
282
314
  read_only_fields = fields
283
315
 
284
316
 
317
+ class CableImportSourceSerializer(serializers.ModelSerializer):
318
+ """Read-only serializer for one Cable provenance record."""
319
+
320
+ class Meta:
321
+ model = CableImportSource
322
+ fields = [
323
+ "id",
324
+ "cable",
325
+ "profile",
326
+ "trace_identity",
327
+ "segment_index",
328
+ "from_text",
329
+ "to_text",
330
+ "direction",
331
+ "workbook_fingerprint",
332
+ "sheet",
333
+ "block_ordinal",
334
+ "row_start",
335
+ "row_end",
336
+ "export_timestamp",
337
+ ]
338
+ read_only_fields = fields
339
+
340
+
285
341
  class ResolutionProposalSerializer(serializers.ModelSerializer):
286
342
  """Read-only serializer for the Resolution Proposal audit record."""
287
343
 
@@ -328,28 +384,20 @@ class ResolutionProposalHistorySerializer(serializers.ModelSerializer):
328
384
 
329
385
 
330
386
  class InferenceBackendSerializer(NetBoxModelSerializer):
331
- """Serialize one Inference Backend row, without its credential reference.
387
+ """Serialize one Inference Backend row with a write-only credential reference.
332
388
 
333
389
  NetBox resolves this class by model name to freeze a delete event payload.
334
390
  """
335
391
 
392
+ credential_reference = serializers.JSONField(write_only=True)
393
+
336
394
  class Meta:
337
395
  model = InferenceBackend
338
396
  fields = [
339
397
  "id",
340
398
  "url",
341
399
  "display",
342
- "backend_key",
343
- "display_name",
344
- "adapter_type",
345
- "api_root",
346
- "model",
347
- "authentication",
348
- "response_mode",
349
- "connect_timeout",
350
- "read_timeout",
351
- "enabled",
352
- "tags",
400
+ *InferenceBackendForm.Meta.fields,
353
401
  "custom_fields",
354
402
  "created",
355
403
  "last_updated",
@@ -7,6 +7,8 @@ from netbox.api.routers import NetBoxRouter
7
7
 
8
8
  from .views import (
9
9
  ImportProfileViewSet,
10
+ CableClassMappingViewSet,
11
+ CableImportSourceViewSet,
10
12
  ColumnMappingViewSet,
11
13
  ClassRoleMappingViewSet,
12
14
  DeviceTypeMappingViewSet,
@@ -21,6 +23,8 @@ from .views import (
21
23
 
22
24
  router = NetBoxRouter()
23
25
  router.register("profiles", ImportProfileViewSet)
26
+ router.register("cable-class-mappings", CableClassMappingViewSet)
27
+ router.register("cable-provenance", CableImportSourceViewSet)
24
28
  router.register("column-mappings", ColumnMappingViewSet)
25
29
  router.register("class-role-mappings", ClassRoleMappingViewSet)
26
30
  router.register("device-type-mappings", DeviceTypeMappingViewSet)
@@ -3,16 +3,17 @@
3
3
  """DRF viewsets for the data-import plugin API."""
4
4
 
5
5
  from django.http import Http404
6
- from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
6
+ from netbox.api.viewsets import NetBoxModelViewSet
7
7
  from rest_framework import mixins, permissions, viewsets
8
- from rest_framework.exceptions import ValidationError
8
+ from rest_framework.exceptions import PermissionDenied, ValidationError
9
9
  from rest_framework.permissions import DjangoModelPermissions
10
10
 
11
11
  from ..field_keys import SELECT_TERMINATION_TASK, parse_termination_field_key
12
12
  from ..models import (
13
13
  locked_profile_policy,
14
- locked_resolution_policy,
15
14
  ImportProfile,
15
+ CableClassMapping,
16
+ CableImportSource,
16
17
  ColumnMapping,
17
18
  ClassRoleMapping,
18
19
  DeviceTypeMapping,
@@ -23,8 +24,15 @@ from ..models import (
23
24
  InferenceBackend,
24
25
  ResolutionProposal,
25
26
  )
27
+ from ..object_permissions import (
28
+ ObjectPermissionDenied,
29
+ delete_permission_scoped_objects,
30
+ save_permission_scoped_object,
31
+ )
26
32
  from .serializers import (
27
33
  ImportProfileSerializer,
34
+ CableClassMappingSerializer,
35
+ CableImportSourceSerializer,
28
36
  ColumnMappingSerializer,
29
37
  ClassRoleMappingSerializer,
30
38
  DeviceTypeMappingSerializer,
@@ -84,6 +92,72 @@ class _PluginModelViewSet(_ProfileScopedQuerySetMixin, viewsets.ModelViewSet):
84
92
 
85
93
  permission_classes = [permissions.IsAuthenticated, DjangoModelPermissionsWithView]
86
94
 
95
+ def perform_create(self, serializer):
96
+ """Create one policy row inside its profile and object-permission scope."""
97
+ model = serializer.Meta.model
98
+ profile_id = serializer.validated_data["profile"].pk
99
+ try:
100
+ with locked_profile_policy(profile_id):
101
+ serializer.run_validation(serializer.initial_data)
102
+ values = serializer.model_cleaned_values()
103
+ profile = values.pop("profile")
104
+ result = save_permission_scoped_object(
105
+ self.request.user,
106
+ model,
107
+ {"pk": None, "profile": profile},
108
+ values,
109
+ on_existing="reject",
110
+ )
111
+ except ImportProfile.DoesNotExist:
112
+ raise Http404 from None
113
+ except ObjectPermissionDenied as exc:
114
+ raise PermissionDenied from exc
115
+ serializer.instance = result.instance
116
+
117
+ def perform_update(self, serializer):
118
+ """Update one policy row under every affected profile and object scope."""
119
+ model = serializer.Meta.model
120
+ row_pk = serializer.instance.pk
121
+ stored_profile_id = model.objects.filter(pk=row_pk).values_list("profile_id", flat=True).first()
122
+ if stored_profile_id is None:
123
+ raise Http404
124
+ requested_profile = serializer.validated_data.get("profile")
125
+ requested_profile_id = requested_profile.pk if requested_profile is not None else stored_profile_id
126
+ try:
127
+ with locked_profile_policy(stored_profile_id, requested_profile_id):
128
+ serializer.instance = model.objects.get(pk=row_pk, profile_id=stored_profile_id)
129
+ serializer.run_validation(serializer.initial_data)
130
+ result = save_permission_scoped_object(
131
+ self.request.user,
132
+ model,
133
+ {"pk": row_pk, "profile_id": stored_profile_id},
134
+ serializer.model_cleaned_values(),
135
+ )
136
+ except (model.DoesNotExist, ImportProfile.DoesNotExist):
137
+ raise Http404 from None
138
+ except ObjectPermissionDenied as exc:
139
+ raise PermissionDenied from exc
140
+ serializer.instance = result.instance
141
+
142
+ def perform_destroy(self, instance):
143
+ """Delete one policy row under its current profile and object scope."""
144
+ model = type(instance)
145
+ profile_id = model.objects.filter(pk=instance.pk).values_list("profile_id", flat=True).first()
146
+ if profile_id is None:
147
+ raise Http404
148
+ try:
149
+ with locked_profile_policy(profile_id):
150
+ deleted = delete_permission_scoped_objects(
151
+ self.request.user,
152
+ model.objects.filter(pk=instance.pk, profile_id=profile_id),
153
+ )
154
+ if deleted != 1:
155
+ raise Http404
156
+ except ImportProfile.DoesNotExist:
157
+ raise Http404 from None
158
+ except ObjectPermissionDenied as exc:
159
+ raise PermissionDenied from exc
160
+
87
161
 
88
162
  class ColumnMappingViewSet(_PluginModelViewSet):
89
163
  """CRUD viewset for ColumnMapping."""
@@ -92,6 +166,13 @@ class ColumnMappingViewSet(_PluginModelViewSet):
92
166
  serializer_class = ColumnMappingSerializer
93
167
 
94
168
 
169
+ class CableClassMappingViewSet(_PluginModelViewSet):
170
+ """CRUD viewset for CableClassMapping."""
171
+
172
+ queryset = CableClassMapping.objects.select_related("profile")
173
+ serializer_class = CableClassMappingSerializer
174
+
175
+
95
176
  class ClassRoleMappingViewSet(_PluginModelViewSet):
96
177
  """CRUD viewset for ClassRoleMapping."""
97
178
 
@@ -120,53 +201,12 @@ class ColumnTransformRuleViewSet(_PluginModelViewSet):
120
201
  serializer_class = ColumnTransformRuleSerializer
121
202
 
122
203
 
123
- def _revalidate_against_the_stored_row(serializer):
124
- """Read the resolution again and check the request against the row as it now stands.
125
-
126
- save() writes every field, so a request that changed another field first would otherwise be
127
- undone. The whole validation runs again rather than validate() alone, because the field checks
128
- read the stored row too, and the profile lock makes this reading of it authoritative. The
129
- result is discarded: the values are the request's own, which the first pass already holds.
130
- """
131
- serializer.instance = SourceResolution.objects.get(pk=serializer.instance.pk)
132
- serializer.run_validation(serializer.initial_data)
133
-
134
-
135
204
  class SourceResolutionViewSet(_PluginModelViewSet):
136
205
  """CRUD viewset for SourceResolution (rerere)."""
137
206
 
138
207
  queryset = SourceResolution.objects.select_related("profile")
139
208
  serializer_class = SourceResolutionSerializer
140
209
 
141
- # Each write serializes against an executing import, which holds the same profile row.
142
- def perform_create(self, serializer):
143
- """Create the resolution under the profile lock."""
144
- try:
145
- with locked_profile_policy(serializer.validated_data["profile"].pk):
146
- serializer.save()
147
- except ImportProfile.DoesNotExist:
148
- # The profile is read to validate the request, and can be deleted before the lock.
149
- raise Http404 from None
150
-
151
- def perform_update(self, serializer):
152
- """Update the resolution under its profile lock."""
153
- # ValidatedModelSerializer.validate() writes the request values onto the instance, so only
154
- # its primary key still names the stored row.
155
- try:
156
- with locked_resolution_policy(serializer.instance.pk):
157
- _revalidate_against_the_stored_row(serializer)
158
- serializer.save()
159
- except (SourceResolution.DoesNotExist, ImportProfile.DoesNotExist):
160
- raise Http404 from None
161
-
162
- def perform_destroy(self, instance):
163
- """Delete the resolution under its profile lock."""
164
- try:
165
- with locked_resolution_policy(instance.pk):
166
- instance.delete()
167
- except (SourceResolution.DoesNotExist, ImportProfile.DoesNotExist):
168
- raise Http404 from None
169
-
170
210
 
171
211
  class ImportExecutionViewSet(_ProfileScopedQuerySetMixin, viewsets.ReadOnlyModelViewSet):
172
212
  """Read-only viewset for the Import Execution audit history."""
@@ -176,6 +216,27 @@ class ImportExecutionViewSet(_ProfileScopedQuerySetMixin, viewsets.ReadOnlyModel
176
216
  permission_classes = [permissions.IsAuthenticated, DjangoModelPermissionsWithView]
177
217
 
178
218
 
219
+ class CableImportSourceViewSet(_ProfileScopedQuerySetMixin, viewsets.ReadOnlyModelViewSet):
220
+ """Read-only viewset for per-Cable import provenance."""
221
+
222
+ queryset = CableImportSource.objects.select_related("cable", "profile")
223
+ serializer_class = CableImportSourceSerializer
224
+ permission_classes = [permissions.IsAuthenticated, DjangoModelPermissionsWithView]
225
+
226
+ def get_queryset(self):
227
+ """Apply the profile scope and an optional Cable ID filter."""
228
+ profile_scope = ImportProfile.objects.restrict(self.request.user, "view")
229
+ qs = super().get_queryset().filter(profile__in=profile_scope)
230
+ cable_id = self.request.query_params.get("cable_id")
231
+ if cable_id is not None:
232
+ try:
233
+ cable_id = int(cable_id)
234
+ except (TypeError, ValueError) as exc:
235
+ raise ValidationError({"cable_id": "Enter a whole number."}) from exc
236
+ qs = qs.filter(cable_id=cable_id)
237
+ return qs
238
+
239
+
179
240
  class ResolutionProposalViewSet(_ProfileScopedQuerySetMixin, viewsets.ReadOnlyModelViewSet):
180
241
  """Read-only viewset for Resolution Proposal history."""
181
242
 
@@ -225,12 +286,8 @@ class ResolutionProposalHistoryViewSet(mixins.ListModelMixin, viewsets.GenericVi
225
286
  )
226
287
 
227
288
 
228
- class InferenceBackendViewSet(NetBoxReadOnlyModelViewSet):
229
- """Read-only viewset for Inference Backend rows.
230
-
231
- Read-only on purpose: a backend row carries the destination NetBox itself calls, and the UI form
232
- is the one place that validates the `api_root` trust boundary against the allowlist.
233
- """
289
+ class InferenceBackendViewSet(NetBoxModelViewSet):
290
+ """CRUD viewset whose configuration fields derive from the Inference Backend form."""
234
291
 
235
292
  queryset = InferenceBackend.objects.prefetch_related("tags")
236
293
  serializer_class = InferenceBackendSerializer
@@ -36,6 +36,13 @@ from .field_keys import (
36
36
  from .object_permissions import enforce_saved_object_permission
37
37
  from .plan import Diagnostic, Disposition, PlannedChange, Severity, SynchronizationUnit
38
38
  from .target_runtime import DeletedObject, PreconditionFailed
39
+ from .trace_device_resolution import (
40
+ STALE as DEVICE_STALE,
41
+ collect_trace_device_evidence,
42
+ resolve_trace_devices,
43
+ resolved_trace_device,
44
+ source_device_key,
45
+ )
39
46
  from .values import identity_text, source_text
40
47
 
41
48
  CABLE_STATUS = "connected"
@@ -207,7 +214,9 @@ class _TraceAnalysis:
207
214
  segments: list = field(default_factory=list)
208
215
  proven: dict = field(default_factory=dict)
209
216
  policies: dict = field(default_factory=dict)
217
+ devices: dict = field(default_factory=dict)
210
218
  terminations: dict = field(default_factory=dict)
219
+ resolution_started: bool = False
211
220
  topology_read: bool = False
212
221
  logical_cable: Any = None
213
222
  retains_logical_cable: bool = False
@@ -303,38 +312,19 @@ def _source_record(trace, segment_index: int) -> dict:
303
312
  }
304
313
 
305
314
 
306
- def _visible_devices(netbox_reader, names, *, _lock_rows=False) -> dict[str, list]:
307
- """Return the visible Devices at the import target, grouped by comparison name."""
308
- from django.db.models import Q
309
-
310
- wanted = {identity_text(name) for name in names if identity_text(name)}
311
- if not wanted:
312
- return {}
313
- devices = netbox_reader.devices()
314
- if netbox_reader.site is not None:
315
- devices = devices.filter(site=netbox_reader.site)
316
- if _lock_rows:
317
- devices = devices.order_by("pk").select_for_update(of=("self",))
318
- lookup = Q()
319
- for name in sorted(wanted):
320
- lookup |= Q(name__iexact=name)
321
- grouped: dict[str, list] = {}
322
- for device in devices.filter(lookup):
323
- comparison = identity_text(device.name)
324
- if comparison in wanted:
325
- grouped.setdefault(comparison, []).append(device)
326
- return grouped
327
-
328
-
329
- def resolved_device_for(field_key: str, netbox_reader, *, _lock_rows=False):
315
+ def resolved_device_for(field_key: str, netbox_reader, *, profile, _lock_rows=False):
330
316
  """Return the one visible Device a termination field key names, or None when it names no single one.
331
317
 
332
318
  A proposal freezes this reference and revalidates it, so the retrieval and the freshness check
333
319
  have to agree on what "the resolved Device" means.
334
320
  """
335
321
  device_name = parse_termination_field_key(field_key)["device"]
336
- devices = _visible_devices(netbox_reader, [device_name], _lock_rows=_lock_rows).get(identity_text(device_name), [])
337
- return devices[0] if len(devices) == 1 else None
322
+ return resolved_trace_device(
323
+ profile=profile,
324
+ reader=netbox_reader,
325
+ source_label=device_name,
326
+ lock_rows=_lock_rows,
327
+ )
338
328
 
339
329
 
340
330
  _RESOLVED_DEVICE_UNSET = object()
@@ -361,7 +351,7 @@ def eligible_terminations(
361
351
  kind = parsed["kind"]
362
352
  accessor = _READER_ACCESSOR_BY_KIND[kind]
363
353
  device = (
364
- resolved_device_for(field_key, netbox_reader, _lock_rows=_lock_rows)
354
+ resolved_device_for(field_key, netbox_reader, profile=profile, _lock_rows=_lock_rows)
365
355
  if _resolved_device is _RESOLVED_DEVICE_UNSET
366
356
  else _resolved_device
367
357
  )
@@ -430,6 +420,7 @@ class _CableBatch:
430
420
  """Plan every Source Trace in one batch, so an identical shared segment plans once."""
431
421
 
432
422
  def __init__(self, traces, profile, netbox_reader, *, lock_plan_references: bool = False):
423
+ traces = tuple(traces)
433
424
  self.profile = profile
434
425
  self.reader = netbox_reader
435
426
  self.actor = netbox_reader.actor
@@ -445,7 +436,14 @@ class _CableBatch:
445
436
  self._occupied: dict[tuple[str, int], _ExistingCable] = {}
446
437
  self._mapping_rows: dict[str, Any] | None = None
447
438
  self._stored = self._stored_resolutions()
448
- self._devices = self._load_devices()
439
+ active_traces = (analysis.trace for analysis in self.analyses if not analysis.stopped)
440
+ self._device_evidence = collect_trace_device_evidence(active_traces)
441
+ self._device_resolutions = resolve_trace_devices(
442
+ profile=self.profile,
443
+ reader=self.reader,
444
+ evidence=self._device_evidence,
445
+ lock_rows=self.lock_plan_references,
446
+ )
449
447
  self._resolve_terminations()
450
448
  self._load_mappings()
451
449
  self._build_segments()
@@ -512,16 +510,6 @@ class _CableBatch:
512
510
  ).select_related("selected_object_type")
513
511
  return {row.field_key: row for row in rows}
514
512
 
515
- def _load_devices(self) -> dict[str, list]:
516
- """Return the visible Devices every planned trace names, grouped by comparison name."""
517
- names = [
518
- reference.device
519
- for analysis in self.analyses
520
- if not analysis.stopped
521
- for reference in self._references(analysis.trace)
522
- ]
523
- return _visible_devices(self.reader, names)
524
-
525
513
  def _components_for(self, device_id: int, kind: str) -> dict[str, list]:
526
514
  """Return one Device's terminations of one kind, grouped by comparison name."""
527
515
  cached = self._components.get((device_id, kind))
@@ -537,6 +525,7 @@ class _CableBatch:
537
525
  for analysis in self.analyses:
538
526
  if analysis.stopped:
539
527
  continue
528
+ analysis.resolution_started = True
540
529
  resolved: dict[tuple, _Termination] = {}
541
530
  for reference in self._references(analysis.trace):
542
531
  if reference.identity_key in resolved:
@@ -548,19 +537,24 @@ class _CableBatch:
548
537
 
549
538
  def _resolve_one(self, analysis: _TraceAnalysis, reference) -> _Termination | None:
550
539
  """Return the NetBox object one Termination Reference names, or record the open decision."""
551
- devices = self._devices.get(identity_text(reference.device), [])
552
- if len(devices) != 1:
553
- analysis.block("trace.device_unresolved", {**_reference_display(reference), "matches": len(devices)})
540
+ resolution = self._device_resolutions[source_device_key(reference.device)]
541
+ analysis.devices.setdefault(resolution.evidence.key, resolution.to_question())
542
+ if resolution.device is None:
543
+ code = "trace.device_resolution_stale" if resolution.state == DEVICE_STALE else "trace.device_unresolved"
544
+ analysis.block(
545
+ code,
546
+ {**_reference_display(reference), "matches": resolution.exact_match_count},
547
+ )
554
548
  # With no resolved Device there is nothing to pick from, so the picker cannot help here.
555
549
  self._record_resolution(
556
550
  analysis,
557
551
  reference,
558
552
  UNRESOLVED,
559
553
  None,
560
- reason=f"The source names {len(devices)} matching Devices, so no port list applies.",
554
+ reason="Resolve the source Device before choosing one of its ports.",
561
555
  )
562
556
  return None
563
- device = devices[0]
557
+ device = resolution.device
564
558
  stored = self._stored.get(_field_key(reference))
565
559
  if stored is not None:
566
560
  termination = self._stored_termination(analysis, reference, device, stored)
@@ -1222,9 +1216,11 @@ class _CableBatch:
1222
1216
  },
1223
1217
  "segments": segments,
1224
1218
  "logical_cable": self._logical_cable_display(analysis),
1219
+ "resolution_started": analysis.resolution_started,
1225
1220
  "topology_known": analysis.topology_read,
1226
1221
  # A unit with no changes proposes nothing, so the panel must not offer to delete one.
1227
1222
  "deletes_logical_cable": writes and analysis.deleted_logical_cable is not None,
1223
+ "devices": list(analysis.devices.values()),
1228
1224
  "terminations": list(analysis.terminations.values()),
1229
1225
  }
1230
1226
 
@@ -259,6 +259,7 @@ POLICY_SECTIONS: tuple[PolicySection, ...] = (
259
259
  PolicySection("source_resolutions", "Source Resolutions", _DEVICE_ONLY),
260
260
  PolicySection("device_existing_matches", "Device Existing Matches", _DEVICE_ONLY),
261
261
  PolicySection("ignored_field_differences", "Ignored Field Differences", _DEVICE_ONLY),
262
+ PolicySection("trace_device_resolutions", "Trace Device Resolutions", _TRACE_ONLY),
262
263
  PolicySection("termination_resolutions", "Termination Resolutions", _TRACE_ONLY),
263
264
  PolicySection("cable_class_mappings", "CableClass Mappings", _TRACE_ONLY),
264
265
  )