netbox-data-import 2.1.0__tar.gz → 2.2.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 (221) hide show
  1. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/PKG-INFO +1 -1
  2. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/__init__.py +3 -3
  3. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/api/serializers.py +77 -0
  4. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/api/urls.py +14 -1
  5. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/api/views.py +85 -58
  6. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/cable_target.py +28 -5
  7. netbox_data_import-2.2.0/netbox_data_import/inference_adapter.py +419 -0
  8. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/inference_backend.py +12 -0
  9. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/inference_credentials.py +31 -11
  10. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/inference_settings.py +29 -0
  11. netbox_data_import-2.2.0/netbox_data_import/inference_transport.py +130 -0
  12. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/inference_trust.py +3 -4
  13. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/jobs.py +21 -1
  14. netbox_data_import-2.2.0/netbox_data_import/migrations/0033_resolutionproposal.py +160 -0
  15. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/models.py +245 -23
  16. netbox_data_import-2.2.0/netbox_data_import/object_permissions.py +340 -0
  17. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/preview_row_actions.py +3 -3
  18. netbox_data_import-2.2.0/netbox_data_import/proposal_decisions.py +92 -0
  19. netbox_data_import-2.2.0/netbox_data_import/proposal_jobs.py +226 -0
  20. netbox_data_import-2.2.0/netbox_data_import/proposal_presentation.py +317 -0
  21. netbox_data_import-2.2.0/netbox_data_import/proposal_response.py +129 -0
  22. netbox_data_import-2.2.0/netbox_data_import/proposal_tasks.py +183 -0
  23. netbox_data_import-2.2.0/netbox_data_import/resolution_proposals.py +164 -0
  24. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/review_workspace.py +1 -0
  25. netbox_data_import-2.2.0/netbox_data_import/static/netbox_data_import/js/trace_proposals.js +200 -0
  26. netbox_data_import-2.2.0/netbox_data_import/templates/netbox_data_import/_proposal_action.html +10 -0
  27. netbox_data_import-2.2.0/netbox_data_import/templates/netbox_data_import/_proposal_card.html +31 -0
  28. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/templates/netbox_data_import/trace_workspace.html +85 -31
  29. netbox_data_import-2.2.0/netbox_data_import/termination_proposal.py +207 -0
  30. netbox_data_import-2.2.0/netbox_data_import/tests/inference_http.py +236 -0
  31. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_api.py +118 -0
  32. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_atomic_exits.py +3 -0
  33. netbox_data_import-2.2.0/netbox_data_import/tests/test_digest_indexed_models.py +181 -0
  34. netbox_data_import-2.2.0/netbox_data_import/tests/test_inference_adapter.py +1119 -0
  35. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_inference_backend.py +14 -0
  36. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_inference_credentials.py +155 -31
  37. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_inference_secret_containment.py +14 -0
  38. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_inference_settings.py +103 -39
  39. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_migrations.py +41 -0
  40. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_object_permission_writer.py +305 -1
  41. netbox_data_import-2.2.0/netbox_data_import/tests/test_proposal_decisions.py +507 -0
  42. netbox_data_import-2.2.0/netbox_data_import/tests/test_proposal_jobs.py +678 -0
  43. netbox_data_import-2.2.0/netbox_data_import/tests/test_proposal_response.py +150 -0
  44. netbox_data_import-2.2.0/netbox_data_import/tests/test_proposal_tasks.py +216 -0
  45. netbox_data_import-2.2.0/netbox_data_import/tests/test_proposal_workspace.py +1352 -0
  46. netbox_data_import-2.2.0/netbox_data_import/tests/test_resolution_proposal_api.py +316 -0
  47. netbox_data_import-2.2.0/netbox_data_import/tests/test_resolution_proposals.py +526 -0
  48. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_standard_views.py +90 -2
  49. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_trace_workspace.py +47 -1
  50. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/urls.py +5 -0
  51. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/views.py +319 -10
  52. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import.egg-info/PKG-INFO +1 -1
  53. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import.egg-info/SOURCES.txt +21 -0
  54. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/pyproject.toml +2 -1
  55. netbox_data_import-2.1.0/netbox_data_import/inference_adapter.py +0 -260
  56. netbox_data_import-2.1.0/netbox_data_import/object_permissions.py +0 -168
  57. netbox_data_import-2.1.0/netbox_data_import/tests/test_inference_adapter.py +0 -432
  58. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/LICENSE +0 -0
  59. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/README.md +0 -0
  60. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/adapter_config.py +0 -0
  61. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/adapter_forms.py +0 -0
  62. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/adapters.py +0 -0
  63. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/api/__init__.py +0 -0
  64. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/catalog.py +0 -0
  65. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/contact_resolution.py +0 -0
  66. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/device_field_review.py +0 -0
  67. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/device_identity.py +0 -0
  68. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/field_keys.py +0 -0
  69. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/filters.py +0 -0
  70. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/flat_workbook.py +0 -0
  71. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/forms.py +0 -0
  72. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/graphql/__init__.py +0 -0
  73. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/graphql/filters.py +0 -0
  74. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/graphql/schema.py +0 -0
  75. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/graphql/types.py +0 -0
  76. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/import_engine.py +0 -0
  77. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/inference_connection_test.py +0 -0
  78. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/ip_assignment.py +0 -0
  79. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0001_initial.py +0 -0
  80. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0002_importjob.py +0 -0
  81. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0003_remove_classrolemapping_unique_class_role_mapping_per_profile_and_more.py +0 -0
  82. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0004_features.py +0 -0
  83. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0005_deviceexistingmatch.py +0 -0
  84. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0006_deviceexistingmatch_asset_tag.py +0 -0
  85. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0007_manufacturermapping.py +0 -0
  86. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0008_unique_constraints.py +0 -0
  87. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0009_add_rack_type_to_classrolemapping.py +0 -0
  88. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0010_alter_manufacturermapping_id.py +0 -0
  89. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0011_importprofile_capture_extra_data.py +0 -0
  90. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0012_classrolemapping_rack_type_related_name.py +0 -0
  91. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0013_remove_columnmapping_unique_target.py +0 -0
  92. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0014_alter_columnmapping_target_field_and_more.py +0 -0
  93. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0015_cleanup_duplicate_device_matches.py +0 -0
  94. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0016_deviceexistingmatch_ndi_devicematch_profile_device.py +0 -0
  95. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0017_native_contact_sync.py +0 -0
  96. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0018_ignoredfielddifference.py +0 -0
  97. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0019_deviceimportsource.py +0 -0
  98. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0020_migrate_import_source_custom_field.py +0 -0
  99. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0021_importprofile_adapter_config.py +0 -0
  100. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0022_migrate_profile_adapter_config.py +0 -0
  101. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0023_drop_moved_profile_columns.py +0 -0
  102. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0024_rename_importjob_importexecution.py +0 -0
  103. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0025_remove_importexecution_dry_run_and_more.py +0 -0
  104. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0026_alter_sourcedocument_profile.py +0 -0
  105. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0027_cableclassmapping_terminationresolution.py +0 -0
  106. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0028_cableimportsource.py +0 -0
  107. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0029_alter_cableimportsource_from_text_and_more.py +0 -0
  108. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0030_remove_device_type_creation_config.py +0 -0
  109. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0031_inferencebackend.py +0 -0
  110. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/0032_inference_timeout_floor.py +0 -0
  111. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/migrations/__init__.py +0 -0
  112. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/navigation.py +0 -0
  113. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/netbox_reader.py +0 -0
  114. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/plan.py +0 -0
  115. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/signals.py +0 -0
  116. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/source_resolution.py +0 -0
  117. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/static/netbox_data_import/js/conflict_modal.js +0 -0
  118. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/static/netbox_data_import/js/contact_candidate_modal.js +0 -0
  119. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/static/netbox_data_import/js/preview_row_actions.js +0 -0
  120. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/static/netbox_data_import/js/preview_row_controls.js +0 -0
  121. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/static/netbox_data_import/js/split_name_modal.js +0 -0
  122. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/static/netbox_data_import/js/sync_row_modal.js +0 -0
  123. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/static/netbox_data_import/js/trace_termination_picker.js +0 -0
  124. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tables.py +0 -0
  125. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/target_modules.py +0 -0
  126. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/target_runtime.py +0 -0
  127. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/template_content.py +0 -0
  128. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/templates/netbox_data_import/_import_progress.html +0 -0
  129. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/templates/netbox_data_import/analysis.html +0 -0
  130. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/templates/netbox_data_import/bulk_yaml_import.html +0 -0
  131. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/templates/netbox_data_import/cableclassmapping_edit.html +0 -0
  132. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/templates/netbox_data_import/classrolemapping_edit.html +0 -0
  133. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/templates/netbox_data_import/columnmapping_edit.html +0 -0
  134. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/templates/netbox_data_import/columntransformrule_edit.html +0 -0
  135. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/templates/netbox_data_import/device_import_data.html +0 -0
  136. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/templates/netbox_data_import/devicetypemapping_edit.html +0 -0
  137. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/templates/netbox_data_import/import_preview.html +0 -0
  138. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/templates/netbox_data_import/import_profile_yaml.html +0 -0
  139. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/templates/netbox_data_import/import_progress.html +0 -0
  140. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/templates/netbox_data_import/import_results.html +0 -0
  141. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/templates/netbox_data_import/import_setup.html +0 -0
  142. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/templates/netbox_data_import/importexecution_list.html +0 -0
  143. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/templates/netbox_data_import/importprofile.html +0 -0
  144. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/templates/netbox_data_import/importprofile_list.html +0 -0
  145. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/templates/netbox_data_import/inferencebackend.html +0 -0
  146. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/templates/netbox_data_import/source_resolution_list.html +0 -0
  147. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/__init__.py +0 -0
  148. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/conftest.py +0 -0
  149. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/fixtures/combined_name.xlsx +0 -0
  150. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/fixtures/sample_cans.xlsx +0 -0
  151. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/fixtures/trace_copper.xlsx +0 -0
  152. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/fixtures/trace_fiber.xlsx +0 -0
  153. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/helpers.py +0 -0
  154. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/isolated_settings.py +0 -0
  155. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/mixins.py +0 -0
  156. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/mock_discipline.py +0 -0
  157. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/netbox_configuration.py +0 -0
  158. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/parallel.py +0 -0
  159. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_adapter_config.py +0 -0
  160. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_adapter_config_migration.py +0 -0
  161. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_cable_module.py +0 -0
  162. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_ci_configuration.py +0 -0
  163. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_comment_style.py +0 -0
  164. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_contact_resolution_ajax.py +0 -0
  165. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_contact_role_suggestion.py +0 -0
  166. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_contact_sync.py +0 -0
  167. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_cutover_edge_contracts.py +0 -0
  168. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_devcontainer_e2e.py +0 -0
  169. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_device_module.py +0 -0
  170. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_execution_audit.py +0 -0
  171. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_graphql_filters.py +0 -0
  172. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_import_cutover.py +0 -0
  173. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_import_engine.py +0 -0
  174. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_import_execution.py +0 -0
  175. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_import_plan.py +0 -0
  176. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_import_source_migration.py +0 -0
  177. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_inference_connection_test.py +0 -0
  178. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_inference_trust.py +0 -0
  179. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_installation_docs.py +0 -0
  180. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_ip_target.py +0 -0
  181. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_legacy_profile_yaml.py +0 -0
  182. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_misc_coverage.py +0 -0
  183. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_mock_discipline.py +0 -0
  184. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_model_base_conventions.py +0 -0
  185. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_models.py +0 -0
  186. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_module_boundaries.py +0 -0
  187. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_navigation.py +0 -0
  188. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_netbox_reader.py +0 -0
  189. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_parallel_test_setup.py +0 -0
  190. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_placement_reference_locks.py +0 -0
  191. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_preview_assets.py +0 -0
  192. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_preview_state_ownership.py +0 -0
  193. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_pristine_preview_rows.py +0 -0
  194. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_profile_policy_crud_scoping.py +0 -0
  195. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_quick_action_input_bounds.py +0 -0
  196. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_quick_action_object_permissions.py +0 -0
  197. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_quick_action_profile_id.py +0 -0
  198. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_rack_module.py +0 -0
  199. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_release_metadata.py +0 -0
  200. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_remove_device_type_creation_config_migration.py +0 -0
  201. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_review_actions.py +0 -0
  202. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_review_resolutions.py +0 -0
  203. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_review_workspace.py +0 -0
  204. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_serial_display.py +0 -0
  205. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_source_adapter.py +0 -0
  206. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_target_field_catalog.py +0 -0
  207. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_target_module_edge_contracts.py +0 -0
  208. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_trace_decisions.py +0 -0
  209. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_trace_workbook.py +0 -0
  210. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_views.py +0 -0
  211. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_views_coverage2.py +0 -0
  212. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/tests/test_views_extra.py +0 -0
  213. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/trace_schema.py +0 -0
  214. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/trace_workbook.py +0 -0
  215. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/transform_regex.py +0 -0
  216. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/utils.py +0 -0
  217. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import/values.py +0 -0
  218. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import.egg-info/dependency_links.txt +0 -0
  219. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import.egg-info/requires.txt +0 -0
  220. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/netbox_data_import.egg-info/top_level.txt +0 -0
  221. {netbox_data_import-2.1.0 → netbox_data_import-2.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netbox-data-import
3
- Version: 2.1.0
3
+ Version: 2.2.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.1.0"
7
+ __version__ = "2.2.0"
8
8
 
9
9
 
10
10
  class NetBoxDataImportConfig(PluginConfig):
@@ -40,10 +40,10 @@ class NetBoxDataImportConfig(PluginConfig):
40
40
  raise ImproperlyConfigured(f"Plugin {cls.__module__} has an invalid configuration: {exc}") from exc
41
41
 
42
42
  def ready(self):
43
- """Import the jobs module, which NetBox does not load, so its @system_job registration runs."""
43
+ """Import the modules NetBox does not load, so their job and task registrations run."""
44
44
  super().ready()
45
45
 
46
- from . import jobs
46
+ from . import jobs, termination_proposal
47
47
 
48
48
 
49
49
  config = NetBoxDataImportConfig
@@ -19,6 +19,8 @@ from ..models import (
19
19
  ColumnTransformRule,
20
20
  SourceResolution,
21
21
  ImportExecution,
22
+ InferenceBackend,
23
+ ResolutionProposal,
22
24
  validate_adapter_target_module,
23
25
  validate_contact_candidate_resolution,
24
26
  validate_section_applicability,
@@ -278,3 +280,78 @@ class ImportExecutionSerializer(serializers.ModelSerializer):
278
280
  "failure_detail",
279
281
  ]
280
282
  read_only_fields = fields
283
+
284
+
285
+ class ResolutionProposalSerializer(serializers.ModelSerializer):
286
+ """Read-only serializer for the Resolution Proposal audit record."""
287
+
288
+ class Meta:
289
+ model = ResolutionProposal
290
+ fields = [
291
+ "id",
292
+ "profile",
293
+ "task_type",
294
+ "field_key",
295
+ "status",
296
+ "source_evidence",
297
+ "resolved_device_type",
298
+ "resolved_device_id",
299
+ "prompt_version",
300
+ "response_schema_version",
301
+ "candidate_snapshot",
302
+ "requested_by",
303
+ "created",
304
+ "last_updated",
305
+ "outcome",
306
+ "selected_candidate_id",
307
+ "selected_object_type",
308
+ "selected_object_id",
309
+ "explanation",
310
+ "backend_metadata",
311
+ "response_diagnostic",
312
+ "failure_reason",
313
+ "decision",
314
+ "decided_by",
315
+ "decided_at",
316
+ "written_resolution",
317
+ ]
318
+ read_only_fields = fields
319
+
320
+
321
+ class ResolutionProposalHistorySerializer(serializers.ModelSerializer):
322
+ """Serialize one attempt summary for profile-scoped field history."""
323
+
324
+ class Meta:
325
+ model = ResolutionProposal
326
+ fields = ["id", "created", "status", "outcome", "decision", "failure_reason"]
327
+ read_only_fields = fields
328
+
329
+
330
+ class InferenceBackendSerializer(NetBoxModelSerializer):
331
+ """Serialize one Inference Backend row, without its credential reference.
332
+
333
+ NetBox resolves this class by model name to freeze a delete event payload.
334
+ """
335
+
336
+ class Meta:
337
+ model = InferenceBackend
338
+ fields = [
339
+ "id",
340
+ "url",
341
+ "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",
353
+ "custom_fields",
354
+ "created",
355
+ "last_updated",
356
+ ]
357
+ brief_fields = ["id", "url", "display", "backend_key", "display_name", "enabled"]
@@ -2,6 +2,7 @@
2
2
  # Copyright (C) 2025 Marcin Zieba <marcinpsk@gmail.com>
3
3
  """URL router for the data-import plugin API."""
4
4
 
5
+ from django.urls import path
5
6
  from netbox.api.routers import NetBoxRouter
6
7
 
7
8
  from .views import (
@@ -13,6 +14,9 @@ from .views import (
13
14
  ColumnTransformRuleViewSet,
14
15
  SourceResolutionViewSet,
15
16
  ImportExecutionViewSet,
17
+ InferenceBackendViewSet,
18
+ ResolutionProposalHistoryViewSet,
19
+ ResolutionProposalViewSet,
16
20
  )
17
21
 
18
22
  router = NetBoxRouter()
@@ -24,6 +28,15 @@ router.register("ignored-devices", IgnoredDeviceViewSet)
24
28
  router.register("column-transforms", ColumnTransformRuleViewSet)
25
29
  router.register("source-resolutions", SourceResolutionViewSet)
26
30
  router.register("executions", ImportExecutionViewSet)
31
+ router.register("ai-backends", InferenceBackendViewSet)
32
+ router.register("resolution-proposals", ResolutionProposalViewSet)
27
33
 
28
34
  app_name = "netbox_data_import"
29
- urlpatterns = router.urls
35
+ urlpatterns = [
36
+ path(
37
+ "resolution-proposal-history/",
38
+ ResolutionProposalHistoryViewSet.as_view({"get": "list"}),
39
+ name="resolutionproposalhistory-list",
40
+ ),
41
+ *router.urls,
42
+ ]
@@ -3,10 +3,12 @@
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
7
- from rest_framework import viewsets, permissions
6
+ from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
7
+ from rest_framework import mixins, permissions, viewsets
8
+ from rest_framework.exceptions import ValidationError
8
9
  from rest_framework.permissions import DjangoModelPermissions
9
10
 
11
+ from ..field_keys import SELECT_TERMINATION_TASK, parse_termination_field_key
10
12
  from ..models import (
11
13
  locked_profile_policy,
12
14
  locked_resolution_policy,
@@ -18,6 +20,8 @@ from ..models import (
18
20
  ColumnTransformRule,
19
21
  SourceResolution,
20
22
  ImportExecution,
23
+ InferenceBackend,
24
+ ResolutionProposal,
21
25
  )
22
26
  from .serializers import (
23
27
  ImportProfileSerializer,
@@ -28,6 +32,9 @@ from .serializers import (
28
32
  ColumnTransformRuleSerializer,
29
33
  SourceResolutionSerializer,
30
34
  ImportExecutionSerializer,
35
+ InferenceBackendSerializer,
36
+ ResolutionProposalHistorySerializer,
37
+ ResolutionProposalSerializer,
31
38
  )
32
39
 
33
40
 
@@ -56,7 +63,23 @@ class ImportProfileViewSet(NetBoxModelViewSet):
56
63
  serializer_class = ImportProfileSerializer
57
64
 
58
65
 
59
- class _PluginModelViewSet(viewsets.ModelViewSet):
66
+ class _ProfileScopedQuerySetMixin(viewsets.GenericViewSet):
67
+ """Restrict profile-owned rows and validate their optional profile filter."""
68
+
69
+ def get_queryset(self):
70
+ """Return viewable rows, filtered by a valid profile ID when supplied."""
71
+ qs = super().get_queryset().restrict(self.request.user, "view")
72
+ profile_id = self.request.query_params.get("profile_id")
73
+ if profile_id is not None:
74
+ try:
75
+ profile_id = int(profile_id)
76
+ except (TypeError, ValueError) as exc:
77
+ raise ValidationError({"profile_id": "Enter a whole number."}) from exc
78
+ qs = qs.filter(profile_id=profile_id)
79
+ return qs
80
+
81
+
82
+ class _PluginModelViewSet(_ProfileScopedQuerySetMixin, viewsets.ModelViewSet):
60
83
  """Base class for plain-model viewsets in this plugin."""
61
84
 
62
85
  permission_classes = [permissions.IsAuthenticated, DjangoModelPermissionsWithView]
@@ -68,14 +91,6 @@ class ColumnMappingViewSet(_PluginModelViewSet):
68
91
  queryset = ColumnMapping.objects.select_related("profile")
69
92
  serializer_class = ColumnMappingSerializer
70
93
 
71
- def get_queryset(self):
72
- """Filter by profile_id query param if provided."""
73
- qs = super().get_queryset()
74
- profile_id = self.request.query_params.get("profile_id")
75
- if profile_id:
76
- qs = qs.filter(profile_id=profile_id)
77
- return qs
78
-
79
94
 
80
95
  class ClassRoleMappingViewSet(_PluginModelViewSet):
81
96
  """CRUD viewset for ClassRoleMapping."""
@@ -83,14 +98,6 @@ class ClassRoleMappingViewSet(_PluginModelViewSet):
83
98
  queryset = ClassRoleMapping.objects.select_related("profile", "rack_type")
84
99
  serializer_class = ClassRoleMappingSerializer
85
100
 
86
- def get_queryset(self):
87
- """Filter by profile_id query param if provided."""
88
- qs = super().get_queryset()
89
- profile_id = self.request.query_params.get("profile_id")
90
- if profile_id:
91
- qs = qs.filter(profile_id=profile_id)
92
- return qs
93
-
94
101
 
95
102
  class DeviceTypeMappingViewSet(_PluginModelViewSet):
96
103
  """CRUD viewset for DeviceTypeMapping."""
@@ -98,14 +105,6 @@ class DeviceTypeMappingViewSet(_PluginModelViewSet):
98
105
  queryset = DeviceTypeMapping.objects.select_related("profile")
99
106
  serializer_class = DeviceTypeMappingSerializer
100
107
 
101
- def get_queryset(self):
102
- """Filter by profile_id query param if provided."""
103
- qs = super().get_queryset()
104
- profile_id = self.request.query_params.get("profile_id")
105
- if profile_id:
106
- qs = qs.filter(profile_id=profile_id)
107
- return qs
108
-
109
108
 
110
109
  class IgnoredDeviceViewSet(_PluginModelViewSet):
111
110
  """CRUD viewset for IgnoredDevice."""
@@ -113,14 +112,6 @@ class IgnoredDeviceViewSet(_PluginModelViewSet):
113
112
  queryset = IgnoredDevice.objects.select_related("profile")
114
113
  serializer_class = IgnoredDeviceSerializer
115
114
 
116
- def get_queryset(self):
117
- """Filter by profile_id query param if provided."""
118
- qs = super().get_queryset()
119
- profile_id = self.request.query_params.get("profile_id")
120
- if profile_id:
121
- qs = qs.filter(profile_id=profile_id)
122
- return qs
123
-
124
115
 
125
116
  class ColumnTransformRuleViewSet(_PluginModelViewSet):
126
117
  """CRUD viewset for ColumnTransformRule."""
@@ -128,14 +119,6 @@ class ColumnTransformRuleViewSet(_PluginModelViewSet):
128
119
  queryset = ColumnTransformRule.objects.select_related("profile")
129
120
  serializer_class = ColumnTransformRuleSerializer
130
121
 
131
- def get_queryset(self):
132
- """Filter by profile_id query param if provided."""
133
- qs = super().get_queryset()
134
- profile_id = self.request.query_params.get("profile_id")
135
- if profile_id:
136
- qs = qs.filter(profile_id=profile_id)
137
- return qs
138
-
139
122
 
140
123
  def _revalidate_against_the_stored_row(serializer):
141
124
  """Read the resolution again and check the request against the row as it now stands.
@@ -184,26 +167,70 @@ class SourceResolutionViewSet(_PluginModelViewSet):
184
167
  except (SourceResolution.DoesNotExist, ImportProfile.DoesNotExist):
185
168
  raise Http404 from None
186
169
 
187
- def get_queryset(self):
188
- """Filter by profile_id query param if provided."""
189
- qs = super().get_queryset()
190
- profile_id = self.request.query_params.get("profile_id")
191
- if profile_id:
192
- qs = qs.filter(profile_id=profile_id)
193
- return qs
194
-
195
170
 
196
- class ImportExecutionViewSet(viewsets.ReadOnlyModelViewSet):
171
+ class ImportExecutionViewSet(_ProfileScopedQuerySetMixin, viewsets.ReadOnlyModelViewSet):
197
172
  """Read-only viewset for the Import Execution audit history."""
198
173
 
199
174
  queryset = ImportExecution.objects.select_related("profile")
200
175
  serializer_class = ImportExecutionSerializer
201
176
  permission_classes = [permissions.IsAuthenticated, DjangoModelPermissionsWithView]
202
177
 
178
+
179
+ class ResolutionProposalViewSet(_ProfileScopedQuerySetMixin, viewsets.ReadOnlyModelViewSet):
180
+ """Read-only viewset for Resolution Proposal history."""
181
+
182
+ queryset = ResolutionProposal.objects.select_related("profile")
183
+ serializer_class = ResolutionProposalSerializer
184
+ permission_classes = [permissions.IsAuthenticated, DjangoModelPermissionsWithView]
185
+
203
186
  def get_queryset(self):
204
- """Filter by profile_id query param if provided."""
205
- qs = super().get_queryset()
206
- profile_id = self.request.query_params.get("profile_id")
207
- if profile_id:
208
- qs = qs.filter(profile_id=profile_id)
209
- return qs
187
+ """Return proposals only for profiles that the actor can view."""
188
+ profile_scope = ImportProfile.objects.restrict(self.request.user, "view")
189
+ return super().get_queryset().filter(profile__in=profile_scope)
190
+
191
+
192
+ class ResolutionProposalHistoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
193
+ """Read one field's complete paginated history with workspace permissions."""
194
+
195
+ queryset = ResolutionProposal.objects.none()
196
+ serializer_class = ResolutionProposalHistorySerializer
197
+ permission_classes = [permissions.IsAuthenticated]
198
+
199
+ def get_queryset(self):
200
+ """Return one profile field's history when the actor can view that profile."""
201
+ profile_values = self.request.query_params.getlist("profile_id")
202
+ field_values = self.request.query_params.getlist("field_key")
203
+ if len(profile_values) != 1 or not profile_values[0]:
204
+ raise ValidationError({"profile_id": "Provide one whole-number profile ID."})
205
+ if len(field_values) != 1 or not field_values[0]:
206
+ raise ValidationError({"field_key": "Provide one canonical termination field key."})
207
+ try:
208
+ profile_id = int(profile_values[0])
209
+ except (TypeError, ValueError) as exc:
210
+ raise ValidationError({"profile_id": "Provide one whole-number profile ID."}) from exc
211
+ try:
212
+ parse_termination_field_key(field_values[0])
213
+ except ValueError as exc:
214
+ raise ValidationError({"field_key": "Provide one canonical termination field key."}) from exc
215
+ if not ImportProfile.objects.restrict(self.request.user, "view").filter(pk=profile_id).exists():
216
+ raise Http404
217
+ return (
218
+ ResolutionProposal.objects.filter(
219
+ profile_id=profile_id,
220
+ task_type=SELECT_TERMINATION_TASK,
221
+ field_key=field_values[0],
222
+ )
223
+ .select_related("profile")
224
+ .order_by("-created", "-pk")
225
+ )
226
+
227
+
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
+ """
234
+
235
+ queryset = InferenceBackend.objects.prefetch_related("tags")
236
+ serializer_class = InferenceBackendSerializer
@@ -303,7 +303,7 @@ def _source_record(trace, segment_index: int) -> dict:
303
303
  }
304
304
 
305
305
 
306
- def _visible_devices(netbox_reader, names) -> dict[str, list]:
306
+ def _visible_devices(netbox_reader, names, *, _lock_rows=False) -> dict[str, list]:
307
307
  """Return the visible Devices at the import target, grouped by comparison name."""
308
308
  from django.db.models import Q
309
309
 
@@ -313,6 +313,8 @@ def _visible_devices(netbox_reader, names) -> dict[str, list]:
313
313
  devices = netbox_reader.devices()
314
314
  if netbox_reader.site is not None:
315
315
  devices = devices.filter(site=netbox_reader.site)
316
+ if _lock_rows:
317
+ devices = devices.order_by("pk").select_for_update(of=("self",))
316
318
  lookup = Q()
317
319
  for name in sorted(wanted):
318
320
  lookup |= Q(name__iexact=name)
@@ -324,6 +326,20 @@ def _visible_devices(netbox_reader, names) -> dict[str, list]:
324
326
  return grouped
325
327
 
326
328
 
329
+ def resolved_device_for(field_key: str, netbox_reader, *, _lock_rows=False):
330
+ """Return the one visible Device a termination field key names, or None when it names no single one.
331
+
332
+ A proposal freezes this reference and revalidates it, so the retrieval and the freshness check
333
+ have to agree on what "the resolved Device" means.
334
+ """
335
+ 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
338
+
339
+
340
+ _RESOLVED_DEVICE_UNSET = object()
341
+
342
+
327
343
  def eligible_terminations(
328
344
  field_key: str,
329
345
  netbox_reader,
@@ -331,6 +347,8 @@ def eligible_terminations(
331
347
  profile,
332
348
  search: str = "",
333
349
  limit: int = ELIGIBLE_TERMINATION_LIMIT,
350
+ _resolved_device=_RESOLVED_DEVICE_UNSET,
351
+ _lock_rows: bool = False,
334
352
  ) -> EligibleTerminations:
335
353
  """Return one page of candidates for a canonical termination field key.
336
354
 
@@ -340,12 +358,15 @@ def eligible_terminations(
340
358
  query.
341
359
  """
342
360
  parsed = parse_termination_field_key(field_key)
343
- device_name, kind = parsed["device"], parsed["kind"]
361
+ kind = parsed["kind"]
344
362
  accessor = _READER_ACCESSOR_BY_KIND[kind]
345
- devices = _visible_devices(netbox_reader, [device_name]).get(identity_text(device_name), [])
346
- if len(devices) != 1:
363
+ device = (
364
+ resolved_device_for(field_key, netbox_reader, _lock_rows=_lock_rows)
365
+ if _resolved_device is _RESOLVED_DEVICE_UNSET
366
+ else _resolved_device
367
+ )
368
+ if device is None:
347
369
  return EligibleTerminations(candidates=(), total=0)
348
- device = devices[0]
349
370
  candidates = getattr(netbox_reader, accessor)().filter(device_id=device.pk)
350
371
  if parsed["role"] == MAPPED_PEER_ROLE:
351
372
  from .models import TerminationResolution, index_digest
@@ -399,6 +420,8 @@ def eligible_terminations(
399
420
  )
400
421
  if search:
401
422
  candidates = candidates.filter(name__icontains=search)
423
+ if _lock_rows:
424
+ tuple(candidates.order_by("pk").select_for_update(of=("self",)).values_list("pk", flat=True))
402
425
  candidates = candidates.order_by("name", "pk")
403
426
  return EligibleTerminations(candidates=tuple(candidates[:limit]), total=candidates.count())
404
427