netbox-data-import 2.3.0__tar.gz → 2.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/PKG-INFO +1 -1
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/__init__.py +1 -1
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/adapters.py +53 -2
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/cable_target.py +39 -43
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/catalog.py +1 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/forms.py +24 -3
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/import_engine.py +3 -1
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/inference_adapter.py +147 -48
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/inference_backend.py +18 -0
- netbox_data_import-2.4.0/netbox_data_import/inference_connection_test.py +146 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/inference_credentials.py +71 -20
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/inference_settings.py +3 -4
- netbox_data_import-2.4.0/netbox_data_import/inference_transport.py +349 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/ip_assignment.py +72 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/jobs.py +0 -23
- netbox_data_import-2.4.0/netbox_data_import/migrations/0034_tracedeviceresolution.py +41 -0
- netbox_data_import-2.4.0/netbox_data_import/migrations/0035_retire_superseded_proposals.py +28 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/models.py +69 -11
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/netbox_reader.py +6 -0
- netbox_data_import-2.4.0/netbox_data_import/object_permissions.py +677 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/profile_yaml.py +168 -60
- netbox_data_import-2.4.0/netbox_data_import/proposal_contract.py +30 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/proposal_jobs.py +12 -17
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/proposal_response.py +36 -16
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/review_workspace.py +74 -12
- netbox_data_import-2.4.0/netbox_data_import/source_trace.py +124 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/static/netbox_data_import/js/sync_row_modal.js +2 -1
- netbox_data_import-2.4.0/netbox_data_import/static/netbox_data_import/js/trace_device_picker.js +130 -0
- netbox_data_import-2.4.0/netbox_data_import/static/netbox_data_import/js/trace_picker_search.js +54 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/static/netbox_data_import/js/trace_termination_picker.js +16 -37
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/target_modules.py +244 -78
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/import_preview.html +43 -3
- netbox_data_import-2.4.0/netbox_data_import/templates/netbox_data_import/importprofile_edit.html +45 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/inferencebackend.html +20 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/trace_workspace.html +118 -2
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/termination_proposal.py +14 -6
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/helpers.py +37 -2
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/inference_http.py +7 -2
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/mock_discipline.py +2 -1
- netbox_data_import-2.4.0/netbox_data_import/tests/redaction.py +54 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_atomic_exits.py +4 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_cable_module.py +68 -3
- netbox_data_import-2.4.0/netbox_data_import/tests/test_configuration_docs.py +55 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_configuration_surfaces.py +317 -0
- netbox_data_import-2.4.0/netbox_data_import/tests/test_conflict_row_actions.py +241 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_contact_sync.py +2 -43
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_cutover_edge_contracts.py +10 -60
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_device_module.py +190 -15
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_digest_indexed_models.py +18 -1
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_import_cutover.py +95 -23
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_import_engine.py +109 -2
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_import_execution.py +105 -8
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_inference_adapter.py +431 -3
- netbox_data_import-2.4.0/netbox_data_import/tests/test_inference_connection_test.py +666 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_inference_credentials.py +80 -50
- netbox_data_import-2.4.0/netbox_data_import/tests/test_inference_secret_containment.py +338 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_inference_settings.py +6 -3
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_migrations.py +1 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_mock_discipline.py +11 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_module_boundaries.py +55 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_object_permission_writer.py +609 -3
- netbox_data_import-2.4.0/netbox_data_import/tests/test_policy_write_refusals.py +155 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_pristine_preview_rows.py +2 -18
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_proposal_jobs.py +82 -4
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_proposal_response.py +36 -16
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_proposal_tasks.py +12 -2
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_proposal_workspace.py +19 -2
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_quick_action_input_bounds.py +18 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_rack_module.py +53 -1
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_resolution_proposal_api.py +4 -3
- netbox_data_import-2.4.0/netbox_data_import/tests/test_retire_superseded_proposals_migration.py +173 -0
- netbox_data_import-2.4.0/netbox_data_import/tests/test_source_trace_contract.py +66 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_standard_views.py +23 -1
- netbox_data_import-2.4.0/netbox_data_import/tests/test_target_module_edge_contracts.py +1069 -0
- netbox_data_import-2.4.0/netbox_data_import/tests/test_trace_device_resolution.py +843 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_trace_workbook.py +0 -29
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_trace_workspace.py +161 -14
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_views.py +59 -1
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_views_coverage2.py +11 -8
- netbox_data_import-2.4.0/netbox_data_import/trace_device_resolution.py +482 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/trace_workbook.py +19 -104
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/urls.py +11 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/views.py +458 -93
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import.egg-info/PKG-INFO +1 -1
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import.egg-info/SOURCES.txt +15 -1
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/pyproject.toml +6 -2
- netbox_data_import-2.3.0/netbox_data_import/inference_connection_test.py +0 -73
- netbox_data_import-2.3.0/netbox_data_import/inference_transport.py +0 -130
- netbox_data_import-2.3.0/netbox_data_import/object_permissions.py +0 -340
- netbox_data_import-2.3.0/netbox_data_import/tests/test_inference_connection_test.py +0 -452
- netbox_data_import-2.3.0/netbox_data_import/tests/test_inference_secret_containment.py +0 -216
- netbox_data_import-2.3.0/netbox_data_import/tests/test_legacy_profile_yaml.py +0 -171
- netbox_data_import-2.3.0/netbox_data_import/tests/test_target_module_edge_contracts.py +0 -449
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/LICENSE +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/README.md +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/adapter_config.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/adapter_forms.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/api/__init__.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/api/serializers.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/api/urls.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/api/views.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/contact_resolution.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/device_field_review.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/device_identity.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/field_keys.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/filters.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/flat_workbook.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/graphql/__init__.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/graphql/filters.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/graphql/schema.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/graphql/types.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/inference_trust.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0001_initial.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0002_importjob.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0003_remove_classrolemapping_unique_class_role_mapping_per_profile_and_more.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0004_features.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0005_deviceexistingmatch.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0006_deviceexistingmatch_asset_tag.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0007_manufacturermapping.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0008_unique_constraints.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0009_add_rack_type_to_classrolemapping.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0010_alter_manufacturermapping_id.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0011_importprofile_capture_extra_data.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0012_classrolemapping_rack_type_related_name.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0013_remove_columnmapping_unique_target.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0014_alter_columnmapping_target_field_and_more.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0015_cleanup_duplicate_device_matches.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0016_deviceexistingmatch_ndi_devicematch_profile_device.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0017_native_contact_sync.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0018_ignoredfielddifference.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0019_deviceimportsource.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0020_migrate_import_source_custom_field.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0021_importprofile_adapter_config.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0022_migrate_profile_adapter_config.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0023_drop_moved_profile_columns.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0024_rename_importjob_importexecution.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0025_remove_importexecution_dry_run_and_more.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0026_alter_sourcedocument_profile.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0027_cableclassmapping_terminationresolution.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0028_cableimportsource.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0029_alter_cableimportsource_from_text_and_more.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0030_remove_device_type_creation_config.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0031_inferencebackend.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0032_inference_timeout_floor.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/0033_resolutionproposal.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/migrations/__init__.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/navigation.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/plan.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/preview_row_actions.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/proposal_decisions.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/proposal_presentation.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/proposal_tasks.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/resolution_proposals.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/signals.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/source_resolution.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/static/netbox_data_import/js/conflict_modal.js +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/static/netbox_data_import/js/contact_candidate_modal.js +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/static/netbox_data_import/js/preview_row_actions.js +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/static/netbox_data_import/js/preview_row_controls.js +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/static/netbox_data_import/js/split_name_modal.js +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/static/netbox_data_import/js/trace_proposals.js +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tables.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/target_runtime.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/template_content.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/_import_progress.html +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/_proposal_action.html +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/_proposal_card.html +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/analysis.html +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/bulk_yaml_import.html +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/cableclassmapping_edit.html +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/classrolemapping_edit.html +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/columnmapping_edit.html +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/columntransformrule_edit.html +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/device_import_data.html +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/devicetypemapping_edit.html +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/import_profile_yaml.html +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/import_progress.html +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/import_results.html +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/import_setup.html +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/importexecution_list.html +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/importprofile.html +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/importprofile_list.html +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/templates/netbox_data_import/source_resolution_list.html +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/__init__.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/conftest.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/fixtures/combined_name.xlsx +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/fixtures/sample_cans.xlsx +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/fixtures/trace_copper.xlsx +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/fixtures/trace_fiber.xlsx +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/isolated_settings.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/mixins.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/netbox_configuration.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/parallel.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_adapter_config.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_adapter_config_migration.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_api.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_ci_configuration.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_comment_style.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_contact_resolution_ajax.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_contact_role_suggestion.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_devcontainer_e2e.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_execution_audit.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_graphql_filters.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_import_plan.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_import_source_migration.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_inference_backend.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_inference_trust.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_installation_docs.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_ip_target.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_misc_coverage.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_model_base_conventions.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_models.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_navigation.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_netbox_reader.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_parallel_test_setup.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_placement_reference_locks.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_preview_assets.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_preview_state_ownership.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_profile_policy_crud_scoping.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_proposal_decisions.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_quick_action_object_permissions.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_quick_action_profile_id.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_release_metadata.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_remove_device_type_creation_config_migration.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_resolution_proposals.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_review_actions.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_review_resolutions.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_review_workspace.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_serial_display.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_source_adapter.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_target_field_catalog.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_trace_decisions.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/tests/test_views_extra.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/trace_schema.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/transform_regex.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/utils.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/values.py +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import.egg-info/dependency_links.txt +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import.egg-info/requires.txt +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import.egg-info/top_level.txt +0 -0
- {netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/setup.cfg +0 -0
|
@@ -13,13 +13,14 @@ the NetBox boundary.
|
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
-
from dataclasses import dataclass, field
|
|
16
|
+
from dataclasses import dataclass, field, replace
|
|
17
17
|
from typing import TYPE_CHECKING
|
|
18
18
|
|
|
19
19
|
from .catalog import OutputKind, has_implemented_module
|
|
20
|
+
from .values import identity_text
|
|
20
21
|
|
|
21
22
|
if TYPE_CHECKING:
|
|
22
|
-
from .
|
|
23
|
+
from .source_trace import SourceTrace
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
class UnknownSourceAdapter(Exception):
|
|
@@ -39,6 +40,16 @@ class SourceDiagnostic:
|
|
|
39
40
|
row_number: int | None = None
|
|
40
41
|
|
|
41
42
|
|
|
43
|
+
def _foreign_row_number(row) -> int | None:
|
|
44
|
+
"""Return the source row a foreign row states, so its diagnostic can name a location."""
|
|
45
|
+
provenance = getattr(row, "provenance", ())
|
|
46
|
+
if provenance:
|
|
47
|
+
return provenance[0].row_start
|
|
48
|
+
if isinstance(row, dict):
|
|
49
|
+
return row.get("_row_number")
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
42
53
|
@dataclass(frozen=True)
|
|
43
54
|
class SourceBatch:
|
|
44
55
|
"""The typed source items and source diagnostics from one file (section 1)."""
|
|
@@ -48,6 +59,46 @@ class SourceBatch:
|
|
|
48
59
|
diagnostics: tuple[SourceDiagnostic, ...] = ()
|
|
49
60
|
unused_columns: dict[str, dict] = field(default_factory=dict)
|
|
50
61
|
|
|
62
|
+
def __post_init__(self):
|
|
63
|
+
"""Exclude a row the output kinds cannot carry, and mark a Trace with empty Device references."""
|
|
64
|
+
from .source_trace import SourceTrace
|
|
65
|
+
|
|
66
|
+
trace_batch = OutputKind.SOURCE_TRACE in self.output_kinds
|
|
67
|
+
expected = SourceTrace if trace_batch else dict
|
|
68
|
+
carries = "Source Traces" if trace_batch else "source rows"
|
|
69
|
+
rows = []
|
|
70
|
+
diagnostics = list(self.diagnostics)
|
|
71
|
+
for row in self.rows:
|
|
72
|
+
if not isinstance(row, expected):
|
|
73
|
+
# Excluded here, because every reader below reads the row the output kinds declare.
|
|
74
|
+
diagnostics.append(
|
|
75
|
+
SourceDiagnostic(
|
|
76
|
+
code="source.row_type_unexpected",
|
|
77
|
+
message=f"This batch carries {carries} only; got {type(row).__name__}.",
|
|
78
|
+
row_number=_foreign_row_number(row),
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
continue
|
|
82
|
+
if not trace_batch:
|
|
83
|
+
rows.append(row)
|
|
84
|
+
continue
|
|
85
|
+
summary = row.endpoint_summary
|
|
86
|
+
references = [summary.from_termination, summary.to_termination, *row.corroboration]
|
|
87
|
+
for segment in row.segments:
|
|
88
|
+
references.extend((segment.left, segment.right))
|
|
89
|
+
if all(identity_text(reference.device) for reference in references):
|
|
90
|
+
rows.append(row)
|
|
91
|
+
continue
|
|
92
|
+
diagnostic = SourceDiagnostic(
|
|
93
|
+
code="trace.device_required",
|
|
94
|
+
message="A Source Trace Termination Reference must name a Device.",
|
|
95
|
+
row_number=row.provenance[0].row_start,
|
|
96
|
+
)
|
|
97
|
+
diagnostics.append(diagnostic)
|
|
98
|
+
rows.append(replace(row, errors=(*row.errors, diagnostic)))
|
|
99
|
+
object.__setattr__(self, "rows", tuple(rows))
|
|
100
|
+
object.__setattr__(self, "diagnostics", tuple(diagnostics))
|
|
101
|
+
|
|
51
102
|
|
|
52
103
|
class SourceAdapter:
|
|
53
104
|
"""Base class for a Source Adapter declaration."""
|
|
@@ -36,6 +36,13 @@ from .field_keys import (
|
|
|
36
36
|
from .object_permissions import enforce_saved_object_permission
|
|
37
37
|
from .plan import Diagnostic, Disposition, PlannedChange, Severity, SynchronizationUnit
|
|
38
38
|
from .target_runtime import DeletedObject, PreconditionFailed
|
|
39
|
+
from .trace_device_resolution import (
|
|
40
|
+
STALE as DEVICE_STALE,
|
|
41
|
+
collect_trace_device_evidence,
|
|
42
|
+
resolve_trace_devices,
|
|
43
|
+
resolved_trace_device,
|
|
44
|
+
source_device_key,
|
|
45
|
+
)
|
|
39
46
|
from .values import identity_text, source_text
|
|
40
47
|
|
|
41
48
|
CABLE_STATUS = "connected"
|
|
@@ -207,7 +214,9 @@ class _TraceAnalysis:
|
|
|
207
214
|
segments: list = field(default_factory=list)
|
|
208
215
|
proven: dict = field(default_factory=dict)
|
|
209
216
|
policies: dict = field(default_factory=dict)
|
|
217
|
+
devices: dict = field(default_factory=dict)
|
|
210
218
|
terminations: dict = field(default_factory=dict)
|
|
219
|
+
resolution_started: bool = False
|
|
211
220
|
topology_read: bool = False
|
|
212
221
|
logical_cable: Any = None
|
|
213
222
|
retains_logical_cable: bool = False
|
|
@@ -303,38 +312,19 @@ def _source_record(trace, segment_index: int) -> dict:
|
|
|
303
312
|
}
|
|
304
313
|
|
|
305
314
|
|
|
306
|
-
def
|
|
307
|
-
"""Return the visible Devices at the import target, grouped by comparison name."""
|
|
308
|
-
from django.db.models import Q
|
|
309
|
-
|
|
310
|
-
wanted = {identity_text(name) for name in names if identity_text(name)}
|
|
311
|
-
if not wanted:
|
|
312
|
-
return {}
|
|
313
|
-
devices = netbox_reader.devices()
|
|
314
|
-
if netbox_reader.site is not None:
|
|
315
|
-
devices = devices.filter(site=netbox_reader.site)
|
|
316
|
-
if _lock_rows:
|
|
317
|
-
devices = devices.order_by("pk").select_for_update(of=("self",))
|
|
318
|
-
lookup = Q()
|
|
319
|
-
for name in sorted(wanted):
|
|
320
|
-
lookup |= Q(name__iexact=name)
|
|
321
|
-
grouped: dict[str, list] = {}
|
|
322
|
-
for device in devices.filter(lookup):
|
|
323
|
-
comparison = identity_text(device.name)
|
|
324
|
-
if comparison in wanted:
|
|
325
|
-
grouped.setdefault(comparison, []).append(device)
|
|
326
|
-
return grouped
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
def resolved_device_for(field_key: str, netbox_reader, *, _lock_rows=False):
|
|
315
|
+
def resolved_device_for(field_key: str, netbox_reader, *, profile, _lock_rows=False):
|
|
330
316
|
"""Return the one visible Device a termination field key names, or None when it names no single one.
|
|
331
317
|
|
|
332
318
|
A proposal freezes this reference and revalidates it, so the retrieval and the freshness check
|
|
333
319
|
have to agree on what "the resolved Device" means.
|
|
334
320
|
"""
|
|
335
321
|
device_name = parse_termination_field_key(field_key)["device"]
|
|
336
|
-
|
|
337
|
-
|
|
322
|
+
return resolved_trace_device(
|
|
323
|
+
profile=profile,
|
|
324
|
+
reader=netbox_reader,
|
|
325
|
+
source_label=device_name,
|
|
326
|
+
lock_rows=_lock_rows,
|
|
327
|
+
)
|
|
338
328
|
|
|
339
329
|
|
|
340
330
|
_RESOLVED_DEVICE_UNSET = object()
|
|
@@ -361,7 +351,7 @@ def eligible_terminations(
|
|
|
361
351
|
kind = parsed["kind"]
|
|
362
352
|
accessor = _READER_ACCESSOR_BY_KIND[kind]
|
|
363
353
|
device = (
|
|
364
|
-
resolved_device_for(field_key, netbox_reader, _lock_rows=_lock_rows)
|
|
354
|
+
resolved_device_for(field_key, netbox_reader, profile=profile, _lock_rows=_lock_rows)
|
|
365
355
|
if _resolved_device is _RESOLVED_DEVICE_UNSET
|
|
366
356
|
else _resolved_device
|
|
367
357
|
)
|
|
@@ -430,6 +420,7 @@ class _CableBatch:
|
|
|
430
420
|
"""Plan every Source Trace in one batch, so an identical shared segment plans once."""
|
|
431
421
|
|
|
432
422
|
def __init__(self, traces, profile, netbox_reader, *, lock_plan_references: bool = False):
|
|
423
|
+
traces = tuple(traces)
|
|
433
424
|
self.profile = profile
|
|
434
425
|
self.reader = netbox_reader
|
|
435
426
|
self.actor = netbox_reader.actor
|
|
@@ -445,7 +436,14 @@ class _CableBatch:
|
|
|
445
436
|
self._occupied: dict[tuple[str, int], _ExistingCable] = {}
|
|
446
437
|
self._mapping_rows: dict[str, Any] | None = None
|
|
447
438
|
self._stored = self._stored_resolutions()
|
|
448
|
-
|
|
439
|
+
active_traces = (analysis.trace for analysis in self.analyses if not analysis.stopped)
|
|
440
|
+
self._device_evidence = collect_trace_device_evidence(active_traces)
|
|
441
|
+
self._device_resolutions = resolve_trace_devices(
|
|
442
|
+
profile=self.profile,
|
|
443
|
+
reader=self.reader,
|
|
444
|
+
evidence=self._device_evidence,
|
|
445
|
+
lock_rows=self.lock_plan_references,
|
|
446
|
+
)
|
|
449
447
|
self._resolve_terminations()
|
|
450
448
|
self._load_mappings()
|
|
451
449
|
self._build_segments()
|
|
@@ -512,16 +510,6 @@ class _CableBatch:
|
|
|
512
510
|
).select_related("selected_object_type")
|
|
513
511
|
return {row.field_key: row for row in rows}
|
|
514
512
|
|
|
515
|
-
def _load_devices(self) -> dict[str, list]:
|
|
516
|
-
"""Return the visible Devices every planned trace names, grouped by comparison name."""
|
|
517
|
-
names = [
|
|
518
|
-
reference.device
|
|
519
|
-
for analysis in self.analyses
|
|
520
|
-
if not analysis.stopped
|
|
521
|
-
for reference in self._references(analysis.trace)
|
|
522
|
-
]
|
|
523
|
-
return _visible_devices(self.reader, names)
|
|
524
|
-
|
|
525
513
|
def _components_for(self, device_id: int, kind: str) -> dict[str, list]:
|
|
526
514
|
"""Return one Device's terminations of one kind, grouped by comparison name."""
|
|
527
515
|
cached = self._components.get((device_id, kind))
|
|
@@ -537,6 +525,7 @@ class _CableBatch:
|
|
|
537
525
|
for analysis in self.analyses:
|
|
538
526
|
if analysis.stopped:
|
|
539
527
|
continue
|
|
528
|
+
analysis.resolution_started = True
|
|
540
529
|
resolved: dict[tuple, _Termination] = {}
|
|
541
530
|
for reference in self._references(analysis.trace):
|
|
542
531
|
if reference.identity_key in resolved:
|
|
@@ -548,19 +537,24 @@ class _CableBatch:
|
|
|
548
537
|
|
|
549
538
|
def _resolve_one(self, analysis: _TraceAnalysis, reference) -> _Termination | None:
|
|
550
539
|
"""Return the NetBox object one Termination Reference names, or record the open decision."""
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
540
|
+
resolution = self._device_resolutions[source_device_key(reference.device)]
|
|
541
|
+
analysis.devices.setdefault(resolution.evidence.key, resolution.to_question())
|
|
542
|
+
if resolution.device is None:
|
|
543
|
+
code = "trace.device_resolution_stale" if resolution.state == DEVICE_STALE else "trace.device_unresolved"
|
|
544
|
+
analysis.block(
|
|
545
|
+
code,
|
|
546
|
+
{**_reference_display(reference), "matches": resolution.exact_match_count},
|
|
547
|
+
)
|
|
554
548
|
# With no resolved Device there is nothing to pick from, so the picker cannot help here.
|
|
555
549
|
self._record_resolution(
|
|
556
550
|
analysis,
|
|
557
551
|
reference,
|
|
558
552
|
UNRESOLVED,
|
|
559
553
|
None,
|
|
560
|
-
reason=
|
|
554
|
+
reason="Resolve the source Device before choosing one of its ports.",
|
|
561
555
|
)
|
|
562
556
|
return None
|
|
563
|
-
device =
|
|
557
|
+
device = resolution.device
|
|
564
558
|
stored = self._stored.get(_field_key(reference))
|
|
565
559
|
if stored is not None:
|
|
566
560
|
termination = self._stored_termination(analysis, reference, device, stored)
|
|
@@ -1222,9 +1216,11 @@ class _CableBatch:
|
|
|
1222
1216
|
},
|
|
1223
1217
|
"segments": segments,
|
|
1224
1218
|
"logical_cable": self._logical_cable_display(analysis),
|
|
1219
|
+
"resolution_started": analysis.resolution_started,
|
|
1225
1220
|
"topology_known": analysis.topology_read,
|
|
1226
1221
|
# A unit with no changes proposes nothing, so the panel must not offer to delete one.
|
|
1227
1222
|
"deletes_logical_cable": writes and analysis.deleted_logical_cable is not None,
|
|
1223
|
+
"devices": list(analysis.devices.values()),
|
|
1228
1224
|
"terminations": list(analysis.terminations.values()),
|
|
1229
1225
|
}
|
|
1230
1226
|
|
|
@@ -259,6 +259,7 @@ POLICY_SECTIONS: tuple[PolicySection, ...] = (
|
|
|
259
259
|
PolicySection("source_resolutions", "Source Resolutions", _DEVICE_ONLY),
|
|
260
260
|
PolicySection("device_existing_matches", "Device Existing Matches", _DEVICE_ONLY),
|
|
261
261
|
PolicySection("ignored_field_differences", "Ignored Field Differences", _DEVICE_ONLY),
|
|
262
|
+
PolicySection("trace_device_resolutions", "Trace Device Resolutions", _TRACE_ONLY),
|
|
262
263
|
PolicySection("termination_resolutions", "Termination Resolutions", _TRACE_ONLY),
|
|
263
264
|
PolicySection("cable_class_mappings", "CableClass Mappings", _TRACE_ONLY),
|
|
264
265
|
)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import copy
|
|
4
4
|
|
|
5
5
|
from django import forms
|
|
6
|
+
from django.utils.html import format_html
|
|
6
7
|
from dcim.models import Site, Location
|
|
7
8
|
from tenancy.models import Tenant
|
|
8
9
|
from netbox.forms import NetBoxModelBulkEditForm, NetBoxModelForm, NetBoxModelImportForm
|
|
@@ -25,6 +26,24 @@ from .models import (
|
|
|
25
26
|
|
|
26
27
|
_EXPLICIT_NONE = "__explicit_none__"
|
|
27
28
|
|
|
29
|
+
_CREDENTIAL_REFERENCE_EXAMPLE = """{
|
|
30
|
+
"backend": "vault_kv_v2",
|
|
31
|
+
"mount": "secret",
|
|
32
|
+
"path": "inference/backend",
|
|
33
|
+
"field": "api_key"
|
|
34
|
+
}"""
|
|
35
|
+
|
|
36
|
+
_CREDENTIAL_REFERENCE_HELP = format_html(
|
|
37
|
+
"This JSON tells the plugin where to find the API key in Vault. It does not contain the API key. "
|
|
38
|
+
"Configure Vault first in the NetBox plugin settings at "
|
|
39
|
+
"<code>PLUGINS_CONFIG["netbox_data_import"]["vault"]</code>. "
|
|
40
|
+
'<details class="mt-1" data-credential-reference-help>'
|
|
41
|
+
"<summary>Show the required JSON shape</summary>"
|
|
42
|
+
'<pre class="mt-2 mb-0"><code>{}</code></pre>'
|
|
43
|
+
"</details>",
|
|
44
|
+
_CREDENTIAL_REFERENCE_EXAMPLE,
|
|
45
|
+
)
|
|
46
|
+
|
|
28
47
|
|
|
29
48
|
class _RuntimeCableChoiceField(forms.ChoiceField):
|
|
30
49
|
"""Render only current choices while letting shared validation classify submitted values."""
|
|
@@ -132,7 +151,7 @@ class ImportProfileForm(NetBoxModelForm):
|
|
|
132
151
|
return self.instance.source_adapter
|
|
133
152
|
if self.is_bound:
|
|
134
153
|
return self.data.get(self.add_prefix("source_adapter")) or self.instance.source_adapter
|
|
135
|
-
return self.instance.source_adapter
|
|
154
|
+
return self.initial.get("source_adapter") or self.instance.source_adapter
|
|
136
155
|
|
|
137
156
|
def clean(self):
|
|
138
157
|
"""Collect the adapter-declared fields into ``adapter_config``."""
|
|
@@ -306,8 +325,10 @@ class InferenceBackendForm(NetBoxModelForm):
|
|
|
306
325
|
"tags",
|
|
307
326
|
)
|
|
308
327
|
help_texts = {
|
|
309
|
-
"credential_reference":
|
|
310
|
-
|
|
328
|
+
"credential_reference": _CREDENTIAL_REFERENCE_HELP,
|
|
329
|
+
"model": (
|
|
330
|
+
"Enter the exact model id. After you save the backend, run the connection test to discover "
|
|
331
|
+
"model suggestions when the endpoint supports them."
|
|
311
332
|
),
|
|
312
333
|
}
|
|
313
334
|
|
|
@@ -18,7 +18,7 @@ from django.db import DatabaseError
|
|
|
18
18
|
from . import adapter_config, adapters, catalog, target_modules
|
|
19
19
|
from .models import FailureReason, ImportExecution, SourceDocument, locked_profile_policy
|
|
20
20
|
from .netbox_reader import NetBoxReader, PlanningTargetUnavailable
|
|
21
|
-
from .object_permissions import ObjectPermissionDenied
|
|
21
|
+
from .object_permissions import ObjectPermissionDenied, clear_user_permission_caches
|
|
22
22
|
from .plan import Diagnostic, Disposition, ImportPlan, PlanInvalid, Severity, executable_units, merge_changes
|
|
23
23
|
from .source_resolution import derive_effective_rows
|
|
24
24
|
from .target_runtime import DeletedObject, ExecutionContext, PreconditionFailed
|
|
@@ -225,6 +225,7 @@ class ImportEngine:
|
|
|
225
225
|
progress_callback,
|
|
226
226
|
) -> None:
|
|
227
227
|
"""Compare the selection against a fresh plan and apply it, inside the caller's transaction."""
|
|
228
|
+
clear_user_permission_caches(actor)
|
|
228
229
|
current = cls.plan(
|
|
229
230
|
profile,
|
|
230
231
|
source_document,
|
|
@@ -253,6 +254,7 @@ class ImportEngine:
|
|
|
253
254
|
if runtime is None:
|
|
254
255
|
raise EngineConfigurationError(f"No Target Module runtime is registered for '{change.target_module}'.")
|
|
255
256
|
try:
|
|
257
|
+
clear_user_permission_caches(actor)
|
|
256
258
|
applied = runtime.apply(change, context)
|
|
257
259
|
except (PreconditionFailed, ObjectPermissionDenied, ValidationError, DatabaseError) as exc:
|
|
258
260
|
raise _ExecutionFailed(
|
{netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/inference_adapter.py
RENAMED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
# SPDX-FileCopyrightText: 2026 Marcin Zieba <marcinpsk@gmail.com>
|
|
3
3
|
"""The OpenAI-compatible Inference Backend adapter (specification 8.1, 8.3, 8.4).
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
nothing about NetBox, candidates, proposals or jobs: it takes an
|
|
7
|
-
InferenceCompletion, or raises a typed backend error.
|
|
5
|
+
The adapter makes one non-streaming Chat Completions call and can discover optional model-id
|
|
6
|
+
suggestions. It knows nothing about NetBox, candidates, proposals or jobs: it takes an
|
|
7
|
+
InferenceRequest and returns an InferenceCompletion, or raises a typed backend error.
|
|
8
8
|
|
|
9
9
|
A returned completion means the call reached the backend and the envelope parsed. Deciding what the
|
|
10
10
|
content means belongs to the application service, not here.
|
|
@@ -19,7 +19,15 @@ from contextlib import suppress
|
|
|
19
19
|
import requests
|
|
20
20
|
from urllib3.exceptions import ReadTimeoutError
|
|
21
21
|
|
|
22
|
-
from .inference_transport import
|
|
22
|
+
from .inference_transport import (
|
|
23
|
+
ResponseBodyTooLarge,
|
|
24
|
+
ResponseProcessingFailure,
|
|
25
|
+
WallClockDeadline,
|
|
26
|
+
WallClockDeadlineExceeded,
|
|
27
|
+
is_preconnect_failure,
|
|
28
|
+
request_to_resolved_address,
|
|
29
|
+
)
|
|
30
|
+
from .inference_settings import MODEL_MAX_LENGTH
|
|
23
31
|
from .inference_trust import (
|
|
24
32
|
InvalidInferenceConfiguration,
|
|
25
33
|
assert_resolved_address_allowed,
|
|
@@ -28,6 +36,10 @@ from .inference_trust import (
|
|
|
28
36
|
)
|
|
29
37
|
|
|
30
38
|
CHAT_COMPLETIONS_PATH = "/chat/completions"
|
|
39
|
+
MODELS_PATH = "/models"
|
|
40
|
+
CHAT_COMPLETION_RESPONSE_LIMIT = 65_536
|
|
41
|
+
MODEL_DISCOVERY_LIMIT = 100
|
|
42
|
+
MODEL_DISCOVERY_RESPONSE_LIMIT = 65_536
|
|
31
43
|
DIAGNOSTIC_TEXT_LIMIT = 4096
|
|
32
44
|
|
|
33
45
|
# Section 8.4 rejects every other terminal reason: only a completed answer is a completion.
|
|
@@ -193,6 +205,46 @@ def _retry_after(response) -> int | None:
|
|
|
193
205
|
return None
|
|
194
206
|
|
|
195
207
|
|
|
208
|
+
def _status_failure(status: int, diagnostic, retry_after: int | None) -> InferenceBackendError | None:
|
|
209
|
+
"""Return the typed failure one HTTP status maps to, or None when the status carries none."""
|
|
210
|
+
if 300 <= status < 400:
|
|
211
|
+
return InvalidBackendConfiguration(
|
|
212
|
+
f"The backend redirected the call (HTTP {status}), which is not followed.", diagnostic=diagnostic
|
|
213
|
+
)
|
|
214
|
+
if status in (401, 403):
|
|
215
|
+
return AuthenticationFailure(f"The backend refused the credential (HTTP {status}).", diagnostic=diagnostic)
|
|
216
|
+
if status == 429:
|
|
217
|
+
return RateLimited("The backend rate limited the call.", retry_after=retry_after, diagnostic=diagnostic)
|
|
218
|
+
if status in TRANSIENT_STATUSES:
|
|
219
|
+
return TransportFailure(f"The backend answered HTTP {status}.", diagnostic=diagnostic)
|
|
220
|
+
if status >= 400:
|
|
221
|
+
return InvalidBackendConfiguration(
|
|
222
|
+
f"The backend rejected the request (HTTP {status}), which repeating cannot fix.",
|
|
223
|
+
diagnostic=diagnostic,
|
|
224
|
+
)
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _processing_failure(exc: ResponseProcessingFailure, api_key: str) -> InferenceBackendError:
|
|
229
|
+
"""Return the typed failure one interrupted or refused response body maps to."""
|
|
230
|
+
diagnostic = ResponseDiagnostic(receipt=BODY_INTERRUPTED, status_code=exc.response.status_code)
|
|
231
|
+
if isinstance(exc.cause, ResponseBodyTooLarge):
|
|
232
|
+
# The body is refused unread, so the status is the only classification left.
|
|
233
|
+
return _status_failure(exc.response.status_code, diagnostic, _retry_after(exc.response)) or MalformedEnvelope(
|
|
234
|
+
"The backend response is too large.", diagnostic=diagnostic
|
|
235
|
+
)
|
|
236
|
+
if isinstance(exc.cause, ValueError):
|
|
237
|
+
return InvalidBackendConfiguration(
|
|
238
|
+
f"The backend answered with a location this delivery cannot use ({type(exc.cause).__name__}).",
|
|
239
|
+
diagnostic=_diagnostic(exc.response, api_key),
|
|
240
|
+
)
|
|
241
|
+
if isinstance(exc.cause, requests.Timeout) or _is_response_read_timeout(exc.cause):
|
|
242
|
+
return BackendTimeout(
|
|
243
|
+
"The backend did not finish its answer inside the configured limits.", diagnostic=diagnostic
|
|
244
|
+
)
|
|
245
|
+
return TransportFailure(f"The backend answer was interrupted ({type(exc.cause).__name__}).", diagnostic=diagnostic)
|
|
246
|
+
|
|
247
|
+
|
|
196
248
|
def _is_response_read_timeout(exc: Exception) -> bool:
|
|
197
249
|
"""Return whether Requests timed out after it had received response headers."""
|
|
198
250
|
return isinstance(exc, requests.ConnectionError) and bool(exc.args) and isinstance(exc.args[0], ReadTimeoutError)
|
|
@@ -211,6 +263,7 @@ class OpenAICompatibleAdapter:
|
|
|
211
263
|
connect_timeout: int = 5,
|
|
212
264
|
read_timeout: int = 60,
|
|
213
265
|
session: "requests.Session | None" = None,
|
|
266
|
+
deadline: WallClockDeadline | None = None,
|
|
214
267
|
):
|
|
215
268
|
# Verbatim: normalizing here would pass the request-time recheck a value the form refuses.
|
|
216
269
|
self.api_root = api_root
|
|
@@ -221,6 +274,7 @@ class OpenAICompatibleAdapter:
|
|
|
221
274
|
self.connect_timeout = connect_timeout
|
|
222
275
|
self.read_timeout = read_timeout
|
|
223
276
|
self._session = session or requests.Session()
|
|
277
|
+
self._deadline = deadline
|
|
224
278
|
|
|
225
279
|
def _check_response_mode(self, request: InferenceRequest) -> None:
|
|
226
280
|
"""Reject a request for a mode this backend is not configured to serve."""
|
|
@@ -253,17 +307,32 @@ class OpenAICompatibleAdapter:
|
|
|
253
307
|
"""Return all approved addresses, rechecking the destination at request time."""
|
|
254
308
|
try:
|
|
255
309
|
validate_api_root(self.api_root, self.allowlist, self.authentication)
|
|
256
|
-
addresses =
|
|
310
|
+
addresses = (
|
|
311
|
+
resolve_addresses(self.api_root)
|
|
312
|
+
if self._deadline is None
|
|
313
|
+
else self._deadline.run(resolve_addresses, self.api_root)
|
|
314
|
+
)
|
|
257
315
|
assert_resolved_address_allowed(self.api_root, self.allowlist, addresses)
|
|
316
|
+
except WallClockDeadlineExceeded:
|
|
317
|
+
raise BackendTimeout("The connection test exceeded its overall time limit.") from None
|
|
258
318
|
except InvalidInferenceConfiguration as exc:
|
|
259
319
|
raise InvalidBackendConfiguration(str(exc)) from None
|
|
260
320
|
return addresses
|
|
261
321
|
|
|
262
|
-
def
|
|
263
|
-
|
|
264
|
-
|
|
322
|
+
def _send(
|
|
323
|
+
self,
|
|
324
|
+
method: str,
|
|
325
|
+
path: str,
|
|
326
|
+
api_key: str,
|
|
327
|
+
*,
|
|
328
|
+
response_body_limit: int,
|
|
329
|
+
**kwargs,
|
|
330
|
+
) -> requests.Response:
|
|
331
|
+
"""Send one authenticated request through the checked and pinned destination boundary."""
|
|
265
332
|
resolved_addresses = self._resolved_destinations()
|
|
266
|
-
headers = {"
|
|
333
|
+
headers = {"Accept": "application/json"}
|
|
334
|
+
if "json" in kwargs:
|
|
335
|
+
headers["Content-Type"] = "application/json"
|
|
267
336
|
if self.authentication == "bearer":
|
|
268
337
|
headers["Authorization"] = f"Bearer {api_key}"
|
|
269
338
|
connection_failure: requests.RequestException | None = None
|
|
@@ -271,31 +340,19 @@ class OpenAICompatibleAdapter:
|
|
|
271
340
|
try:
|
|
272
341
|
response = request_to_resolved_address(
|
|
273
342
|
self._session,
|
|
274
|
-
|
|
275
|
-
f"{self.api_root}{
|
|
343
|
+
method,
|
|
344
|
+
f"{self.api_root}{path}",
|
|
276
345
|
resolved_address,
|
|
277
|
-
json=self._body(request),
|
|
278
346
|
headers=headers,
|
|
279
347
|
timeout=(self.connect_timeout, self.read_timeout),
|
|
348
|
+
deadline=self._deadline,
|
|
349
|
+
response_body_limit=response_body_limit,
|
|
280
350
|
# A redirect is a different destination, so it is refused rather than followed.
|
|
281
351
|
allow_redirects=False,
|
|
352
|
+
**kwargs,
|
|
282
353
|
)
|
|
283
354
|
except ResponseProcessingFailure as exc:
|
|
284
|
-
|
|
285
|
-
raise InvalidBackendConfiguration(
|
|
286
|
-
f"The backend answered with a location this delivery cannot use ({type(exc.cause).__name__}).",
|
|
287
|
-
diagnostic=_diagnostic(exc.response, api_key),
|
|
288
|
-
) from None
|
|
289
|
-
diagnostic = ResponseDiagnostic(receipt=BODY_INTERRUPTED, status_code=exc.response.status_code)
|
|
290
|
-
if isinstance(exc.cause, requests.Timeout) or _is_response_read_timeout(exc.cause):
|
|
291
|
-
raise BackendTimeout(
|
|
292
|
-
"The backend did not finish its answer inside the configured limits.",
|
|
293
|
-
diagnostic=diagnostic,
|
|
294
|
-
) from None
|
|
295
|
-
raise TransportFailure(
|
|
296
|
-
f"The backend answer was interrupted ({type(exc.cause).__name__}).",
|
|
297
|
-
diagnostic=diagnostic,
|
|
298
|
-
) from None
|
|
355
|
+
raise _processing_failure(exc, api_key) from None
|
|
299
356
|
except requests.ConnectTimeout as exc:
|
|
300
357
|
connection_failure = exc
|
|
301
358
|
continue
|
|
@@ -321,7 +378,7 @@ class OpenAICompatibleAdapter:
|
|
|
321
378
|
raise InvalidBackendConfiguration(
|
|
322
379
|
f"The backend answered with a location this delivery cannot use ({type(exc).__name__})."
|
|
323
380
|
) from None
|
|
324
|
-
return
|
|
381
|
+
return response
|
|
325
382
|
|
|
326
383
|
if isinstance(connection_failure, requests.ConnectTimeout):
|
|
327
384
|
raise BackendTimeout("The backend did not answer inside the configured limits.") from None
|
|
@@ -330,28 +387,68 @@ class OpenAICompatibleAdapter:
|
|
|
330
387
|
diagnostic=ABSENT_DIAGNOSTIC,
|
|
331
388
|
) from None
|
|
332
389
|
|
|
390
|
+
@staticmethod
|
|
391
|
+
def _raise_for_status(response: requests.Response, api_key: str) -> ResponseDiagnostic:
|
|
392
|
+
"""Return a safe diagnostic for success, or raise the typed HTTP failure."""
|
|
393
|
+
diagnostic = _diagnostic(response, api_key)
|
|
394
|
+
failure = _status_failure(response.status_code, diagnostic, _retry_after(response))
|
|
395
|
+
if failure is not None:
|
|
396
|
+
raise failure
|
|
397
|
+
return diagnostic
|
|
398
|
+
|
|
399
|
+
def complete(self, request: InferenceRequest, api_key: str) -> InferenceCompletion:
|
|
400
|
+
"""Return one completion, or raise the typed error the backend condition maps to."""
|
|
401
|
+
self._check_response_mode(request)
|
|
402
|
+
response = self._send(
|
|
403
|
+
"POST",
|
|
404
|
+
CHAT_COMPLETIONS_PATH,
|
|
405
|
+
api_key,
|
|
406
|
+
response_body_limit=CHAT_COMPLETION_RESPONSE_LIMIT,
|
|
407
|
+
json=self._body(request),
|
|
408
|
+
)
|
|
409
|
+
return self._read(response, api_key)
|
|
410
|
+
|
|
411
|
+
def discover_models(self, api_key: str) -> tuple[str, ...]:
|
|
412
|
+
"""Return bounded model-id suggestions from a compatible optional models endpoint."""
|
|
413
|
+
response = self._send(
|
|
414
|
+
"GET",
|
|
415
|
+
MODELS_PATH,
|
|
416
|
+
api_key,
|
|
417
|
+
response_body_limit=MODEL_DISCOVERY_RESPONSE_LIMIT,
|
|
418
|
+
)
|
|
419
|
+
diagnostic = self._raise_for_status(response, api_key)
|
|
420
|
+
if diagnostic.redacted:
|
|
421
|
+
raise MalformedEnvelope("The backend model list contained credential material.", diagnostic=diagnostic)
|
|
422
|
+
try:
|
|
423
|
+
envelope = response.json()
|
|
424
|
+
except (ValueError, RecursionError):
|
|
425
|
+
raise MalformedEnvelope("The backend model list is not JSON.", diagnostic=diagnostic) from None
|
|
426
|
+
if not isinstance(envelope, dict) or not isinstance(envelope.get("data"), list):
|
|
427
|
+
raise MalformedEnvelope("The backend did not provide a compatible model list.", diagnostic=diagnostic)
|
|
428
|
+
|
|
429
|
+
models = []
|
|
430
|
+
seen = set()
|
|
431
|
+
for item in envelope["data"]:
|
|
432
|
+
model_id = item.get("id") if isinstance(item, dict) else None
|
|
433
|
+
if (
|
|
434
|
+
not isinstance(model_id, str)
|
|
435
|
+
or not model_id
|
|
436
|
+
or model_id != model_id.strip()
|
|
437
|
+
or not model_id.isprintable()
|
|
438
|
+
or len(model_id) > MODEL_MAX_LENGTH
|
|
439
|
+
or model_id in seen
|
|
440
|
+
):
|
|
441
|
+
continue
|
|
442
|
+
seen.add(model_id)
|
|
443
|
+
models.append(model_id)
|
|
444
|
+
if len(models) == MODEL_DISCOVERY_LIMIT:
|
|
445
|
+
break
|
|
446
|
+
return tuple(models)
|
|
447
|
+
|
|
333
448
|
def _read(self, response, api_key: str = "") -> InferenceCompletion:
|
|
334
449
|
"""Classify the answer, then parse the one envelope a completed call returns."""
|
|
335
|
-
# Captured before any
|
|
336
|
-
diagnostic =
|
|
337
|
-
status = response.status_code
|
|
338
|
-
if status in (301, 302, 303, 307, 308):
|
|
339
|
-
raise InvalidBackendConfiguration(
|
|
340
|
-
f"The backend redirected the call (HTTP {status}), which is not followed.", diagnostic=diagnostic
|
|
341
|
-
)
|
|
342
|
-
if status in (401, 403):
|
|
343
|
-
raise AuthenticationFailure(f"The backend refused the credential (HTTP {status}).", diagnostic=diagnostic)
|
|
344
|
-
if status == 429:
|
|
345
|
-
raise RateLimited(
|
|
346
|
-
"The backend rate limited the call.", retry_after=_retry_after(response), diagnostic=diagnostic
|
|
347
|
-
)
|
|
348
|
-
if status in TRANSIENT_STATUSES:
|
|
349
|
-
raise TransportFailure(f"The backend answered HTTP {status}.", diagnostic=diagnostic)
|
|
350
|
-
if status >= 400:
|
|
351
|
-
raise InvalidBackendConfiguration(
|
|
352
|
-
f"The backend rejected the request (HTTP {status}), which repeating cannot fix.",
|
|
353
|
-
diagnostic=diagnostic,
|
|
354
|
-
)
|
|
450
|
+
# Captured before any parsing, so every failure below can carry what it received.
|
|
451
|
+
diagnostic = self._raise_for_status(response, api_key)
|
|
355
452
|
try:
|
|
356
453
|
envelope = response.json()
|
|
357
454
|
except (ValueError, RecursionError):
|
|
@@ -405,6 +502,8 @@ def encode_payload(value) -> str:
|
|
|
405
502
|
|
|
406
503
|
__all__ = (
|
|
407
504
|
"CHAT_COMPLETIONS_PATH",
|
|
505
|
+
"MODELS_PATH",
|
|
506
|
+
"MODEL_DISCOVERY_LIMIT",
|
|
408
507
|
"AuthenticationFailure",
|
|
409
508
|
"BackendTimeout",
|
|
410
509
|
"InferenceBackendError",
|
{netbox_data_import-2.3.0 → netbox_data_import-2.4.0}/netbox_data_import/inference_backend.py
RENAMED
|
@@ -93,6 +93,24 @@ class ResolvedInferenceBackend:
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
|
|
96
|
+
def adapter_for_backend(backend: ResolvedInferenceBackend, *, deadline=None):
|
|
97
|
+
"""Build the adapter that both proposal jobs and foreground tests use for this backend."""
|
|
98
|
+
if backend.adapter_type != "openai_compatible":
|
|
99
|
+
raise InvalidInferenceConfiguration(f"Unsupported Inference Backend adapter '{backend.adapter_type}'.")
|
|
100
|
+
from .inference_adapter import OpenAICompatibleAdapter
|
|
101
|
+
|
|
102
|
+
return OpenAICompatibleAdapter(
|
|
103
|
+
api_root=backend.api_root,
|
|
104
|
+
model=backend.model,
|
|
105
|
+
allowlist=origin_allowlist(),
|
|
106
|
+
authentication=backend.authentication,
|
|
107
|
+
response_mode=backend.response_mode,
|
|
108
|
+
connect_timeout=backend.connect_timeout,
|
|
109
|
+
read_timeout=backend.read_timeout,
|
|
110
|
+
deadline=deadline,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
96
114
|
def _from_row(row, allowlist) -> ResolvedInferenceBackend:
|
|
97
115
|
"""Return the resolved backend one enabled database row describes."""
|
|
98
116
|
# A saved row outlives the allowlist that approved it, so spec 8.3 validates both sources alike.
|