netbox-data-import 2.0.0__tar.gz → 2.1.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 (199) hide show
  1. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/PKG-INFO +2 -1
  2. netbox_data_import-2.1.0/netbox_data_import/__init__.py +49 -0
  3. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/api/serializers.py +7 -2
  4. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/cable_target.py +175 -10
  5. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/field_keys.py +9 -4
  6. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/forms.py +27 -0
  7. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/import_engine.py +3 -4
  8. netbox_data_import-2.1.0/netbox_data_import/inference_adapter.py +260 -0
  9. netbox_data_import-2.1.0/netbox_data_import/inference_backend.py +162 -0
  10. netbox_data_import-2.1.0/netbox_data_import/inference_connection_test.py +73 -0
  11. netbox_data_import-2.1.0/netbox_data_import/inference_credentials.py +276 -0
  12. netbox_data_import-2.1.0/netbox_data_import/inference_settings.py +274 -0
  13. netbox_data_import-2.1.0/netbox_data_import/inference_trust.py +231 -0
  14. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/ip_assignment.py +2 -1
  15. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/jobs.py +24 -2
  16. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0001_initial.py +4 -1
  17. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0022_migrate_profile_adapter_config.py +1 -1
  18. netbox_data_import-2.1.0/netbox_data_import/migrations/0031_inferencebackend.py +52 -0
  19. netbox_data_import-2.1.0/netbox_data_import/migrations/0032_inference_timeout_floor.py +24 -0
  20. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/models.py +88 -8
  21. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/navigation.py +18 -0
  22. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/plan.py +2 -2
  23. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/preview_row_actions.py +77 -3
  24. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/review_workspace.py +189 -0
  25. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/source_resolution.py +7 -5
  26. netbox_data_import-2.1.0/netbox_data_import/static/netbox_data_import/js/trace_termination_picker.js +147 -0
  27. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tables.py +14 -0
  28. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/target_modules.py +9 -9
  29. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/import_preview.html +8 -0
  30. netbox_data_import-2.1.0/netbox_data_import/templates/netbox_data_import/inferencebackend.html +95 -0
  31. netbox_data_import-2.1.0/netbox_data_import/templates/netbox_data_import/trace_workspace.html +287 -0
  32. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/helpers.py +42 -3
  33. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/isolated_settings.py +2 -2
  34. netbox_data_import-2.1.0/netbox_data_import/tests/mock_discipline.py +560 -0
  35. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_adapter_config_migration.py +4 -4
  36. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_cable_module.py +258 -4
  37. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_contact_resolution_ajax.py +5 -4
  38. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_devcontainer_e2e.py +2 -0
  39. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_device_module.py +3 -3
  40. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_execution_audit.py +2 -2
  41. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_import_cutover.py +25 -2
  42. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_import_plan.py +1 -1
  43. netbox_data_import-2.1.0/netbox_data_import/tests/test_inference_adapter.py +432 -0
  44. netbox_data_import-2.1.0/netbox_data_import/tests/test_inference_backend.py +325 -0
  45. netbox_data_import-2.1.0/netbox_data_import/tests/test_inference_connection_test.py +452 -0
  46. netbox_data_import-2.1.0/netbox_data_import/tests/test_inference_credentials.py +403 -0
  47. netbox_data_import-2.1.0/netbox_data_import/tests/test_inference_secret_containment.py +202 -0
  48. netbox_data_import-2.1.0/netbox_data_import/tests/test_inference_settings.py +482 -0
  49. netbox_data_import-2.1.0/netbox_data_import/tests/test_inference_trust.py +255 -0
  50. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_migrations.py +111 -0
  51. netbox_data_import-2.1.0/netbox_data_import/tests/test_mock_discipline.py +865 -0
  52. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_module_boundaries.py +1 -1
  53. netbox_data_import-2.1.0/netbox_data_import/tests/test_navigation.py +121 -0
  54. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_placement_reference_locks.py +3 -3
  55. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_preview_assets.py +2 -2
  56. netbox_data_import-2.1.0/netbox_data_import/tests/test_preview_state_ownership.py +254 -0
  57. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_quick_action_input_bounds.py +7 -7
  58. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_remove_device_type_creation_config_migration.py +2 -3
  59. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_review_actions.py +3 -3
  60. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_review_resolutions.py +2 -2
  61. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_standard_views.py +2 -0
  62. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_target_module_edge_contracts.py +1 -1
  63. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_trace_workbook.py +29 -0
  64. netbox_data_import-2.1.0/netbox_data_import/tests/test_trace_workspace.py +1662 -0
  65. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_views.py +69 -17
  66. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_views_coverage2.py +10 -4
  67. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/trace_workbook.py +14 -8
  68. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/urls.py +32 -1
  69. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/views.py +544 -71
  70. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import.egg-info/PKG-INFO +2 -1
  71. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import.egg-info/SOURCES.txt +22 -0
  72. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import.egg-info/requires.txt +1 -0
  73. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/pyproject.toml +45 -4
  74. netbox_data_import-2.0.0/netbox_data_import/__init__.py +0 -28
  75. netbox_data_import-2.0.0/netbox_data_import/tests/test_navigation.py +0 -22
  76. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/LICENSE +0 -0
  77. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/README.md +0 -0
  78. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/adapter_config.py +0 -0
  79. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/adapter_forms.py +0 -0
  80. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/adapters.py +0 -0
  81. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/api/__init__.py +0 -0
  82. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/api/urls.py +0 -0
  83. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/api/views.py +0 -0
  84. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/catalog.py +0 -0
  85. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/contact_resolution.py +0 -0
  86. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/device_field_review.py +0 -0
  87. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/device_identity.py +0 -0
  88. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/filters.py +0 -0
  89. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/flat_workbook.py +0 -0
  90. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/graphql/__init__.py +0 -0
  91. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/graphql/filters.py +0 -0
  92. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/graphql/schema.py +0 -0
  93. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/graphql/types.py +0 -0
  94. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0002_importjob.py +0 -0
  95. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0003_remove_classrolemapping_unique_class_role_mapping_per_profile_and_more.py +0 -0
  96. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0004_features.py +0 -0
  97. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0005_deviceexistingmatch.py +0 -0
  98. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0006_deviceexistingmatch_asset_tag.py +0 -0
  99. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0007_manufacturermapping.py +0 -0
  100. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0008_unique_constraints.py +0 -0
  101. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0009_add_rack_type_to_classrolemapping.py +0 -0
  102. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0010_alter_manufacturermapping_id.py +0 -0
  103. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0011_importprofile_capture_extra_data.py +0 -0
  104. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0012_classrolemapping_rack_type_related_name.py +0 -0
  105. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0013_remove_columnmapping_unique_target.py +0 -0
  106. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0014_alter_columnmapping_target_field_and_more.py +0 -0
  107. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0015_cleanup_duplicate_device_matches.py +0 -0
  108. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0016_deviceexistingmatch_ndi_devicematch_profile_device.py +0 -0
  109. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0017_native_contact_sync.py +0 -0
  110. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0018_ignoredfielddifference.py +0 -0
  111. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0019_deviceimportsource.py +0 -0
  112. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0020_migrate_import_source_custom_field.py +0 -0
  113. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0021_importprofile_adapter_config.py +0 -0
  114. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0023_drop_moved_profile_columns.py +0 -0
  115. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0024_rename_importjob_importexecution.py +0 -0
  116. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0025_remove_importexecution_dry_run_and_more.py +0 -0
  117. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0026_alter_sourcedocument_profile.py +0 -0
  118. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0027_cableclassmapping_terminationresolution.py +0 -0
  119. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0028_cableimportsource.py +0 -0
  120. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0029_alter_cableimportsource_from_text_and_more.py +0 -0
  121. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/0030_remove_device_type_creation_config.py +0 -0
  122. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/migrations/__init__.py +0 -0
  123. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/netbox_reader.py +0 -0
  124. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/object_permissions.py +0 -0
  125. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/signals.py +0 -0
  126. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/static/netbox_data_import/js/conflict_modal.js +0 -0
  127. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/static/netbox_data_import/js/contact_candidate_modal.js +0 -0
  128. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/static/netbox_data_import/js/preview_row_actions.js +0 -0
  129. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/static/netbox_data_import/js/preview_row_controls.js +0 -0
  130. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/static/netbox_data_import/js/split_name_modal.js +0 -0
  131. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/static/netbox_data_import/js/sync_row_modal.js +0 -0
  132. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/target_runtime.py +0 -0
  133. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/template_content.py +0 -0
  134. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/_import_progress.html +0 -0
  135. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/analysis.html +0 -0
  136. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/bulk_yaml_import.html +0 -0
  137. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/cableclassmapping_edit.html +0 -0
  138. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/classrolemapping_edit.html +0 -0
  139. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/columnmapping_edit.html +0 -0
  140. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/columntransformrule_edit.html +0 -0
  141. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/device_import_data.html +0 -0
  142. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/devicetypemapping_edit.html +0 -0
  143. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/import_profile_yaml.html +0 -0
  144. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/import_progress.html +0 -0
  145. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/import_results.html +0 -0
  146. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/import_setup.html +0 -0
  147. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/importexecution_list.html +0 -0
  148. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/importprofile.html +0 -0
  149. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/importprofile_list.html +0 -0
  150. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/templates/netbox_data_import/source_resolution_list.html +0 -0
  151. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/__init__.py +0 -0
  152. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/conftest.py +0 -0
  153. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/fixtures/combined_name.xlsx +0 -0
  154. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/fixtures/sample_cans.xlsx +0 -0
  155. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/fixtures/trace_copper.xlsx +0 -0
  156. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/fixtures/trace_fiber.xlsx +0 -0
  157. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/mixins.py +0 -0
  158. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/netbox_configuration.py +0 -0
  159. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/parallel.py +0 -0
  160. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_adapter_config.py +0 -0
  161. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_api.py +0 -0
  162. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_atomic_exits.py +0 -0
  163. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_ci_configuration.py +0 -0
  164. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_comment_style.py +0 -0
  165. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_contact_role_suggestion.py +0 -0
  166. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_contact_sync.py +0 -0
  167. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_cutover_edge_contracts.py +0 -0
  168. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_graphql_filters.py +0 -0
  169. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_import_engine.py +0 -0
  170. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_import_execution.py +0 -0
  171. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_import_source_migration.py +0 -0
  172. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_installation_docs.py +0 -0
  173. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_ip_target.py +0 -0
  174. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_legacy_profile_yaml.py +0 -0
  175. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_misc_coverage.py +0 -0
  176. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_model_base_conventions.py +0 -0
  177. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_models.py +0 -0
  178. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_netbox_reader.py +0 -0
  179. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_object_permission_writer.py +0 -0
  180. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_parallel_test_setup.py +0 -0
  181. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_pristine_preview_rows.py +0 -0
  182. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_profile_policy_crud_scoping.py +0 -0
  183. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_quick_action_object_permissions.py +0 -0
  184. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_quick_action_profile_id.py +0 -0
  185. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_rack_module.py +0 -0
  186. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_release_metadata.py +0 -0
  187. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_review_workspace.py +0 -0
  188. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_serial_display.py +0 -0
  189. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_source_adapter.py +0 -0
  190. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_target_field_catalog.py +0 -0
  191. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_trace_decisions.py +0 -0
  192. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/tests/test_views_extra.py +0 -0
  193. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/trace_schema.py +0 -0
  194. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/transform_regex.py +0 -0
  195. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/utils.py +0 -0
  196. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import/values.py +2 -2
  197. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import.egg-info/dependency_links.txt +0 -0
  198. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/netbox_data_import.egg-info/top_level.txt +0 -0
  199. {netbox_data_import-2.0.0 → netbox_data_import-2.1.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netbox-data-import
3
- Version: 2.0.0
3
+ Version: 2.1.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
@@ -18,6 +18,7 @@ Requires-Dist: defusedxml>=0.7.1
18
18
  Requires-Dist: google-re2>=1.1.20251105
19
19
  Requires-Dist: openpyxl>=3.1
20
20
  Requires-Dist: pyyaml>=6
21
+ Requires-Dist: requests>=2.33.0
21
22
  Dynamic: license-file
22
23
 
23
24
  <!--
@@ -0,0 +1,49 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (C) 2025 Marcin Zieba <marcinpsk@gmail.com>
3
+ from typing import Any
4
+
5
+ from netbox.plugins import PluginConfig
6
+
7
+ __version__ = "2.1.0"
8
+
9
+
10
+ class NetBoxDataImportConfig(PluginConfig):
11
+ """NetBox plugin configuration for the Data Import plugin."""
12
+
13
+ name = "netbox_data_import"
14
+ verbose_name = "NetBox Data Import"
15
+ description = "NetBox plugin for importing data from external DCIM systems"
16
+ version = __version__
17
+ base_url = "data-import"
18
+ author = "Marcin Zieba"
19
+ author_email = "marcinpsk@gmail.com"
20
+ min_version = "4.6.0"
21
+ graphql_schema = "graphql.schema.schema"
22
+
23
+ default_settings: dict[str, Any] = {
24
+ # A deployment that names no allowlist reaches no origin, rather than every origin.
25
+ "inference_backend_origin_allowlist": [],
26
+ }
27
+
28
+ @classmethod
29
+ def validate(cls, user_config, netbox_version):
30
+ """Reject a malformed Inference Backend configuration before the application serves a request."""
31
+ super().validate(user_config, netbox_version)
32
+
33
+ from django.core.exceptions import ImproperlyConfigured
34
+
35
+ from .inference_settings import InvalidInferenceConfiguration, validate_plugin_settings
36
+
37
+ try:
38
+ validate_plugin_settings(user_config)
39
+ except InvalidInferenceConfiguration as exc:
40
+ raise ImproperlyConfigured(f"Plugin {cls.__module__} has an invalid configuration: {exc}") from exc
41
+
42
+ def ready(self):
43
+ """Import the jobs module, which NetBox does not load, so its @system_job registration runs."""
44
+ super().ready()
45
+
46
+ from . import jobs
47
+
48
+
49
+ config = NetBoxDataImportConfig
@@ -25,6 +25,12 @@ from ..models import (
25
25
  )
26
26
 
27
27
 
28
+ def _validate_candidate_values(candidate_values):
29
+ """Require a JSON object at the Contact candidate boundary."""
30
+ if not isinstance(candidate_values, dict):
31
+ raise TypeError("Contact candidate values must be a JSON object.")
32
+
33
+
28
34
  class PolicySectionApplicabilityMixin:
29
35
  """Apply the shared policy-section applicability rule on the REST write path."""
30
36
 
@@ -222,8 +228,7 @@ class SourceResolutionSerializer(PolicySectionSerializer):
222
228
  resolved_fields = attrs.get("resolved_fields", getattr(instance, "resolved_fields", None))
223
229
  try:
224
230
  candidate_values = json.loads(original_value)
225
- if not isinstance(candidate_values, dict):
226
- raise ValueError
231
+ _validate_candidate_values(candidate_values)
227
232
  configured_sources = profile.column_mappings.filter(target_field="candidate:contact").values_list(
228
233
  "source_column", flat=True
229
234
  )
@@ -41,6 +41,23 @@ from .values import identity_text, source_text
41
41
  CABLE_STATUS = "connected"
42
42
  ELIGIBLE_TERMINATION_LIMIT = 20
43
43
 
44
+ # Section 10.2 badge vocabulary. T8 adds the proposal states to the same set.
45
+ UNRESOLVED = "unresolved"
46
+ AUTOMATICALLY_RESOLVED = "automatically resolved"
47
+ MANUALLY_RESOLVED = "manually resolved"
48
+
49
+ CREATE_SEGMENT = "create"
50
+ REUSE_SEGMENT = "reuse existing"
51
+ CONFLICT_SEGMENT = "conflict"
52
+ _CONFLICT_CODES = frozenset(
53
+ {
54
+ "cable.termination_occupied",
55
+ "cable.multi_termination_conflict",
56
+ "cable.planned_termination_conflict",
57
+ "cable.resolved_segment_conflict",
58
+ }
59
+ )
60
+
44
61
  _KIND_BY_MODEL_NAME = {
45
62
  "interface": INTERFACE_KIND,
46
63
  "frontport": FRONT_PORT_KIND,
@@ -190,7 +207,10 @@ class _TraceAnalysis:
190
207
  segments: list = field(default_factory=list)
191
208
  proven: dict = field(default_factory=dict)
192
209
  policies: dict = field(default_factory=dict)
210
+ terminations: dict = field(default_factory=dict)
211
+ topology_read: bool = False
193
212
  logical_cable: Any = None
213
+ retains_logical_cable: bool = False
194
214
  blocked: bool = False
195
215
  invalid: bool = False
196
216
 
@@ -204,10 +224,16 @@ class _TraceAnalysis:
204
224
  """Return the desired segments no existing Cable already proves."""
205
225
  return [segment for segment in self.segments if segment.index not in self.proven]
206
226
 
227
+ @property
228
+ def deleted_logical_cable(self):
229
+ """Return the Logical Cable this trace removes, which a Cable that proves its path is not."""
230
+ return None if self.retains_logical_cable else self.logical_cable
231
+
207
232
  @property
208
233
  def delete_identity(self) -> str | None:
209
234
  """Return the Planned Change identity that removes this trace's Logical Cable."""
210
- return None if self.logical_cable is None else _delete_identity(self.logical_cable.cable.pk)
235
+ removed = self.deleted_logical_cable
236
+ return None if removed is None else _delete_identity(removed.cable.pk)
211
237
 
212
238
  def error(self, code: str, display: dict, identities=()) -> None:
213
239
  """Record one blocking or invalidating finding."""
@@ -502,19 +528,64 @@ class _CableBatch:
502
528
  devices = self._devices.get(identity_text(reference.device), [])
503
529
  if len(devices) != 1:
504
530
  analysis.block("trace.device_unresolved", {**_reference_display(reference), "matches": len(devices)})
531
+ # With no resolved Device there is nothing to pick from, so the picker cannot help here.
532
+ self._record_resolution(
533
+ analysis,
534
+ reference,
535
+ UNRESOLVED,
536
+ None,
537
+ reason=f"The source names {len(devices)} matching Devices, so no port list applies.",
538
+ )
505
539
  return None
506
540
  device = devices[0]
507
541
  stored = self._stored.get(_field_key(reference))
508
542
  if stored is not None:
509
- return self._stored_termination(analysis, reference, device, stored)
543
+ termination = self._stored_termination(analysis, reference, device, stored)
544
+ state = UNRESOLVED if termination is None else MANUALLY_RESOLVED
545
+ self._record_resolution(analysis, reference, state, termination)
546
+ return termination
510
547
  kind = claimed_termination_kind(reference.port_class)
511
548
  candidates = self._components_for(device.pk, kind).get(identity_text(reference.port), [])
512
549
  if len(candidates) != 1:
513
550
  analysis.block(
514
551
  "cable.termination_unresolved", {**_reference_display(reference), "matches": len(candidates)}
515
552
  )
553
+ self._record_resolution(analysis, reference, UNRESOLVED, None)
516
554
  return None
517
- return self._termination(candidates[0])
555
+ termination = self._termination(candidates[0])
556
+ self._record_resolution(analysis, reference, AUTOMATICALLY_RESOLVED, termination)
557
+ return termination
558
+
559
+ @staticmethod
560
+ def _record_resolution(
561
+ analysis: _TraceAnalysis,
562
+ reference,
563
+ state: str,
564
+ termination,
565
+ reason: str = "",
566
+ role: str = TERMINATION_ROLE,
567
+ ) -> None:
568
+ """Record how one Termination Reference was settled, for its badge and its picker."""
569
+ key = _field_key(reference, role)
570
+ claimed = claimed_termination_kind(reference.port_class)
571
+ if role == MAPPED_PEER_ROLE:
572
+ # The picker offers the ports on the far side of the panel, which are the opposite kind.
573
+ kind = REAR_PORT_KIND if claimed == FRONT_PORT_KIND else FRONT_PORT_KIND
574
+ label = f"{_endpoint_label(reference)} (mapped peer)"
575
+ else:
576
+ kind, label = claimed, _endpoint_label(reference)
577
+ analysis.terminations.setdefault(
578
+ key,
579
+ {
580
+ "field_key": key,
581
+ "label": label,
582
+ "kind": kind,
583
+ "state": state,
584
+ "selected": "" if termination is None else termination.display,
585
+ "selectable": not reason,
586
+ "reason": reason,
587
+ },
588
+ )
518
589
 
519
590
  def _stored_termination(self, analysis: _TraceAnalysis, reference, device, stored) -> _Termination | None:
520
591
  """Return the object one saved decision selected, rechecked against current target state."""
@@ -714,7 +785,9 @@ class _CableBatch:
714
785
  )
715
786
  if selected in peers:
716
787
  peer, mapping = peers[selected]
788
+ self._record_resolution(analysis, reference, MANUALLY_RESOLVED, peer, role=MAPPED_PEER_ROLE)
717
789
  return self._substituted(analysis, reference, exit_end, peer, mapping)
790
+ self._record_resolution(analysis, reference, UNRESOLVED, None, role=MAPPED_PEER_ROLE)
718
791
  analysis.block(
719
792
  "cable.ambiguous_mapped_peer",
720
793
  {
@@ -835,6 +908,8 @@ class _CableBatch:
835
908
  """
836
909
  pending = self._writing()
837
910
  for analysis in pending:
911
+ # Only these analyses look at the live topology, so only they may report what is there.
912
+ analysis.topology_read = True
838
913
  for segment in analysis.segments:
839
914
  candidate = self._occupied.get(segment.left.key)
840
915
  proven = candidate if candidate is not None and candidate.satisfies(segment) else None
@@ -873,7 +948,7 @@ class _CableBatch:
873
948
  {"segment_index": 0, **cable_display},
874
949
  identities=cable_identities,
875
950
  )
876
- analysis.logical_cable = None
951
+ analysis.retains_logical_cable = True
877
952
 
878
953
  def _note_reuse(self, analysis: _TraceAnalysis, segment: _DesiredSegment, proven: _ExistingCable) -> None:
879
954
  """Record the proven physical segment, so its live state joins the unit fingerprint."""
@@ -908,7 +983,8 @@ class _CableBatch:
908
983
 
909
984
  def _report_conflicts(self, analysis: _TraceAnalysis) -> None:
910
985
  """Block the trace when a Cable this import may not touch holds a termination it needs."""
911
- logical_id = None if analysis.logical_cable is None else analysis.logical_cable.cable.pk
986
+ removed = analysis.deleted_logical_cable
987
+ logical_id = None if removed is None else removed.cable.pk
912
988
  for segment in analysis.pending:
913
989
  for termination in segment.terminations:
914
990
  occupying = self._occupied.get(termination.key)
@@ -965,7 +1041,7 @@ class _CableBatch:
965
1041
  if analysis.pending and not self.actor.has_perm("dcim.add_cable"):
966
1042
  analysis.block("cable.permission_denied", {"permission": "dcim.add_cable"})
967
1043
  return
968
- logical = analysis.logical_cable
1044
+ logical = analysis.deleted_logical_cable
969
1045
  if logical is not None and not self.actor.has_perm("dcim.delete_cable", logical.cable):
970
1046
  cable_display, cable_identities = self._cable_diagnostic_disclosure(logical.cable)
971
1047
  analysis.block(
@@ -1076,16 +1152,99 @@ class _CableBatch:
1076
1152
  disposition=disposition,
1077
1153
  changes=changes,
1078
1154
  diagnostics=tuple(analysis.diagnostics),
1079
- display={**analysis.display, "detail": self._detail(analysis, disposition)},
1155
+ display={
1156
+ **analysis.display,
1157
+ "detail": self._detail(analysis, disposition),
1158
+ "trace": self._workspace(analysis, bool(changes)),
1159
+ },
1080
1160
  )
1081
1161
 
1162
+ def _workspace(self, analysis: _TraceAnalysis, writes: bool) -> dict:
1163
+ """Return what the three review workspace panels show for one Source Trace."""
1164
+ summary = analysis.trace.endpoint_summary
1165
+ resolved = {segment.index: segment for segment in analysis.segments}
1166
+ by_reference = self._resolved.get(analysis.identity, {})
1167
+ conflicted = {
1168
+ diagnostic.display.get("segment_index")
1169
+ for diagnostic in analysis.diagnostics
1170
+ if diagnostic.code in _CONFLICT_CODES
1171
+ }
1172
+ segments = []
1173
+ stated_segments = list(analysis.trace.segments)
1174
+ for index, stated in enumerate(stated_segments):
1175
+ planned = resolved.get(index)
1176
+ source_left, source_right = _endpoint_label(stated.left), _endpoint_label(stated.right)
1177
+ left = source_left if planned is None else planned.left.display
1178
+ right = source_right if planned is None else planned.right.display
1179
+ segments.append(
1180
+ {
1181
+ "index": index,
1182
+ "cable_class": source_text(stated.cable_class),
1183
+ "source_left": source_left,
1184
+ "source_right": source_right,
1185
+ "left": left,
1186
+ "right": right,
1187
+ # A path that re-enters one panel implies a claim the source never states.
1188
+ "pass_through": index > 0 and same_device_and_cards(stated_segments[index - 1].right, stated.left),
1189
+ # Planning substituted the entry port only where the source re-used one port.
1190
+ "substituted": self._entered_through_claim(planned, by_reference.get(stated.left.identity_key)),
1191
+ "status": self._segment_status(analysis, index, conflicted, planned, writes),
1192
+ }
1193
+ )
1194
+ return {
1195
+ "identity": analysis.trace.identity,
1196
+ "endpoints": {
1197
+ "from": _endpoint_label(summary.from_termination),
1198
+ "to": _endpoint_label(summary.to_termination),
1199
+ },
1200
+ "segments": segments,
1201
+ "logical_cable": self._logical_cable_display(analysis),
1202
+ "topology_known": analysis.topology_read,
1203
+ # A unit with no changes proposes nothing, so the panel must not offer to delete one.
1204
+ "deletes_logical_cable": writes and analysis.deleted_logical_cable is not None,
1205
+ "terminations": list(analysis.terminations.values()),
1206
+ }
1207
+
1208
+ @staticmethod
1209
+ def _entered_through_claim(planned: _DesiredSegment | None, stated: _Termination | None) -> bool:
1210
+ """Return whether planning entered this segment through a port the source never stated."""
1211
+ return planned is not None and stated is not None and planned.left.key != stated.key
1212
+
1213
+ @staticmethod
1214
+ def _segment_status(analysis: _TraceAnalysis, index: int, conflicted: set, planned, writes: bool) -> str:
1215
+ """Return the verdict the proposed panel shows for one segment, or none for an unplanned one."""
1216
+ if index in conflicted:
1217
+ return CONFLICT_SEGMENT
1218
+ if planned is None:
1219
+ return ""
1220
+ if index in analysis.proven:
1221
+ return REUSE_SEGMENT
1222
+ # A blocked unit contributes no change, so nothing here is going to be created.
1223
+ return CREATE_SEGMENT if writes else ""
1224
+
1225
+ def _logical_cable_display(self, analysis: _TraceAnalysis) -> dict | None:
1226
+ """Return the Logical Cable NetBox holds for this trace, as far as the actor may see it."""
1227
+ if analysis.logical_cable is None:
1228
+ return None
1229
+ cable = analysis.logical_cable.cable
1230
+ disclosure, _identities = self._cable_diagnostic_disclosure(cable)
1231
+ if not disclosure["cable_visible"]:
1232
+ return {"visible": False, "display": "", "description": "", "tags": []}
1233
+ # Section 6.3 reviews what the deletion removes, which the deletion payload also carries.
1234
+ return {
1235
+ "visible": True,
1236
+ "display": disclosure["cable"],
1237
+ "description": cable.description,
1238
+ "tags": sorted(cable.tags.values_list("name", flat=True)),
1239
+ }
1240
+
1082
1241
  def _changes(self, analysis: _TraceAnalysis) -> tuple[PlannedChange, ...]:
1083
1242
  """Return the deletion and the creations one actionable trace performs, in that order."""
1084
1243
  if analysis.stopped:
1085
1244
  return ()
1086
1245
  changes = []
1087
- if analysis.logical_cable is not None:
1088
- changes.append(self._delete_change(analysis.logical_cable))
1246
+ if analysis.deleted_logical_cable is not None:
1247
+ changes.append(self._delete_change(analysis.deleted_logical_cable))
1089
1248
  changes.extend(self._create_change(segment, analysis.policies[segment.index]) for segment in analysis.pending)
1090
1249
  return tuple(changes)
1091
1250
 
@@ -1130,7 +1289,7 @@ class _CableBatch:
1130
1289
  if disposition != Disposition.ACTIONABLE:
1131
1290
  return ""
1132
1291
  count = len(analysis.pending)
1133
- if analysis.logical_cable is not None:
1292
+ if analysis.deleted_logical_cable is not None:
1134
1293
  return f"Would replace the logical cable with {count} physical segment(s)."
1135
1294
  return f"Would create {count} physical segment(s)."
1136
1295
 
@@ -1269,8 +1428,14 @@ def _cable_terminations(cable_id: int) -> list:
1269
1428
 
1270
1429
 
1271
1430
  __all__ = (
1431
+ "AUTOMATICALLY_RESOLVED",
1272
1432
  "CABLE_STATUS",
1433
+ "CONFLICT_SEGMENT",
1434
+ "CREATE_SEGMENT",
1273
1435
  "ELIGIBLE_TERMINATION_LIMIT",
1436
+ "MANUALLY_RESOLVED",
1437
+ "REUSE_SEGMENT",
1438
+ "UNRESOLVED",
1274
1439
  "CableModule",
1275
1440
  "EligibleTerminations",
1276
1441
  "eligible_terminations",
@@ -70,14 +70,19 @@ def termination_field_key(*, device, cards, port, kind: str, role: str = TERMINA
70
70
  )
71
71
 
72
72
 
73
+ def _validate_termination_field_data(data):
74
+ """Reject malformed termination field-key data before canonicalization."""
75
+ if not isinstance(data, dict) or set(data) != {"cards", "device", "kind", "port", "role"}:
76
+ raise ValueError
77
+ if not all(isinstance(data[name], str) for name in data):
78
+ raise ValueError
79
+
80
+
73
81
  def parse_termination_field_key(value: str) -> dict[str, str]:
74
82
  """Parse an exact canonical termination field key."""
75
83
  try:
76
84
  data = json.loads(value)
77
- if not isinstance(data, dict) or set(data) != {"cards", "device", "kind", "port", "role"}:
78
- raise ValueError
79
- if not all(isinstance(data[name], str) for name in data):
80
- raise ValueError
85
+ _validate_termination_field_data(data)
81
86
  canonical = termination_field_key(**data)
82
87
  except (TypeError, ValueError) as exc:
83
88
  raise ValueError(f"'{value}' is not a canonical termination field key.") from exc
@@ -12,6 +12,7 @@ from .catalog import CATALOG
12
12
  from .models import (
13
13
  CableClassMapping,
14
14
  ImportProfile,
15
+ InferenceBackend,
15
16
  ColumnMapping,
16
17
  ClassRoleMapping,
17
18
  DeviceTypeMapping,
@@ -285,6 +286,32 @@ class ColumnTransformRuleForm(forms.ModelForm):
285
286
  self.fields[name].choices = [("", "---------"), *preserved]
286
287
 
287
288
 
289
+ class InferenceBackendForm(NetBoxModelForm):
290
+ """Create or edit one AI backend. Model validation applies the api_root trust boundary."""
291
+
292
+ class Meta:
293
+ model = InferenceBackend
294
+ fields = (
295
+ "backend_key",
296
+ "display_name",
297
+ "adapter_type",
298
+ "api_root",
299
+ "model",
300
+ "authentication",
301
+ "response_mode",
302
+ "credential_reference",
303
+ "connect_timeout",
304
+ "read_timeout",
305
+ "enabled",
306
+ "tags",
307
+ )
308
+ help_texts = {
309
+ "credential_reference": (
310
+ "A typed Vault KV v2 reference: backend, mount, path and field. It never holds a key value."
311
+ ),
312
+ }
313
+
314
+
288
315
  class ImportSetupForm(forms.Form):
289
316
  """Form for the import wizard step 1: select profile, upload file, choose site/location/tenant."""
290
317
 
@@ -9,6 +9,7 @@ workbook, no column and no NetBox object type.
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ from contextlib import suppress
12
13
  from typing import cast
13
14
 
14
15
  from django.core.exceptions import ValidationError
@@ -201,7 +202,7 @@ class ImportEngine:
201
202
  rolled_back=failure.rolled_back,
202
203
  not_attempted=failure.not_attempted,
203
204
  )
204
- raise failure.cause
205
+ raise failure.cause from failure
205
206
  except Exception as exc:
206
207
  # Every other failure after the reservation, so the row can never stay pending.
207
208
  cls._mark_failed(
@@ -335,10 +336,8 @@ class ImportEngine:
335
336
  @staticmethod
336
337
  def _mark_failed(execution, **detail) -> None:
337
338
  """Finish a failed row unless a concurrent finisher already chose its outcome."""
338
- try:
339
+ with suppress(ValueError):
339
340
  execution.mark_failed(**detail)
340
- except ValueError:
341
- pass
342
341
 
343
342
  @staticmethod
344
343
  def _stored_source(profile, source_document) -> SourceDocument: