netbox-data-import 2.1.0__tar.gz → 2.3.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 (226) hide show
  1. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/PKG-INFO +1 -1
  2. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/__init__.py +3 -3
  3. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/api/serializers.py +125 -0
  4. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/api/urls.py +18 -1
  5. netbox_data_import-2.3.0/netbox_data_import/api/views.py +293 -0
  6. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/cable_target.py +28 -5
  7. netbox_data_import-2.3.0/netbox_data_import/graphql/schema.py +30 -0
  8. netbox_data_import-2.3.0/netbox_data_import/graphql/types.py +42 -0
  9. netbox_data_import-2.3.0/netbox_data_import/inference_adapter.py +419 -0
  10. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/inference_backend.py +12 -0
  11. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/inference_credentials.py +31 -11
  12. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/inference_settings.py +29 -0
  13. netbox_data_import-2.3.0/netbox_data_import/inference_transport.py +130 -0
  14. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/inference_trust.py +3 -4
  15. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/jobs.py +21 -1
  16. netbox_data_import-2.3.0/netbox_data_import/migrations/0033_resolutionproposal.py +160 -0
  17. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/models.py +247 -23
  18. netbox_data_import-2.3.0/netbox_data_import/object_permissions.py +340 -0
  19. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/preview_row_actions.py +3 -3
  20. netbox_data_import-2.3.0/netbox_data_import/profile_yaml.py +350 -0
  21. netbox_data_import-2.3.0/netbox_data_import/proposal_decisions.py +92 -0
  22. netbox_data_import-2.3.0/netbox_data_import/proposal_jobs.py +226 -0
  23. netbox_data_import-2.3.0/netbox_data_import/proposal_presentation.py +317 -0
  24. netbox_data_import-2.3.0/netbox_data_import/proposal_response.py +129 -0
  25. netbox_data_import-2.3.0/netbox_data_import/proposal_tasks.py +183 -0
  26. netbox_data_import-2.3.0/netbox_data_import/resolution_proposals.py +164 -0
  27. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/review_workspace.py +1 -0
  28. netbox_data_import-2.3.0/netbox_data_import/static/netbox_data_import/js/trace_proposals.js +200 -0
  29. netbox_data_import-2.3.0/netbox_data_import/templates/netbox_data_import/_proposal_action.html +10 -0
  30. netbox_data_import-2.3.0/netbox_data_import/templates/netbox_data_import/_proposal_card.html +31 -0
  31. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/templates/netbox_data_import/trace_workspace.html +85 -31
  32. netbox_data_import-2.3.0/netbox_data_import/termination_proposal.py +207 -0
  33. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/helpers.py +17 -5
  34. netbox_data_import-2.3.0/netbox_data_import/tests/inference_http.py +236 -0
  35. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_api.py +118 -0
  36. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_atomic_exits.py +6 -0
  37. netbox_data_import-2.3.0/netbox_data_import/tests/test_configuration_surfaces.py +933 -0
  38. netbox_data_import-2.3.0/netbox_data_import/tests/test_digest_indexed_models.py +181 -0
  39. netbox_data_import-2.3.0/netbox_data_import/tests/test_inference_adapter.py +1119 -0
  40. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_inference_backend.py +14 -0
  41. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_inference_credentials.py +155 -31
  42. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_inference_secret_containment.py +14 -0
  43. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_inference_settings.py +103 -39
  44. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_legacy_profile_yaml.py +1 -1
  45. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_migrations.py +41 -0
  46. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_object_permission_writer.py +305 -1
  47. netbox_data_import-2.3.0/netbox_data_import/tests/test_proposal_decisions.py +507 -0
  48. netbox_data_import-2.3.0/netbox_data_import/tests/test_proposal_jobs.py +678 -0
  49. netbox_data_import-2.3.0/netbox_data_import/tests/test_proposal_response.py +150 -0
  50. netbox_data_import-2.3.0/netbox_data_import/tests/test_proposal_tasks.py +216 -0
  51. netbox_data_import-2.3.0/netbox_data_import/tests/test_proposal_workspace.py +1352 -0
  52. netbox_data_import-2.3.0/netbox_data_import/tests/test_resolution_proposal_api.py +316 -0
  53. netbox_data_import-2.3.0/netbox_data_import/tests/test_resolution_proposals.py +526 -0
  54. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_standard_views.py +90 -2
  55. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_target_field_catalog.py +4 -4
  56. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_trace_workspace.py +47 -1
  57. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_views.py +54 -55
  58. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_views_coverage2.py +1 -13
  59. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/urls.py +5 -0
  60. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/views.py +324 -374
  61. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import.egg-info/PKG-INFO +1 -1
  62. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import.egg-info/SOURCES.txt +23 -0
  63. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/pyproject.toml +2 -1
  64. netbox_data_import-2.1.0/netbox_data_import/api/views.py +0 -209
  65. netbox_data_import-2.1.0/netbox_data_import/graphql/schema.py +0 -22
  66. netbox_data_import-2.1.0/netbox_data_import/graphql/types.py +0 -23
  67. netbox_data_import-2.1.0/netbox_data_import/inference_adapter.py +0 -260
  68. netbox_data_import-2.1.0/netbox_data_import/object_permissions.py +0 -168
  69. netbox_data_import-2.1.0/netbox_data_import/tests/test_inference_adapter.py +0 -432
  70. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/LICENSE +0 -0
  71. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/README.md +0 -0
  72. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/adapter_config.py +0 -0
  73. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/adapter_forms.py +0 -0
  74. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/adapters.py +0 -0
  75. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/api/__init__.py +0 -0
  76. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/catalog.py +0 -0
  77. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/contact_resolution.py +0 -0
  78. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/device_field_review.py +0 -0
  79. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/device_identity.py +0 -0
  80. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/field_keys.py +0 -0
  81. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/filters.py +0 -0
  82. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/flat_workbook.py +0 -0
  83. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/forms.py +0 -0
  84. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/graphql/__init__.py +0 -0
  85. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/graphql/filters.py +0 -0
  86. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/import_engine.py +0 -0
  87. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/inference_connection_test.py +0 -0
  88. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/ip_assignment.py +0 -0
  89. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0001_initial.py +0 -0
  90. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0002_importjob.py +0 -0
  91. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0003_remove_classrolemapping_unique_class_role_mapping_per_profile_and_more.py +0 -0
  92. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0004_features.py +0 -0
  93. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0005_deviceexistingmatch.py +0 -0
  94. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0006_deviceexistingmatch_asset_tag.py +0 -0
  95. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0007_manufacturermapping.py +0 -0
  96. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0008_unique_constraints.py +0 -0
  97. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0009_add_rack_type_to_classrolemapping.py +0 -0
  98. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0010_alter_manufacturermapping_id.py +0 -0
  99. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0011_importprofile_capture_extra_data.py +0 -0
  100. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0012_classrolemapping_rack_type_related_name.py +0 -0
  101. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0013_remove_columnmapping_unique_target.py +0 -0
  102. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0014_alter_columnmapping_target_field_and_more.py +0 -0
  103. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0015_cleanup_duplicate_device_matches.py +0 -0
  104. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0016_deviceexistingmatch_ndi_devicematch_profile_device.py +0 -0
  105. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0017_native_contact_sync.py +0 -0
  106. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0018_ignoredfielddifference.py +0 -0
  107. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0019_deviceimportsource.py +0 -0
  108. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0020_migrate_import_source_custom_field.py +0 -0
  109. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0021_importprofile_adapter_config.py +0 -0
  110. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0022_migrate_profile_adapter_config.py +0 -0
  111. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0023_drop_moved_profile_columns.py +0 -0
  112. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0024_rename_importjob_importexecution.py +0 -0
  113. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0025_remove_importexecution_dry_run_and_more.py +0 -0
  114. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0026_alter_sourcedocument_profile.py +0 -0
  115. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0027_cableclassmapping_terminationresolution.py +0 -0
  116. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0028_cableimportsource.py +0 -0
  117. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0029_alter_cableimportsource_from_text_and_more.py +0 -0
  118. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0030_remove_device_type_creation_config.py +0 -0
  119. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0031_inferencebackend.py +0 -0
  120. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/0032_inference_timeout_floor.py +0 -0
  121. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/migrations/__init__.py +0 -0
  122. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/navigation.py +0 -0
  123. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/netbox_reader.py +0 -0
  124. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/plan.py +0 -0
  125. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/signals.py +0 -0
  126. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/source_resolution.py +0 -0
  127. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/static/netbox_data_import/js/conflict_modal.js +0 -0
  128. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/static/netbox_data_import/js/contact_candidate_modal.js +0 -0
  129. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/static/netbox_data_import/js/preview_row_actions.js +0 -0
  130. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/static/netbox_data_import/js/preview_row_controls.js +0 -0
  131. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/static/netbox_data_import/js/split_name_modal.js +0 -0
  132. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/static/netbox_data_import/js/sync_row_modal.js +0 -0
  133. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/static/netbox_data_import/js/trace_termination_picker.js +0 -0
  134. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tables.py +0 -0
  135. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/target_modules.py +0 -0
  136. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/target_runtime.py +0 -0
  137. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/template_content.py +0 -0
  138. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/templates/netbox_data_import/_import_progress.html +0 -0
  139. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/templates/netbox_data_import/analysis.html +0 -0
  140. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/templates/netbox_data_import/bulk_yaml_import.html +0 -0
  141. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/templates/netbox_data_import/cableclassmapping_edit.html +0 -0
  142. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/templates/netbox_data_import/classrolemapping_edit.html +0 -0
  143. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/templates/netbox_data_import/columnmapping_edit.html +0 -0
  144. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/templates/netbox_data_import/columntransformrule_edit.html +0 -0
  145. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/templates/netbox_data_import/device_import_data.html +0 -0
  146. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/templates/netbox_data_import/devicetypemapping_edit.html +0 -0
  147. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/templates/netbox_data_import/import_preview.html +0 -0
  148. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/templates/netbox_data_import/import_profile_yaml.html +0 -0
  149. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/templates/netbox_data_import/import_progress.html +0 -0
  150. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/templates/netbox_data_import/import_results.html +0 -0
  151. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/templates/netbox_data_import/import_setup.html +0 -0
  152. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/templates/netbox_data_import/importexecution_list.html +0 -0
  153. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/templates/netbox_data_import/importprofile.html +0 -0
  154. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/templates/netbox_data_import/importprofile_list.html +0 -0
  155. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/templates/netbox_data_import/inferencebackend.html +0 -0
  156. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/templates/netbox_data_import/source_resolution_list.html +0 -0
  157. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/__init__.py +0 -0
  158. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/conftest.py +0 -0
  159. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/fixtures/combined_name.xlsx +0 -0
  160. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/fixtures/sample_cans.xlsx +0 -0
  161. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/fixtures/trace_copper.xlsx +0 -0
  162. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/fixtures/trace_fiber.xlsx +0 -0
  163. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/isolated_settings.py +0 -0
  164. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/mixins.py +0 -0
  165. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/mock_discipline.py +0 -0
  166. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/netbox_configuration.py +0 -0
  167. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/parallel.py +0 -0
  168. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_adapter_config.py +0 -0
  169. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_adapter_config_migration.py +0 -0
  170. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_cable_module.py +0 -0
  171. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_ci_configuration.py +0 -0
  172. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_comment_style.py +0 -0
  173. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_contact_resolution_ajax.py +0 -0
  174. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_contact_role_suggestion.py +0 -0
  175. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_contact_sync.py +0 -0
  176. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_cutover_edge_contracts.py +0 -0
  177. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_devcontainer_e2e.py +0 -0
  178. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_device_module.py +0 -0
  179. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_execution_audit.py +0 -0
  180. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_graphql_filters.py +0 -0
  181. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_import_cutover.py +0 -0
  182. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_import_engine.py +0 -0
  183. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_import_execution.py +0 -0
  184. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_import_plan.py +0 -0
  185. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_import_source_migration.py +0 -0
  186. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_inference_connection_test.py +0 -0
  187. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_inference_trust.py +0 -0
  188. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_installation_docs.py +0 -0
  189. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_ip_target.py +0 -0
  190. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_misc_coverage.py +0 -0
  191. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_mock_discipline.py +0 -0
  192. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_model_base_conventions.py +0 -0
  193. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_models.py +0 -0
  194. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_module_boundaries.py +0 -0
  195. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_navigation.py +0 -0
  196. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_netbox_reader.py +0 -0
  197. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_parallel_test_setup.py +0 -0
  198. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_placement_reference_locks.py +0 -0
  199. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_preview_assets.py +0 -0
  200. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_preview_state_ownership.py +0 -0
  201. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_pristine_preview_rows.py +0 -0
  202. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_profile_policy_crud_scoping.py +0 -0
  203. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_quick_action_input_bounds.py +0 -0
  204. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_quick_action_object_permissions.py +0 -0
  205. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_quick_action_profile_id.py +0 -0
  206. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_rack_module.py +0 -0
  207. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_release_metadata.py +0 -0
  208. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_remove_device_type_creation_config_migration.py +0 -0
  209. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_review_actions.py +0 -0
  210. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_review_resolutions.py +0 -0
  211. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_review_workspace.py +0 -0
  212. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_serial_display.py +0 -0
  213. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_source_adapter.py +0 -0
  214. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_target_module_edge_contracts.py +0 -0
  215. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_trace_decisions.py +0 -0
  216. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_trace_workbook.py +0 -0
  217. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/tests/test_views_extra.py +0 -0
  218. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/trace_schema.py +0 -0
  219. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/trace_workbook.py +0 -0
  220. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/transform_regex.py +0 -0
  221. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/utils.py +0 -0
  222. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import/values.py +0 -0
  223. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import.egg-info/dependency_links.txt +0 -0
  224. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import.egg-info/requires.txt +0 -0
  225. {netbox_data_import-2.1.0 → netbox_data_import-2.3.0}/netbox_data_import.egg-info/top_level.txt +0 -0
  226. {netbox_data_import-2.1.0 → netbox_data_import-2.3.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.3.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.3.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
@@ -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,
@@ -19,6 +22,8 @@ from ..models import (
19
22
  ColumnTransformRule,
20
23
  SourceResolution,
21
24
  ImportExecution,
25
+ InferenceBackend,
26
+ ResolutionProposal,
22
27
  validate_adapter_target_module,
23
28
  validate_contact_candidate_resolution,
24
29
  validate_section_applicability,
@@ -61,6 +66,17 @@ class PolicySectionSerializer(PolicySectionApplicabilityMixin, ValidatedModelSer
61
66
  def validate_policy_row(self, attrs):
62
67
  """Check the fields this model resolves through the catalog. Subclasses override."""
63
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
+
64
80
 
65
81
  def _validate_target_keys(instance, attrs, names, *, allow_candidates=True, required=False):
66
82
  """Reject a target key the profile's Source Adapter cannot supply."""
@@ -143,6 +159,24 @@ class ColumnMappingSerializer(PolicySectionSerializer):
143
159
  _validate_target_keys(self.instance, attrs, ("target_field",), required=True)
144
160
 
145
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
+
146
180
  class _RackTypeSlugField(serializers.SlugRelatedField):
147
181
  """SlugRelatedField for RackType that defers the queryset import."""
148
182
 
@@ -278,3 +312,94 @@ class ImportExecutionSerializer(serializers.ModelSerializer):
278
312
  "failure_detail",
279
313
  ]
280
314
  read_only_fields = fields
315
+
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
+
341
+ class ResolutionProposalSerializer(serializers.ModelSerializer):
342
+ """Read-only serializer for the Resolution Proposal audit record."""
343
+
344
+ class Meta:
345
+ model = ResolutionProposal
346
+ fields = [
347
+ "id",
348
+ "profile",
349
+ "task_type",
350
+ "field_key",
351
+ "status",
352
+ "source_evidence",
353
+ "resolved_device_type",
354
+ "resolved_device_id",
355
+ "prompt_version",
356
+ "response_schema_version",
357
+ "candidate_snapshot",
358
+ "requested_by",
359
+ "created",
360
+ "last_updated",
361
+ "outcome",
362
+ "selected_candidate_id",
363
+ "selected_object_type",
364
+ "selected_object_id",
365
+ "explanation",
366
+ "backend_metadata",
367
+ "response_diagnostic",
368
+ "failure_reason",
369
+ "decision",
370
+ "decided_by",
371
+ "decided_at",
372
+ "written_resolution",
373
+ ]
374
+ read_only_fields = fields
375
+
376
+
377
+ class ResolutionProposalHistorySerializer(serializers.ModelSerializer):
378
+ """Serialize one attempt summary for profile-scoped field history."""
379
+
380
+ class Meta:
381
+ model = ResolutionProposal
382
+ fields = ["id", "created", "status", "outcome", "decision", "failure_reason"]
383
+ read_only_fields = fields
384
+
385
+
386
+ class InferenceBackendSerializer(NetBoxModelSerializer):
387
+ """Serialize one Inference Backend row with a write-only credential reference.
388
+
389
+ NetBox resolves this class by model name to freeze a delete event payload.
390
+ """
391
+
392
+ credential_reference = serializers.JSONField(write_only=True)
393
+
394
+ class Meta:
395
+ model = InferenceBackend
396
+ fields = [
397
+ "id",
398
+ "url",
399
+ "display",
400
+ *InferenceBackendForm.Meta.fields,
401
+ "custom_fields",
402
+ "created",
403
+ "last_updated",
404
+ ]
405
+ brief_fields = ["id", "url", "display", "backend_key", "display_name", "enabled"]
@@ -2,10 +2,13 @@
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 (
8
9
  ImportProfileViewSet,
10
+ CableClassMappingViewSet,
11
+ CableImportSourceViewSet,
9
12
  ColumnMappingViewSet,
10
13
  ClassRoleMappingViewSet,
11
14
  DeviceTypeMappingViewSet,
@@ -13,10 +16,15 @@ from .views import (
13
16
  ColumnTransformRuleViewSet,
14
17
  SourceResolutionViewSet,
15
18
  ImportExecutionViewSet,
19
+ InferenceBackendViewSet,
20
+ ResolutionProposalHistoryViewSet,
21
+ ResolutionProposalViewSet,
16
22
  )
17
23
 
18
24
  router = NetBoxRouter()
19
25
  router.register("profiles", ImportProfileViewSet)
26
+ router.register("cable-class-mappings", CableClassMappingViewSet)
27
+ router.register("cable-provenance", CableImportSourceViewSet)
20
28
  router.register("column-mappings", ColumnMappingViewSet)
21
29
  router.register("class-role-mappings", ClassRoleMappingViewSet)
22
30
  router.register("device-type-mappings", DeviceTypeMappingViewSet)
@@ -24,6 +32,15 @@ router.register("ignored-devices", IgnoredDeviceViewSet)
24
32
  router.register("column-transforms", ColumnTransformRuleViewSet)
25
33
  router.register("source-resolutions", SourceResolutionViewSet)
26
34
  router.register("executions", ImportExecutionViewSet)
35
+ router.register("ai-backends", InferenceBackendViewSet)
36
+ router.register("resolution-proposals", ResolutionProposalViewSet)
27
37
 
28
38
  app_name = "netbox_data_import"
29
- urlpatterns = router.urls
39
+ urlpatterns = [
40
+ path(
41
+ "resolution-proposal-history/",
42
+ ResolutionProposalHistoryViewSet.as_view({"get": "list"}),
43
+ name="resolutionproposalhistory-list",
44
+ ),
45
+ *router.urls,
46
+ ]
@@ -0,0 +1,293 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (C) 2025 Marcin Zieba <marcinpsk@gmail.com>
3
+ """DRF viewsets for the data-import plugin API."""
4
+
5
+ from django.http import Http404
6
+ from netbox.api.viewsets import NetBoxModelViewSet
7
+ from rest_framework import mixins, permissions, viewsets
8
+ from rest_framework.exceptions import PermissionDenied, ValidationError
9
+ from rest_framework.permissions import DjangoModelPermissions
10
+
11
+ from ..field_keys import SELECT_TERMINATION_TASK, parse_termination_field_key
12
+ from ..models import (
13
+ locked_profile_policy,
14
+ ImportProfile,
15
+ CableClassMapping,
16
+ CableImportSource,
17
+ ColumnMapping,
18
+ ClassRoleMapping,
19
+ DeviceTypeMapping,
20
+ IgnoredDevice,
21
+ ColumnTransformRule,
22
+ SourceResolution,
23
+ ImportExecution,
24
+ InferenceBackend,
25
+ ResolutionProposal,
26
+ )
27
+ from ..object_permissions import (
28
+ ObjectPermissionDenied,
29
+ delete_permission_scoped_objects,
30
+ save_permission_scoped_object,
31
+ )
32
+ from .serializers import (
33
+ ImportProfileSerializer,
34
+ CableClassMappingSerializer,
35
+ CableImportSourceSerializer,
36
+ ColumnMappingSerializer,
37
+ ClassRoleMappingSerializer,
38
+ DeviceTypeMappingSerializer,
39
+ IgnoredDeviceSerializer,
40
+ ColumnTransformRuleSerializer,
41
+ SourceResolutionSerializer,
42
+ ImportExecutionSerializer,
43
+ InferenceBackendSerializer,
44
+ ResolutionProposalHistorySerializer,
45
+ ResolutionProposalSerializer,
46
+ )
47
+
48
+
49
+ class DjangoModelPermissionsWithView(DjangoModelPermissions):
50
+ """Extends DjangoModelPermissions to require view_* permission for GET requests.
51
+
52
+ The stock DjangoModelPermissions does not map GET to any model permission,
53
+ so list/retrieve endpoints are accessible to any authenticated user. This
54
+ subclass closes that gap.
55
+ """
56
+
57
+ perms_map = {
58
+ **DjangoModelPermissions.perms_map,
59
+ "GET": ["%(app_label)s.view_%(model_name)s"],
60
+ "HEAD": ["%(app_label)s.view_%(model_name)s"],
61
+ "OPTIONS": [],
62
+ }
63
+
64
+
65
+ class ImportProfileViewSet(NetBoxModelViewSet):
66
+ """CRUD viewset for ImportProfile (NetBoxModel)."""
67
+
68
+ queryset = ImportProfile.objects.prefetch_related(
69
+ "tags", "column_mappings", "class_role_mappings", "device_type_mappings"
70
+ )
71
+ serializer_class = ImportProfileSerializer
72
+
73
+
74
+ class _ProfileScopedQuerySetMixin(viewsets.GenericViewSet):
75
+ """Restrict profile-owned rows and validate their optional profile filter."""
76
+
77
+ def get_queryset(self):
78
+ """Return viewable rows, filtered by a valid profile ID when supplied."""
79
+ qs = super().get_queryset().restrict(self.request.user, "view")
80
+ profile_id = self.request.query_params.get("profile_id")
81
+ if profile_id is not None:
82
+ try:
83
+ profile_id = int(profile_id)
84
+ except (TypeError, ValueError) as exc:
85
+ raise ValidationError({"profile_id": "Enter a whole number."}) from exc
86
+ qs = qs.filter(profile_id=profile_id)
87
+ return qs
88
+
89
+
90
+ class _PluginModelViewSet(_ProfileScopedQuerySetMixin, viewsets.ModelViewSet):
91
+ """Base class for plain-model viewsets in this plugin."""
92
+
93
+ permission_classes = [permissions.IsAuthenticated, DjangoModelPermissionsWithView]
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
+
161
+
162
+ class ColumnMappingViewSet(_PluginModelViewSet):
163
+ """CRUD viewset for ColumnMapping."""
164
+
165
+ queryset = ColumnMapping.objects.select_related("profile")
166
+ serializer_class = ColumnMappingSerializer
167
+
168
+
169
+ class CableClassMappingViewSet(_PluginModelViewSet):
170
+ """CRUD viewset for CableClassMapping."""
171
+
172
+ queryset = CableClassMapping.objects.select_related("profile")
173
+ serializer_class = CableClassMappingSerializer
174
+
175
+
176
+ class ClassRoleMappingViewSet(_PluginModelViewSet):
177
+ """CRUD viewset for ClassRoleMapping."""
178
+
179
+ queryset = ClassRoleMapping.objects.select_related("profile", "rack_type")
180
+ serializer_class = ClassRoleMappingSerializer
181
+
182
+
183
+ class DeviceTypeMappingViewSet(_PluginModelViewSet):
184
+ """CRUD viewset for DeviceTypeMapping."""
185
+
186
+ queryset = DeviceTypeMapping.objects.select_related("profile")
187
+ serializer_class = DeviceTypeMappingSerializer
188
+
189
+
190
+ class IgnoredDeviceViewSet(_PluginModelViewSet):
191
+ """CRUD viewset for IgnoredDevice."""
192
+
193
+ queryset = IgnoredDevice.objects.select_related("profile")
194
+ serializer_class = IgnoredDeviceSerializer
195
+
196
+
197
+ class ColumnTransformRuleViewSet(_PluginModelViewSet):
198
+ """CRUD viewset for ColumnTransformRule."""
199
+
200
+ queryset = ColumnTransformRule.objects.select_related("profile")
201
+ serializer_class = ColumnTransformRuleSerializer
202
+
203
+
204
+ class SourceResolutionViewSet(_PluginModelViewSet):
205
+ """CRUD viewset for SourceResolution (rerere)."""
206
+
207
+ queryset = SourceResolution.objects.select_related("profile")
208
+ serializer_class = SourceResolutionSerializer
209
+
210
+
211
+ class ImportExecutionViewSet(_ProfileScopedQuerySetMixin, viewsets.ReadOnlyModelViewSet):
212
+ """Read-only viewset for the Import Execution audit history."""
213
+
214
+ queryset = ImportExecution.objects.select_related("profile")
215
+ serializer_class = ImportExecutionSerializer
216
+ permission_classes = [permissions.IsAuthenticated, DjangoModelPermissionsWithView]
217
+
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
+
240
+ class ResolutionProposalViewSet(_ProfileScopedQuerySetMixin, viewsets.ReadOnlyModelViewSet):
241
+ """Read-only viewset for Resolution Proposal history."""
242
+
243
+ queryset = ResolutionProposal.objects.select_related("profile")
244
+ serializer_class = ResolutionProposalSerializer
245
+ permission_classes = [permissions.IsAuthenticated, DjangoModelPermissionsWithView]
246
+
247
+ def get_queryset(self):
248
+ """Return proposals only for profiles that the actor can view."""
249
+ profile_scope = ImportProfile.objects.restrict(self.request.user, "view")
250
+ return super().get_queryset().filter(profile__in=profile_scope)
251
+
252
+
253
+ class ResolutionProposalHistoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
254
+ """Read one field's complete paginated history with workspace permissions."""
255
+
256
+ queryset = ResolutionProposal.objects.none()
257
+ serializer_class = ResolutionProposalHistorySerializer
258
+ permission_classes = [permissions.IsAuthenticated]
259
+
260
+ def get_queryset(self):
261
+ """Return one profile field's history when the actor can view that profile."""
262
+ profile_values = self.request.query_params.getlist("profile_id")
263
+ field_values = self.request.query_params.getlist("field_key")
264
+ if len(profile_values) != 1 or not profile_values[0]:
265
+ raise ValidationError({"profile_id": "Provide one whole-number profile ID."})
266
+ if len(field_values) != 1 or not field_values[0]:
267
+ raise ValidationError({"field_key": "Provide one canonical termination field key."})
268
+ try:
269
+ profile_id = int(profile_values[0])
270
+ except (TypeError, ValueError) as exc:
271
+ raise ValidationError({"profile_id": "Provide one whole-number profile ID."}) from exc
272
+ try:
273
+ parse_termination_field_key(field_values[0])
274
+ except ValueError as exc:
275
+ raise ValidationError({"field_key": "Provide one canonical termination field key."}) from exc
276
+ if not ImportProfile.objects.restrict(self.request.user, "view").filter(pk=profile_id).exists():
277
+ raise Http404
278
+ return (
279
+ ResolutionProposal.objects.filter(
280
+ profile_id=profile_id,
281
+ task_type=SELECT_TERMINATION_TASK,
282
+ field_key=field_values[0],
283
+ )
284
+ .select_related("profile")
285
+ .order_by("-created", "-pk")
286
+ )
287
+
288
+
289
+ class InferenceBackendViewSet(NetBoxModelViewSet):
290
+ """CRUD viewset whose configuration fields derive from the Inference Backend form."""
291
+
292
+ queryset = InferenceBackend.objects.prefetch_related("tags")
293
+ 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
 
@@ -0,0 +1,30 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (C) 2025 Marcin Zieba <marcinpsk@gmail.com>
3
+
4
+ """GraphQL queries registered with NetBox."""
5
+
6
+ import strawberry
7
+ import strawberry_django
8
+
9
+ from .types import CableClassMappingType, ImportProfileType
10
+
11
+
12
+ @strawberry.type(name="Query")
13
+ class ImportProfilesQuery:
14
+ """Expose import profile detail and list queries."""
15
+
16
+ import_profile: ImportProfileType = strawberry_django.field()
17
+ import_profile_list: list[ImportProfileType] = strawberry_django.field()
18
+
19
+
20
+ @strawberry.type(name="Query")
21
+ class CableClassMappingsQuery:
22
+ """Expose CableClass mapping detail and list queries."""
23
+
24
+ cable_class_mapping: CableClassMappingType = strawberry_django.field()
25
+ cable_class_mapping_list: list[CableClassMappingType] = strawberry_django.field()
26
+
27
+
28
+ schema = [ImportProfilesQuery, CableClassMappingsQuery]
29
+
30
+ __all__ = ("CableClassMappingsQuery", "ImportProfilesQuery", "schema")
@@ -0,0 +1,42 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (C) 2025 Marcin Zieba <marcinpsk@gmail.com>
3
+
4
+ """GraphQL object types for import profile configuration."""
5
+
6
+ import strawberry_django
7
+ from netbox.graphql.types import BaseObjectType, NetBoxObjectType
8
+
9
+ from netbox_data_import.models import CableClassMapping, ImportProfile
10
+
11
+ from .filters import ImportProfileFilter
12
+
13
+ _type_kwargs = {"fields": "__all__", "pagination": True}
14
+ if ImportProfileFilter is not None: # pragma: no cover
15
+ _type_kwargs["filters"] = ImportProfileFilter
16
+
17
+
18
+ @strawberry_django.type(ImportProfile, **_type_kwargs)
19
+ class ImportProfileType(NetBoxObjectType):
20
+ """One saved source-file import configuration."""
21
+
22
+
23
+ @strawberry_django.type(
24
+ CableClassMapping,
25
+ fields=[
26
+ "id",
27
+ "profile",
28
+ "cable_class",
29
+ "cable_type_resolved",
30
+ "cable_type",
31
+ "cable_profile_resolved",
32
+ "cable_profile",
33
+ ],
34
+ pagination=True,
35
+ )
36
+ class CableClassMappingType(BaseObjectType):
37
+ """One trace CableClass policy decision."""
38
+
39
+ profile: ImportProfileType
40
+
41
+
42
+ __all__ = ("CableClassMappingType", "ImportProfileType")