netbox-data-import 2.5.0__tar.gz → 2.6.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.5.0 → netbox_data_import-2.6.0}/PKG-INFO +1 -1
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/__init__.py +1 -1
- netbox_data_import-2.6.0/netbox_data_import/cable_disclosure.py +448 -0
- netbox_data_import-2.6.0/netbox_data_import/cable_policy.py +157 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/cable_target.py +325 -55
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/catalog.py +1 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/forms.py +53 -5
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/inference_backend.py +16 -1
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/jobs.py +3 -1
- netbox_data_import-2.6.0/netbox_data_import/migrations/0036_resolutionproposal_job.py +27 -0
- netbox_data_import-2.6.0/netbox_data_import/migrations/0037_cablesegmentoverride.py +42 -0
- netbox_data_import-2.6.0/netbox_data_import/migrations/0038_cable_tag_integrity.py +104 -0
- netbox_data_import-2.6.0/netbox_data_import/migrations/0039_remove_job_plan_copies.py +22 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/models.py +77 -76
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/plan.py +24 -4
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/preview_row_actions.py +1 -6
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/proposal_decisions.py +3 -3
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/proposal_jobs.py +8 -6
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/proposal_presentation.py +92 -19
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/proposal_tasks.py +46 -6
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/resolution_proposals.py +64 -1
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/review_workspace.py +191 -10
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/trace_picker_search.js +10 -1
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/trace_proposals.js +7 -8
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tables.py +30 -1
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/target_runtime.py +2 -7
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/_proposal_card.html +6 -4
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/trace_workspace.html +105 -7
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/helpers.py +60 -10
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/mock_discipline.py +79 -21
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_atomic_exits.py +12 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_cable_module.py +1236 -25
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_conflict_row_actions.py +1 -1
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_contact_resolution_ajax.py +4 -5
- netbox_data_import-2.6.0/netbox_data_import/tests/test_helpers.py +43 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_import_cutover.py +11 -5
- netbox_data_import-2.6.0/netbox_data_import/tests/test_job_plan_cleanup_migration.py +57 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_migrations.py +102 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_misc_coverage.py +5 -1
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_mock_discipline.py +54 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_proposal_decisions.py +26 -3
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_proposal_jobs.py +96 -3
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_proposal_tasks.py +48 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_proposal_workspace.py +441 -7
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_quick_action_input_bounds.py +1 -1
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_resolution_proposals.py +19 -1
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_review_actions.py +14 -1
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_review_resolutions.py +1 -1
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_review_workspace.py +8 -2
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_target_field_catalog.py +1 -1
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_trace_decisions.py +2 -2
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_trace_device_resolution.py +32 -2
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_trace_workbook.py +62 -16
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_trace_workspace.py +1414 -18
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_views.py +1 -1
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/trace_workbook.py +5 -14
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/urls.py +10 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/views.py +444 -32
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import.egg-info/PKG-INFO +1 -1
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import.egg-info/SOURCES.txt +8 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/pyproject.toml +1 -1
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/LICENSE +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/README.md +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/_dns_worker.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/adapter_config.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/adapter_forms.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/adapters.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/api/__init__.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/api/serializers.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/api/urls.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/api/views.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/contact_resolution.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/device_field_review.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/device_identity.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/field_keys.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/filters.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/flat_workbook.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/graphql/__init__.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/graphql/filters.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/graphql/schema.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/graphql/types.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/import_engine.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/inference_adapter.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/inference_connection_test.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/inference_credentials.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/inference_settings.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/inference_transport.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/inference_trust.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/ip_assignment.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0001_initial.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0002_importjob.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0003_remove_classrolemapping_unique_class_role_mapping_per_profile_and_more.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0004_features.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0005_deviceexistingmatch.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0006_deviceexistingmatch_asset_tag.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0007_manufacturermapping.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0008_unique_constraints.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0009_add_rack_type_to_classrolemapping.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0010_alter_manufacturermapping_id.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0011_importprofile_capture_extra_data.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0012_classrolemapping_rack_type_related_name.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0013_remove_columnmapping_unique_target.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0014_alter_columnmapping_target_field_and_more.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0015_cleanup_duplicate_device_matches.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0016_deviceexistingmatch_ndi_devicematch_profile_device.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0017_native_contact_sync.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0018_ignoredfielddifference.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0019_deviceimportsource.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0020_migrate_import_source_custom_field.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0021_importprofile_adapter_config.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0022_migrate_profile_adapter_config.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0023_drop_moved_profile_columns.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0024_rename_importjob_importexecution.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0025_remove_importexecution_dry_run_and_more.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0026_alter_sourcedocument_profile.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0027_cableclassmapping_terminationresolution.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0028_cableimportsource.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0029_alter_cableimportsource_from_text_and_more.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0030_remove_device_type_creation_config.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0031_inferencebackend.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0032_inference_timeout_floor.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0033_resolutionproposal.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0034_tracedeviceresolution.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/0035_retire_superseded_proposals.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/migrations/__init__.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/navigation.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/netbox_reader.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/object_permissions.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/profile_yaml.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/proposal_contract.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/proposal_response.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/signals.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/source_resolution.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/source_trace.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/conflict_modal.js +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/contact_candidate_modal.js +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/preview_row_actions.js +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/preview_row_controls.js +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/split_name_modal.js +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/sync_row_modal.js +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/trace_device_picker.js +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/static/netbox_data_import/js/trace_termination_picker.js +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/target_modules.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/template_content.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/_import_progress.html +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/_proposal_action.html +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/analysis.html +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/bulk_yaml_import.html +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/cableclassmapping_edit.html +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/classrolemapping_edit.html +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/columnmapping_edit.html +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/columntransformrule_edit.html +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/device_import_data.html +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/devicetypemapping_edit.html +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/import_preview.html +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/import_profile_yaml.html +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/import_progress.html +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/import_results.html +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/import_setup.html +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/importexecution_list.html +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/importprofile.html +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/importprofile_edit.html +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/importprofile_list.html +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/inferencebackend.html +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/templates/netbox_data_import/source_resolution_list.html +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/termination_proposal.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/__init__.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/conftest.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/fixtures/combined_name.xlsx +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/fixtures/sample_workbook.xlsx +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/fixtures/trace_copper.xlsx +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/fixtures/trace_fiber.xlsx +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/inference_http.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/isolated_settings.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/mixins.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/netbox_configuration.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/parallel.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/redaction.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_adapter_config.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_adapter_config_migration.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_api.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_ci_configuration.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_comment_style.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_configuration_docs.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_configuration_surfaces.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_contact_role_suggestion.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_contact_sync.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_cutover_edge_contracts.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_devcontainer_e2e.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_device_module.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_digest_indexed_models.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_execution_audit.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_graphql_filters.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_import_engine.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_import_execution.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_import_plan.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_import_source_migration.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_inference_adapter.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_inference_backend.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_inference_connection_test.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_inference_credentials.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_inference_secret_containment.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_inference_settings.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_inference_trust.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_installation_docs.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_ip_target.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_markdown_tables.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_model_base_conventions.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_models.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_module_boundaries.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_navigation.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_netbox_reader.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_object_permission_writer.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_parallel_test_setup.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_placement_reference_locks.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_policy_write_refusals.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_preview_assets.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_preview_state_ownership.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_pristine_preview_rows.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_profile_policy_crud_scoping.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_proposal_response.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_quick_action_object_permissions.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_quick_action_profile_id.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_rack_module.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_release_metadata.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_remove_device_type_creation_config_migration.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_resolution_proposal_api.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_retire_superseded_proposals_migration.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_serial_display.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_source_adapter.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_source_trace_contract.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_standard_views.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_target_module_edge_contracts.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_text_encoding.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_views_coverage2.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/tests/test_views_extra.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/trace_device_resolution.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/trace_schema.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/transform_regex.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/utils.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import/values.py +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import.egg-info/dependency_links.txt +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import.egg-info/requires.txt +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/netbox_data_import.egg-info/top_level.txt +0 -0
- {netbox_data_import-2.5.0 → netbox_data_import-2.6.0}/setup.cfg +0 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# SPDX-FileCopyrightText: 2026 Marcin Zieba <marcinpsk@gmail.com>
|
|
3
|
+
"""Apply live row permissions to cached Cable presentation data."""
|
|
4
|
+
|
|
5
|
+
from dataclasses import replace
|
|
6
|
+
from types import MappingProxyType
|
|
7
|
+
|
|
8
|
+
CABLE_ROW = "dcim.cable"
|
|
9
|
+
CABLE_CLASS_MAPPING_ROW = "netbox_data_import.cableclassmapping"
|
|
10
|
+
CABLE_SEGMENT_OVERRIDE_ROW = "netbox_data_import.cablesegmentoverride"
|
|
11
|
+
DISCLOSURE_SOURCE = "disclosure_source"
|
|
12
|
+
POLICY_HIDDEN = "a policy you cannot view"
|
|
13
|
+
POLICY_VISIBLE = "policy_visible"
|
|
14
|
+
POLICY_WRITE_REFUSED = "You cannot change a policy you cannot view."
|
|
15
|
+
|
|
16
|
+
_REFERENCE_FIELDS = frozenset({"device", "cards", "port", "port_class"})
|
|
17
|
+
|
|
18
|
+
_DIAGNOSTIC_DISCLOSURE_FIELDS = MappingProxyType(
|
|
19
|
+
{
|
|
20
|
+
"cable.ambiguous_mapped_peer": (_REFERENCE_FIELDS | {"peers"}, frozenset(), frozenset()),
|
|
21
|
+
"cable.attribute_drift": (
|
|
22
|
+
frozenset({"segment_index"}),
|
|
23
|
+
frozenset({"cable", "status", "type", "profile", "label"}),
|
|
24
|
+
frozenset(),
|
|
25
|
+
),
|
|
26
|
+
"cable.cableclass_unmapped": (frozenset({"segment_index", "cable_class"}), frozenset(), frozenset()),
|
|
27
|
+
"cable.media_family_mismatch": (
|
|
28
|
+
frozenset(),
|
|
29
|
+
frozenset({"segments"}),
|
|
30
|
+
frozenset({"segments"}),
|
|
31
|
+
),
|
|
32
|
+
"cable.multi_termination_conflict": (
|
|
33
|
+
frozenset({"segment_index", "port"}),
|
|
34
|
+
frozenset({"cable"}),
|
|
35
|
+
frozenset(),
|
|
36
|
+
),
|
|
37
|
+
"cable.pass_through_not_mapped": (
|
|
38
|
+
_REFERENCE_FIELDS | {"entry", "exit", "mapped"},
|
|
39
|
+
frozenset(),
|
|
40
|
+
frozenset(),
|
|
41
|
+
),
|
|
42
|
+
"cable.pass_through_verified": (
|
|
43
|
+
_REFERENCE_FIELDS | {"entry", "exit"},
|
|
44
|
+
frozenset(),
|
|
45
|
+
frozenset(),
|
|
46
|
+
),
|
|
47
|
+
"cable.permission_denied": (
|
|
48
|
+
_REFERENCE_FIELDS | {"permission"},
|
|
49
|
+
frozenset({"cable"}),
|
|
50
|
+
frozenset(),
|
|
51
|
+
),
|
|
52
|
+
"cable.planned_termination_conflict": (
|
|
53
|
+
frozenset({"segment_index", "termination", "competing_trace"}),
|
|
54
|
+
frozenset(),
|
|
55
|
+
frozenset(),
|
|
56
|
+
),
|
|
57
|
+
"cable.policy_stale": (frozenset({"segment_index", "cable_class"}), frozenset(), frozenset()),
|
|
58
|
+
"cable.profile_incompatible": (frozenset({"segment_index", "cable_class"}), frozenset(), frozenset()),
|
|
59
|
+
"cable.resolved_segment_conflict": (
|
|
60
|
+
frozenset({"segment_index", "terminations"}),
|
|
61
|
+
frozenset(),
|
|
62
|
+
frozenset({"cable_type", "cable_profile"}),
|
|
63
|
+
),
|
|
64
|
+
"cable.same_port_continuation": (_REFERENCE_FIELDS | {"peer"}, frozenset(), frozenset()),
|
|
65
|
+
"cable.segment_override_lost": (
|
|
66
|
+
frozenset({"segment_index"}),
|
|
67
|
+
frozenset(),
|
|
68
|
+
frozenset({"cable_type", "cable_profile"}),
|
|
69
|
+
),
|
|
70
|
+
"cable.segment_reused": (
|
|
71
|
+
frozenset({"segment_index"}),
|
|
72
|
+
frozenset({"cable"}),
|
|
73
|
+
frozenset(),
|
|
74
|
+
),
|
|
75
|
+
"cable.segment_self_connection": (
|
|
76
|
+
frozenset({"segment_index", "cable_class", "termination"}),
|
|
77
|
+
frozenset(),
|
|
78
|
+
frozenset(),
|
|
79
|
+
),
|
|
80
|
+
"cable.termination_kind_mismatch": (
|
|
81
|
+
_REFERENCE_FIELDS | {"selected_display_name", "claimed_kind", "selected_kind"},
|
|
82
|
+
frozenset(),
|
|
83
|
+
frozenset(),
|
|
84
|
+
),
|
|
85
|
+
"cable.termination_occupied": (
|
|
86
|
+
frozenset({"segment_index", "port"}),
|
|
87
|
+
frozenset({"cable"}),
|
|
88
|
+
frozenset(),
|
|
89
|
+
),
|
|
90
|
+
"cable.termination_unresolved": (
|
|
91
|
+
_REFERENCE_FIELDS | {"matches", "selected_display_name"},
|
|
92
|
+
frozenset(),
|
|
93
|
+
frozenset(),
|
|
94
|
+
),
|
|
95
|
+
"cable.unsupported_termination_kind": (
|
|
96
|
+
_REFERENCE_FIELDS | {"selected_object_type"},
|
|
97
|
+
frozenset(),
|
|
98
|
+
frozenset(),
|
|
99
|
+
),
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
CABLE_DIAGNOSTIC_FIELDS = MappingProxyType(
|
|
104
|
+
{code: frozenset().union(*fields) for code, fields in _DIAGNOSTIC_DISCLOSURE_FIELDS.items()}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
CABLE_DIAGNOSTIC_DISCLOSURES = MappingProxyType(
|
|
108
|
+
{code: fields[1] for code, fields in _DIAGNOSTIC_DISCLOSURE_FIELDS.items() if fields[1]}
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
POLICY_DIAGNOSTIC_DISCLOSURES = MappingProxyType(
|
|
112
|
+
{code: fields[2] for code, fields in _DIAGNOSTIC_DISCLOSURE_FIELDS.items() if fields[2]}
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def disclosure_source(row_kind: str, row_pk: int) -> dict:
|
|
117
|
+
"""Return the plan-side identity for one row that supplied display text."""
|
|
118
|
+
return {"kind": row_kind, "pk": row_pk}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def disclosed_cable(cable) -> dict:
|
|
122
|
+
"""Return the fields a planner may record for one visible Cable."""
|
|
123
|
+
return {
|
|
124
|
+
"cable_visible": True,
|
|
125
|
+
"cable": str(cable),
|
|
126
|
+
DISCLOSURE_SOURCE: disclosure_source(CABLE_ROW, cable.pk),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def policy_row_kind(row) -> str:
|
|
131
|
+
"""Return the disclosure kind for one supported Cable policy row."""
|
|
132
|
+
from .models import CableClassMapping, CableSegmentOverride
|
|
133
|
+
|
|
134
|
+
if isinstance(row, CableClassMapping):
|
|
135
|
+
return CABLE_CLASS_MAPPING_ROW
|
|
136
|
+
if isinstance(row, CableSegmentOverride):
|
|
137
|
+
return CABLE_SEGMENT_OVERRIDE_ROW
|
|
138
|
+
raise TypeError(f"Unsupported Cable policy row: {type(row).__name__}")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def disclosed_policy(row, visible: bool, display: dict) -> dict:
|
|
142
|
+
"""Return policy display values only when the planning viewer may read their row."""
|
|
143
|
+
if not visible:
|
|
144
|
+
return _redact_policy(display)
|
|
145
|
+
return {
|
|
146
|
+
**display,
|
|
147
|
+
POLICY_VISIBLE: True,
|
|
148
|
+
DISCLOSURE_SOURCE: disclosure_source(policy_row_kind(row), row.pk),
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def policy_row_is_disclosed(display: dict, row) -> bool:
|
|
153
|
+
"""Return whether a presented value names this policy row as its visible source."""
|
|
154
|
+
return (
|
|
155
|
+
display.get(POLICY_VISIBLE) is True
|
|
156
|
+
and _source_pk(display.get(DISCLOSURE_SOURCE), policy_row_kind(row)) == row.pk
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def validate_diagnostic_disclosures(code: str, display: dict) -> None:
|
|
161
|
+
"""Reject diagnostic display fields that bypass the shared disclosure vocabulary."""
|
|
162
|
+
if not code.startswith("cable."):
|
|
163
|
+
return
|
|
164
|
+
fields = _DIAGNOSTIC_DISCLOSURE_FIELDS.get(code)
|
|
165
|
+
if fields is None:
|
|
166
|
+
raise ValueError(f"Diagnostic '{code}' has no registered display schema.")
|
|
167
|
+
public, cable, policy = fields
|
|
168
|
+
if code == "cable.media_family_mismatch":
|
|
169
|
+
_validate_media_segments(display)
|
|
170
|
+
return
|
|
171
|
+
metadata = set()
|
|
172
|
+
if cable:
|
|
173
|
+
metadata.update({DISCLOSURE_SOURCE, "cable_visible"})
|
|
174
|
+
if policy:
|
|
175
|
+
metadata.update({DISCLOSURE_SOURCE, POLICY_VISIBLE})
|
|
176
|
+
unknown = set(display) - set(public) - set(cable) - set(policy) - metadata
|
|
177
|
+
if unknown:
|
|
178
|
+
names = ", ".join(sorted(unknown))
|
|
179
|
+
raise ValueError(f"Diagnostic '{code}' has unregistered display fields: {names}.")
|
|
180
|
+
source = display.get(DISCLOSURE_SOURCE)
|
|
181
|
+
if set(display) & set(cable) or "cable_visible" in display:
|
|
182
|
+
visible = display.get("cable_visible")
|
|
183
|
+
if not isinstance(visible, bool):
|
|
184
|
+
raise ValueError(f"Diagnostic '{code}' has Cable fields without a visibility flag.")
|
|
185
|
+
if visible and _source_pk(source, CABLE_ROW) is None:
|
|
186
|
+
raise ValueError(f"Diagnostic '{code}' has visible Cable fields without a Cable source.")
|
|
187
|
+
if not visible and source is not None:
|
|
188
|
+
raise ValueError(f"Diagnostic '{code}' has a source for a hidden Cable.")
|
|
189
|
+
if set(display) & set(policy) or POLICY_VISIBLE in display:
|
|
190
|
+
visible = display.get(POLICY_VISIBLE)
|
|
191
|
+
if not isinstance(visible, bool):
|
|
192
|
+
raise ValueError(f"Diagnostic '{code}' has policy fields without a visibility flag.")
|
|
193
|
+
row_kinds = (CABLE_CLASS_MAPPING_ROW, CABLE_SEGMENT_OVERRIDE_ROW)
|
|
194
|
+
if visible and not any(_source_pk(source, kind) is not None for kind in row_kinds):
|
|
195
|
+
raise ValueError(f"Diagnostic '{code}' has visible policy fields without a policy source.")
|
|
196
|
+
if not visible and source is not None:
|
|
197
|
+
raise ValueError(f"Diagnostic '{code}' has a source for a hidden policy.")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _validate_media_segments(display: dict) -> None:
|
|
201
|
+
"""Reject media observations that omit provenance or add unregistered nested fields."""
|
|
202
|
+
if set(display) != {"segments"} or not isinstance(display["segments"], list):
|
|
203
|
+
raise ValueError("Diagnostic 'cable.media_family_mismatch' has an invalid display schema.")
|
|
204
|
+
base = {"segment_index", "retained", "visible", "origin"}
|
|
205
|
+
disclosed = {"cable_type", "family", DISCLOSURE_SOURCE}
|
|
206
|
+
for segment in display["segments"]:
|
|
207
|
+
if not isinstance(segment, dict) or frozenset(segment) not in {frozenset(base), frozenset(base | disclosed)}:
|
|
208
|
+
raise ValueError("Diagnostic 'cable.media_family_mismatch' has invalid segment fields.")
|
|
209
|
+
origin = segment.get("origin")
|
|
210
|
+
if origin not in {"cable", "policy"} or not isinstance(segment.get("visible"), bool):
|
|
211
|
+
raise ValueError("Diagnostic 'cable.media_family_mismatch' has an invalid segment origin.")
|
|
212
|
+
if segment["visible"] is False:
|
|
213
|
+
if set(segment) != base:
|
|
214
|
+
raise ValueError("Diagnostic 'cable.media_family_mismatch' discloses a hidden segment.")
|
|
215
|
+
continue
|
|
216
|
+
row_kinds = (CABLE_ROW,) if origin == "cable" else (CABLE_CLASS_MAPPING_ROW, CABLE_SEGMENT_OVERRIDE_ROW)
|
|
217
|
+
if not any(_source_pk(segment.get(DISCLOSURE_SOURCE), kind) is not None for kind in row_kinds):
|
|
218
|
+
raise ValueError("Diagnostic 'cable.media_family_mismatch' has a visible segment without its source.")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _source_pk(value, row_kind: str) -> int | None:
|
|
222
|
+
if not isinstance(value, dict) or value.get("kind") != row_kind:
|
|
223
|
+
return None
|
|
224
|
+
row_pk = value.get("pk")
|
|
225
|
+
return row_pk if isinstance(row_pk, int) and not isinstance(row_pk, bool) else None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _source_is_visible(value, row_kinds: tuple[str, ...], visible_row_ids: dict[str, set[int]]) -> bool:
|
|
229
|
+
"""Return whether one well-formed source names a row visible in its expected kind."""
|
|
230
|
+
return any(
|
|
231
|
+
(row_pk := _source_pk(value, row_kind)) is not None and row_pk in visible_row_ids[row_kind]
|
|
232
|
+
for row_kind in row_kinds
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _row_ids(units) -> dict[str, set[int]]:
|
|
237
|
+
row_ids: dict[str, set[int]] = {
|
|
238
|
+
CABLE_ROW: set(),
|
|
239
|
+
CABLE_CLASS_MAPPING_ROW: set(),
|
|
240
|
+
CABLE_SEGMENT_OVERRIDE_ROW: set(),
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
def collect(value) -> None:
|
|
244
|
+
if isinstance(value, dict):
|
|
245
|
+
source = value.get(DISCLOSURE_SOURCE)
|
|
246
|
+
for row_kind, ids in row_ids.items():
|
|
247
|
+
if row_pk := _source_pk(source, row_kind):
|
|
248
|
+
ids.add(row_pk)
|
|
249
|
+
for child in value.values():
|
|
250
|
+
collect(child)
|
|
251
|
+
elif isinstance(value, (list, tuple)):
|
|
252
|
+
for child in value:
|
|
253
|
+
collect(child)
|
|
254
|
+
|
|
255
|
+
for unit in units:
|
|
256
|
+
collect(unit.display)
|
|
257
|
+
for diagnostic in unit.diagnostics:
|
|
258
|
+
collect(diagnostic.display)
|
|
259
|
+
return row_ids
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _redact_cable(display: dict, keys: frozenset[str]) -> dict:
|
|
263
|
+
for key in keys:
|
|
264
|
+
display.pop(key, None)
|
|
265
|
+
display.pop(DISCLOSURE_SOURCE, None)
|
|
266
|
+
display["cable_visible"] = False
|
|
267
|
+
return display
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _redact_policy(display: dict) -> dict:
|
|
271
|
+
display = dict(display)
|
|
272
|
+
for key in ("cable_type", "cable_profile"):
|
|
273
|
+
if key in display:
|
|
274
|
+
display[key] = POLICY_HIDDEN
|
|
275
|
+
if "policy" in display:
|
|
276
|
+
display["policy"] = {}
|
|
277
|
+
display.pop(DISCLOSURE_SOURCE, None)
|
|
278
|
+
display[POLICY_VISIBLE] = False
|
|
279
|
+
return display
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _media_segment(segment: dict, visible_row_ids: dict[str, set[int]]) -> dict:
|
|
283
|
+
source = segment.get(DISCLOSURE_SOURCE)
|
|
284
|
+
origin = segment.get("origin")
|
|
285
|
+
row_kinds = (
|
|
286
|
+
(CABLE_ROW,)
|
|
287
|
+
if origin == "cable"
|
|
288
|
+
else (CABLE_CLASS_MAPPING_ROW, CABLE_SEGMENT_OVERRIDE_ROW)
|
|
289
|
+
if origin == "policy"
|
|
290
|
+
else ()
|
|
291
|
+
)
|
|
292
|
+
if segment.get("visible") is True and _source_is_visible(source, row_kinds, visible_row_ids):
|
|
293
|
+
return segment
|
|
294
|
+
return {
|
|
295
|
+
"segment_index": segment["segment_index"],
|
|
296
|
+
"retained": segment["retained"],
|
|
297
|
+
"visible": False,
|
|
298
|
+
"origin": origin if origin in {"cable", "policy"} else "cable",
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _media_message(segments: list[dict]) -> str:
|
|
303
|
+
from .cable_policy import cable_media_family_label, cable_type_label
|
|
304
|
+
|
|
305
|
+
statements = []
|
|
306
|
+
for segment in segments:
|
|
307
|
+
position = segment["segment_index"] + 1
|
|
308
|
+
if not segment["visible"]:
|
|
309
|
+
hidden = POLICY_HIDDEN if segment.get("origin") == "policy" else "a Cable you cannot view"
|
|
310
|
+
statements.append(f"segment {position} uses {hidden}")
|
|
311
|
+
continue
|
|
312
|
+
family = cable_media_family_label(segment["family"])
|
|
313
|
+
statement = f"segment {position} is {cable_type_label(segment['cable_type'])} ({family})"
|
|
314
|
+
if segment["retained"]:
|
|
315
|
+
statement += ", on the Cable this import keeps"
|
|
316
|
+
statements.append(statement)
|
|
317
|
+
remedy = (
|
|
318
|
+
"Correct those Cables in NetBox, then re-read."
|
|
319
|
+
if all(segment["retained"] for segment in segments)
|
|
320
|
+
else "Force the segment that states the wrong medium, or correct the source."
|
|
321
|
+
)
|
|
322
|
+
return f"Verified pass-throughs join these segments, and {'; '.join(statements)}. {remedy}"
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _diagnostic(diagnostic, visible_row_ids: dict[str, set[int]]):
|
|
326
|
+
display = diagnostic.to_dict()["display"]
|
|
327
|
+
if diagnostic.code == "cable.media_family_mismatch":
|
|
328
|
+
segments = [_media_segment(segment, visible_row_ids) for segment in display["segments"]]
|
|
329
|
+
display["segments"] = segments
|
|
330
|
+
display["families"] = sorted(
|
|
331
|
+
{_family_label(segment["family"]) for segment in segments if segment["visible"] and segment.get("family")}
|
|
332
|
+
)
|
|
333
|
+
display["message"] = _media_message(segments)
|
|
334
|
+
elif keys := CABLE_DIAGNOSTIC_DISCLOSURES.get(diagnostic.code):
|
|
335
|
+
if display.get("cable_visible") is True and not _source_is_visible(
|
|
336
|
+
display.get(DISCLOSURE_SOURCE), (CABLE_ROW,), visible_row_ids
|
|
337
|
+
):
|
|
338
|
+
display = _redact_cable(display, keys)
|
|
339
|
+
if (
|
|
340
|
+
diagnostic.code in POLICY_DIAGNOSTIC_DISCLOSURES
|
|
341
|
+
and display.get(POLICY_VISIBLE) is True
|
|
342
|
+
and not _source_is_visible(
|
|
343
|
+
display.get(DISCLOSURE_SOURCE),
|
|
344
|
+
(CABLE_CLASS_MAPPING_ROW, CABLE_SEGMENT_OVERRIDE_ROW),
|
|
345
|
+
visible_row_ids,
|
|
346
|
+
)
|
|
347
|
+
):
|
|
348
|
+
display = _redact_policy(display)
|
|
349
|
+
return replace(diagnostic, display=display)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _family_label(family: str) -> str:
|
|
353
|
+
from .cable_policy import cable_media_family_label
|
|
354
|
+
|
|
355
|
+
return cable_media_family_label(family)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _unit(unit, visible_row_ids: dict[str, set[int]]):
|
|
359
|
+
display = unit.to_dict()["display"]
|
|
360
|
+
trace = display.get("trace")
|
|
361
|
+
if trace is not None:
|
|
362
|
+
logical = trace.get("logical_cable")
|
|
363
|
+
if (
|
|
364
|
+
logical is not None
|
|
365
|
+
and logical.get("visible") is True
|
|
366
|
+
and not _source_is_visible(logical.get(DISCLOSURE_SOURCE), (CABLE_ROW,), visible_row_ids)
|
|
367
|
+
):
|
|
368
|
+
trace["logical_cable"] = {"visible": False, "display": "", "description": "", "tags": []}
|
|
369
|
+
for segment in trace.get("segments") or ():
|
|
370
|
+
if segment.get(POLICY_VISIBLE) is True and not _source_is_visible(
|
|
371
|
+
segment.get(DISCLOSURE_SOURCE),
|
|
372
|
+
(CABLE_CLASS_MAPPING_ROW, CABLE_SEGMENT_OVERRIDE_ROW),
|
|
373
|
+
visible_row_ids,
|
|
374
|
+
):
|
|
375
|
+
segment.pop(DISCLOSURE_SOURCE, None)
|
|
376
|
+
segment.update(_redact_policy(segment))
|
|
377
|
+
for policy in trace.get("cable_policies") or ():
|
|
378
|
+
if policy.get(POLICY_VISIBLE) is True and not _source_is_visible(
|
|
379
|
+
policy.get(DISCLOSURE_SOURCE), (CABLE_CLASS_MAPPING_ROW,), visible_row_ids
|
|
380
|
+
):
|
|
381
|
+
policy.pop(DISCLOSURE_SOURCE, None)
|
|
382
|
+
policy.update(_redact_policy(policy))
|
|
383
|
+
diagnostics = tuple(_diagnostic(item, visible_row_ids) for item in unit.diagnostics)
|
|
384
|
+
return replace(unit, diagnostics=diagnostics, display=display)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def present_units(units, viewer) -> tuple:
|
|
388
|
+
"""Return presentation copies after one live Cable visibility query."""
|
|
389
|
+
if viewer is None:
|
|
390
|
+
raise TypeError("ReviewWorkspace requires a live viewer.")
|
|
391
|
+
units = tuple(units)
|
|
392
|
+
from dcim.models import Cable
|
|
393
|
+
|
|
394
|
+
from .models import CableClassMapping, CableSegmentOverride
|
|
395
|
+
|
|
396
|
+
row_ids = _row_ids(units)
|
|
397
|
+
models = {
|
|
398
|
+
CABLE_ROW: Cable,
|
|
399
|
+
CABLE_CLASS_MAPPING_ROW: CableClassMapping,
|
|
400
|
+
CABLE_SEGMENT_OVERRIDE_ROW: CableSegmentOverride,
|
|
401
|
+
}
|
|
402
|
+
visible = {}
|
|
403
|
+
for row_kind, model in models.items():
|
|
404
|
+
ids = row_ids[row_kind]
|
|
405
|
+
visible[row_kind] = (
|
|
406
|
+
set(model.objects.restrict(viewer, "view").filter(pk__in=sorted(ids)).values_list("pk", flat=True))
|
|
407
|
+
if ids
|
|
408
|
+
else set()
|
|
409
|
+
)
|
|
410
|
+
return tuple(_unit(unit, visible) for unit in units)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def redact_deleted_cables(plan_data: dict) -> dict:
|
|
414
|
+
"""Return an execution copy whose deleted Cable rows authorize no stored display text."""
|
|
415
|
+
from .plan import ImportPlan
|
|
416
|
+
|
|
417
|
+
plan = ImportPlan.from_dict(plan_data)
|
|
418
|
+
visible = _row_ids(plan.units)
|
|
419
|
+
deleted_ids = {
|
|
420
|
+
change.payload["cable_id"]
|
|
421
|
+
for unit in plan.units
|
|
422
|
+
for change in unit.changes
|
|
423
|
+
if change.target_module == "cable" and change.operation == "delete"
|
|
424
|
+
}
|
|
425
|
+
visible[CABLE_ROW].difference_update(deleted_ids)
|
|
426
|
+
units = tuple(_unit(unit, visible) for unit in plan.units)
|
|
427
|
+
return replace(plan, units=units).to_dict()
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
__all__ = (
|
|
431
|
+
"CABLE_CLASS_MAPPING_ROW",
|
|
432
|
+
"CABLE_DIAGNOSTIC_DISCLOSURES",
|
|
433
|
+
"CABLE_DIAGNOSTIC_FIELDS",
|
|
434
|
+
"CABLE_ROW",
|
|
435
|
+
"CABLE_SEGMENT_OVERRIDE_ROW",
|
|
436
|
+
"DISCLOSURE_SOURCE",
|
|
437
|
+
"POLICY_DIAGNOSTIC_DISCLOSURES",
|
|
438
|
+
"POLICY_HIDDEN",
|
|
439
|
+
"POLICY_VISIBLE",
|
|
440
|
+
"POLICY_WRITE_REFUSED",
|
|
441
|
+
"disclosed_cable",
|
|
442
|
+
"disclosed_policy",
|
|
443
|
+
"disclosure_source",
|
|
444
|
+
"policy_row_is_disclosed",
|
|
445
|
+
"present_units",
|
|
446
|
+
"redact_deleted_cables",
|
|
447
|
+
"validate_diagnostic_disclosures",
|
|
448
|
+
)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# SPDX-FileCopyrightText: 2026 Marcin Zieba <marcinpsk@gmail.com>
|
|
3
|
+
"""Own the Cable policy question: the runtime choices, and which stored decision is in force."""
|
|
4
|
+
|
|
5
|
+
from django.core.exceptions import ValidationError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _flatten_choice_groups(choices):
|
|
9
|
+
"""Return value and label pairs from flat or grouped NetBox choices."""
|
|
10
|
+
flattened = []
|
|
11
|
+
for value, label in choices:
|
|
12
|
+
if isinstance(label, (tuple, list)):
|
|
13
|
+
flattened.extend(label)
|
|
14
|
+
else:
|
|
15
|
+
flattened.append((value, label))
|
|
16
|
+
return tuple(flattened)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def cable_type_choices():
|
|
20
|
+
"""Return the Cable Type values offered by the running NetBox instance."""
|
|
21
|
+
from dcim.choices import CableTypeChoices
|
|
22
|
+
|
|
23
|
+
return _flatten_choice_groups(CableTypeChoices.CHOICES)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def cable_profile_choices():
|
|
27
|
+
"""Return the Cable Profile values offered by the running NetBox instance."""
|
|
28
|
+
from dcim.choices import CableProfileChoices
|
|
29
|
+
|
|
30
|
+
return _flatten_choice_groups(CableProfileChoices.CHOICES)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def cable_profile_accepts_one_termination_per_side(value) -> bool:
|
|
34
|
+
"""Return whether NetBox reports one connector on each side of the Cable Profile."""
|
|
35
|
+
from dcim.models import Cable
|
|
36
|
+
|
|
37
|
+
profile_class = Cable(profile=value).profile_class
|
|
38
|
+
return profile_class is not None and len(profile_class.a_connectors) == 1 and len(profile_class.b_connectors) == 1
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def compatible_cable_profile_choices():
|
|
42
|
+
"""Return running Cable Profiles that permit one termination on each side."""
|
|
43
|
+
return tuple(
|
|
44
|
+
(value, label)
|
|
45
|
+
for value, label in cable_profile_choices()
|
|
46
|
+
if cable_profile_accepts_one_termination_per_side(value)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def policy_choice_errors(cable_type, cable_profile):
|
|
51
|
+
"""Return runtime-choice and profile-cardinality errors by model field."""
|
|
52
|
+
errors = {}
|
|
53
|
+
type_values = {value for value, _label in cable_type_choices()}
|
|
54
|
+
profile_values = {value for value, _label in cable_profile_choices()}
|
|
55
|
+
if cable_type is not None and cable_type not in type_values:
|
|
56
|
+
errors["cable_type"] = ValidationError(
|
|
57
|
+
"The selected Cable Type is no longer offered by this NetBox instance.",
|
|
58
|
+
code="cable.policy_stale",
|
|
59
|
+
)
|
|
60
|
+
if cable_profile is not None and cable_profile not in profile_values:
|
|
61
|
+
errors["cable_profile"] = ValidationError(
|
|
62
|
+
"The selected Cable Profile is no longer offered by this NetBox instance.",
|
|
63
|
+
code="cable.policy_stale",
|
|
64
|
+
)
|
|
65
|
+
elif cable_profile is not None and not cable_profile_accepts_one_termination_per_side(cable_profile):
|
|
66
|
+
errors["cable_profile"] = ValidationError(
|
|
67
|
+
"The selected Cable Profile does not permit one termination on each side.",
|
|
68
|
+
code="cable.profile_incompatible",
|
|
69
|
+
)
|
|
70
|
+
return errors
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# An active optical assembly states no terminated medium, so its whole group decides nothing.
|
|
74
|
+
INDETERMINATE_MEDIA_TYPE = "aoc"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _media_groups():
|
|
78
|
+
"""Return the Cable Type values of each group the running instance offers, with its label."""
|
|
79
|
+
from dcim.choices import CableTypeChoices
|
|
80
|
+
|
|
81
|
+
return [
|
|
82
|
+
(label, [item_value for item_value, _item_label in group])
|
|
83
|
+
for label, group in CableTypeChoices.CHOICES
|
|
84
|
+
if isinstance(group, (tuple, list))
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def cable_media_families():
|
|
89
|
+
"""Return the family each Cable Type belongs to, keyed by a value rather than by a group label.
|
|
90
|
+
|
|
91
|
+
A group label is a lazy translation, so keying on it would make classification depend on the
|
|
92
|
+
reader's language and would put translated text in a fingerprint (spec section 4.3).
|
|
93
|
+
"""
|
|
94
|
+
families = {}
|
|
95
|
+
for _label, values in _media_groups():
|
|
96
|
+
families.update(dict.fromkeys(values, min(values)))
|
|
97
|
+
return families
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def cable_media_family_label(family) -> str:
|
|
101
|
+
"""Return the operator-facing name of one media family, in the reader's own language."""
|
|
102
|
+
for label, values in _media_groups():
|
|
103
|
+
if min(values) == family:
|
|
104
|
+
return str(label)
|
|
105
|
+
return family
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def decisive_media_family(cable_type) -> str:
|
|
109
|
+
"""Return the family that decides one Cable Type's medium, or "" when nothing decides it.
|
|
110
|
+
|
|
111
|
+
A value the instance does not group, and one it groups as indeterminate, are incomplete coverage.
|
|
112
|
+
They never read as agreement with another segment and never contradict one.
|
|
113
|
+
"""
|
|
114
|
+
families = cable_media_families()
|
|
115
|
+
family = families.get(cable_type or "", "")
|
|
116
|
+
return "" if family and family == families.get(INDETERMINATE_MEDIA_TYPE, "") else family
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def cable_profile_splits_a_span(cable_profile) -> bool:
|
|
120
|
+
"""Return whether a Cable with this profile cannot be read as one medium along one path.
|
|
121
|
+
|
|
122
|
+
A profile with several connectors on a side, a breakout or a trunk, fans one Cable out into runs
|
|
123
|
+
the path does not state, so its type says nothing about the medium of the segment beside it.
|
|
124
|
+
"""
|
|
125
|
+
return bool(cable_profile) and not cable_profile_accepts_one_termination_per_side(cable_profile)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def policy_in_force(override, mapping):
|
|
129
|
+
"""Return the stored decision that governs one segment: its override, else the CableClass row."""
|
|
130
|
+
return mapping if override is None else override
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def cable_type_label(value) -> str:
|
|
134
|
+
"""Return the operator-facing name of one stored Cable Type value."""
|
|
135
|
+
return "None" if value is None else str(dict(cable_type_choices()).get(value, value))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def cable_profile_label(value) -> str:
|
|
139
|
+
"""Return the operator-facing name of one stored Cable Profile value."""
|
|
140
|
+
return "None" if value is None else str(dict(cable_profile_choices()).get(value, value))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
__all__ = (
|
|
144
|
+
"INDETERMINATE_MEDIA_TYPE",
|
|
145
|
+
"cable_media_families",
|
|
146
|
+
"cable_media_family_label",
|
|
147
|
+
"cable_profile_accepts_one_termination_per_side",
|
|
148
|
+
"cable_profile_choices",
|
|
149
|
+
"cable_profile_label",
|
|
150
|
+
"cable_profile_splits_a_span",
|
|
151
|
+
"cable_type_choices",
|
|
152
|
+
"cable_type_label",
|
|
153
|
+
"compatible_cable_profile_choices",
|
|
154
|
+
"decisive_media_family",
|
|
155
|
+
"policy_choice_errors",
|
|
156
|
+
"policy_in_force",
|
|
157
|
+
)
|