ipfabric_netbox 4.1.0b4__tar.gz → 4.1.0b6__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.

Potentially problematic release.


This version of ipfabric_netbox might be problematic. Click here for more details.

Files changed (78) hide show
  1. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/PKG-INFO +1 -1
  2. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/__init__.py +1 -1
  3. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/api/nested_serializers.py +0 -1
  4. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/api/serializers.py +23 -1
  5. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/api/urls.py +2 -0
  6. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/api/views.py +7 -0
  7. ipfabric_netbox-4.1.0b6/ipfabric_netbox/exceptions.py +81 -0
  8. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/filtersets.py +30 -0
  9. ipfabric_netbox-4.1.0b6/ipfabric_netbox/migrations/0015_ipfabricingestionissue.py +48 -0
  10. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/models.py +25 -0
  11. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/tables.py +23 -0
  12. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricingestion.html +18 -47
  13. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_all.html +6 -1
  14. ipfabric_netbox-4.1.0b6/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_statistics.html +37 -0
  15. ipfabric_netbox-4.1.0b6/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_status.html +1 -0
  16. ipfabric_netbox-4.1.0b6/ipfabric_netbox/templates/ipfabric_netbox/partials/object_tabs.html +12 -0
  17. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/utilities/ipfutils.py +64 -28
  18. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/views.py +55 -30
  19. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/pyproject.toml +1 -1
  20. ipfabric_netbox-4.1.0b4/ipfabric_netbox/exceptions.py +0 -24
  21. ipfabric_netbox-4.1.0b4/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_status.html +0 -1
  22. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/README.md +0 -0
  23. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/api/__init__.py +0 -0
  24. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/choices.py +0 -0
  25. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/data/transform_map.json +0 -0
  26. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/forms.py +0 -0
  27. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/jobs.py +0 -0
  28. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/migrations/0001_initial.py +0 -0
  29. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/migrations/0001_initial_squashed_0013_switch_to_branching_plugin.py +0 -0
  30. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/migrations/0002_ipfabricsnapshot_status.py +0 -0
  31. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/migrations/0003_ipfabricsource_type_and_more.py +0 -0
  32. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/migrations/0004_ipfabricsync_auto_merge.py +0 -0
  33. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/migrations/0005_alter_ipfabricrelationshipfield_source_model_and_more.py +0 -0
  34. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/migrations/0006_alter_ipfabrictransformmap_target_model.py +0 -0
  35. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/migrations/0007_prepare_custom_fields.py +0 -0
  36. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/migrations/0008_prepare_transform_maps.py +0 -0
  37. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/migrations/0009_transformmap_changes_for_netbox_v4_2.py +0 -0
  38. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/migrations/0010_remove_uuid_from_get_or_create.py +0 -0
  39. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/migrations/0011_update_part_number_DCIM_inventory_item_template.py +0 -0
  40. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/migrations/0012_remove_status_field.py +0 -0
  41. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/migrations/0013_switch_to_branching_plugin.py +0 -0
  42. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/migrations/0014_ipfabrictransformmapgroup_ipfabrictransformmap_group.py +0 -0
  43. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/migrations/__init__.py +0 -0
  44. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/navigation.py +0 -0
  45. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/signals.py +0 -0
  46. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/template_content.py +0 -0
  47. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/templates/ipfabric_netbox/inc/clone_form.html +0 -0
  48. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/templates/ipfabric_netbox/inc/diff.html +0 -0
  49. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/templates/ipfabric_netbox/inc/json.html +0 -0
  50. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/templates/ipfabric_netbox/inc/logs_pending.html +0 -0
  51. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/templates/ipfabric_netbox/inc/merge_form.html +0 -0
  52. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/templates/ipfabric_netbox/inc/site_topology_button.html +0 -0
  53. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/templates/ipfabric_netbox/inc/site_topology_modal.html +0 -0
  54. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/templates/ipfabric_netbox/inc/snapshotdata.html +0 -0
  55. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/templates/ipfabric_netbox/inc/transform_map_field_map.html +0 -0
  56. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/templates/ipfabric_netbox/inc/transform_map_relationship_map.html +0 -0
  57. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/templates/ipfabric_netbox/ipfabric_table.html +0 -0
  58. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsnapshot.html +0 -0
  59. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsource.html +0 -0
  60. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync.html +0 -0
  61. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/templates/ipfabric_netbox/ipfabricsync_list.html +0 -0
  62. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap.html +0 -0
  63. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap_list.html +0 -0
  64. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmap_restore.html +0 -0
  65. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/templates/ipfabric_netbox/ipfabrictransformmapgroup.html +0 -0
  66. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/templates/ipfabric_netbox/partials/ingestion_progress.html +0 -0
  67. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/templates/ipfabric_netbox/partials/job_logs.html +0 -0
  68. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/templates/ipfabric_netbox/partials/sync_last_ingestion.html +0 -0
  69. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/templates/static/ipfabric_netbox/css/rack.css +0 -0
  70. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/templatetags/__init__.py +0 -0
  71. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/templatetags/ipfabric_netbox_helpers.py +0 -0
  72. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/tests/__init__.py +0 -0
  73. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/tests/test_models.py +0 -0
  74. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/urls.py +0 -0
  75. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/utilities/__init__.py +0 -0
  76. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/utilities/logging.py +0 -0
  77. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/utilities/nbutils.py +0 -0
  78. {ipfabric_netbox-4.1.0b4 → ipfabric_netbox-4.1.0b6}/ipfabric_netbox/utilities/transform_map.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ipfabric_netbox
3
- Version: 4.1.0b4
3
+ Version: 4.1.0b6
4
4
  Summary: NetBox plugin to sync IP Fabric data into NetBox
5
5
  License: MIT
6
6
  Keywords: netbox,ipfabric,plugin,sync
@@ -6,7 +6,7 @@ class NetboxIPFabricConfig(PluginConfig):
6
6
  name = "ipfabric_netbox"
7
7
  verbose_name = "NetBox IP Fabric SoT Plugin"
8
8
  description = "Sync IP Fabric into NetBox"
9
- version = "4.1.0b4"
9
+ version = "4.1.0b6"
10
10
  base_url = "ipfabric"
11
11
  min_version = "4.2.4"
12
12
 
@@ -10,7 +10,6 @@ from ipfabric_netbox.models import IPFabricSource
10
10
  from ipfabric_netbox.models import IPFabricSync
11
11
  from ipfabric_netbox.models import IPFabricTransformMap
12
12
 
13
- # from .serializers import IPFabricSyncSerializer
14
13
 
15
14
  __all__ = (
16
15
  "NestedIPFabricSourceSerializer",
@@ -7,8 +7,10 @@ from rest_framework import serializers
7
7
 
8
8
  from .nested_serializers import NestedIPFabricSnapshotSerializer
9
9
  from .nested_serializers import NestedIPFabricSourceSerializer
10
+ from .nested_serializers import NestedIPFabricSyncSerializer
10
11
  from .nested_serializers import NestedIPFabricTransformMapSerializer
11
12
  from ipfabric_netbox.models import IPFabricIngestion
13
+ from ipfabric_netbox.models import IPFabricIngestionIssue
12
14
  from ipfabric_netbox.models import IPFabricRelationshipField
13
15
  from ipfabric_netbox.models import IPFabricSnapshot
14
16
  from ipfabric_netbox.models import IPFabricSource
@@ -23,7 +25,9 @@ __all__ = (
23
25
  "IPFabricRelationshipFieldSerializer",
24
26
  "IPFabricTransformFieldSerializer",
25
27
  "IPFabricTransformMapSerializer",
28
+ "IPFabricTransformMapGroupSerializer",
26
29
  "IPFabricIngestionSerializer",
30
+ "IPFabricIngestionIssueSerializer",
27
31
  "IPFabricSourceSerializer",
28
32
  )
29
33
 
@@ -133,7 +137,7 @@ class IPFabricTransformFieldSerializer(NetBoxModelSerializer):
133
137
 
134
138
  class IPFabricIngestionSerializer(NetBoxModelSerializer):
135
139
  branch = BranchSerializer(read_only=True)
136
- sync = IPFabricSyncSerializer(read_only=True)
140
+ sync = NestedIPFabricSyncSerializer(read_only=True)
137
141
 
138
142
  class Meta:
139
143
  model = IPFabricIngestion
@@ -145,6 +149,24 @@ class IPFabricIngestionSerializer(NetBoxModelSerializer):
145
149
  ]
146
150
 
147
151
 
152
+ class IPFabricIngestionIssueSerializer(NetBoxModelSerializer):
153
+ ingestion = IPFabricIngestionSerializer(read_only=True)
154
+
155
+ class Meta:
156
+ model = IPFabricIngestionIssue
157
+ fields = [
158
+ "id",
159
+ "ingestion",
160
+ "timestamp",
161
+ "model",
162
+ "message",
163
+ "raw_data",
164
+ "coalesce_fields",
165
+ "defaults",
166
+ "exception",
167
+ ]
168
+
169
+
148
170
  class IPFabricSourceSerializer(NetBoxModelSerializer):
149
171
  status = ChoiceField(choices=DataSourceStatusChoices)
150
172
  url = serializers.URLField()
@@ -1,6 +1,7 @@
1
1
  # api/urls.py
2
2
  from netbox.api.routers import NetBoxRouter
3
3
 
4
+ from ipfabric_netbox.api.views import IPFabricIngestionIssueViewSet
4
5
  from ipfabric_netbox.api.views import IPFabricIngestionViewSet
5
6
  from ipfabric_netbox.api.views import IPFabricRelationshipFieldiewSet
6
7
  from ipfabric_netbox.api.views import IPFabricSnapshotViewSet
@@ -18,6 +19,7 @@ router.register("transform-map-group", IPFabricTransformMapGroupViewSet)
18
19
  router.register("transform-map", IPFabricTransformMapViewSet)
19
20
  router.register("sync", IPFabricSyncViewSet)
20
21
  router.register("ingestion", IPFabricIngestionViewSet)
22
+ router.register("ingestion-issues", IPFabricIngestionIssueViewSet)
21
23
  router.register("transform-field", IPFabricTransformFieldiewSet)
22
24
  router.register("relationship-field", IPFabricRelationshipFieldiewSet)
23
25
  urlpatterns = router.urls
@@ -5,6 +5,7 @@ from rest_framework.decorators import action
5
5
  from rest_framework.response import Response
6
6
  from utilities.query import count_related
7
7
 
8
+ from .serializers import IPFabricIngestionIssueSerializer
8
9
  from .serializers import IPFabricIngestionSerializer
9
10
  from .serializers import IPFabricRelationshipFieldSerializer
10
11
  from .serializers import IPFabricSnapshotSerializer
@@ -18,6 +19,7 @@ from ipfabric_netbox.filtersets import IPFabricSourceFilterSet
18
19
  from ipfabric_netbox.filtersets import IPFabricTransformFieldFilterSet
19
20
  from ipfabric_netbox.models import IPFabricData
20
21
  from ipfabric_netbox.models import IPFabricIngestion
22
+ from ipfabric_netbox.models import IPFabricIngestionIssue
21
23
  from ipfabric_netbox.models import IPFabricRelationshipField
22
24
  from ipfabric_netbox.models import IPFabricSnapshot
23
25
  from ipfabric_netbox.models import IPFabricSource
@@ -59,6 +61,11 @@ class IPFabricIngestionViewSet(NetBoxReadOnlyModelViewSet):
59
61
  serializer_class = IPFabricIngestionSerializer
60
62
 
61
63
 
64
+ class IPFabricIngestionIssueViewSet(NetBoxReadOnlyModelViewSet):
65
+ queryset = IPFabricIngestionIssue.objects.all()
66
+ serializer_class = IPFabricIngestionIssueSerializer
67
+
68
+
62
69
  class IPFabricSnapshotViewSet(NetBoxModelViewSet):
63
70
  queryset = IPFabricSnapshot.objects.all()
64
71
  serializer_class = IPFabricSnapshotSerializer
@@ -0,0 +1,81 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from core.exceptions import SyncError
4
+
5
+ if TYPE_CHECKING:
6
+ from .models import IPFabricIngestionIssue
7
+ from .models import IPFabricIngestion
8
+
9
+
10
+ class IngestionIssue(Exception):
11
+ """
12
+ This exception is used to indicate an issue during the ingestion process.
13
+ """
14
+
15
+ # Store created issue object ID if it exists for this exception
16
+ issue_id = None
17
+ model: str = ""
18
+ defaults: dict[str, str] = {}
19
+ coalesce_fields: dict[str, str] = {}
20
+
21
+ def __init__(self, model: str, data: dict, context: dict = None, issue_id=None):
22
+ super().__init__()
23
+ self.model = model
24
+ self.data = data
25
+ context = context or {}
26
+ self.defaults = context.pop("defaults", {})
27
+ self.coalesce_fields = context
28
+ self.issue_id = issue_id
29
+
30
+
31
+ class SearchError(IngestionIssue, LookupError):
32
+ def __init__(self, message: str, *args, **kwargs):
33
+ super().__init__(*args, **kwargs)
34
+ self.message = message
35
+
36
+ def __str__(self):
37
+ return self.message
38
+
39
+
40
+ class SyncDataError(IngestionIssue, SyncError):
41
+ def __str__(self):
42
+ return f"Sync failed for {self.model}: coalesce_fields={self.coalesce_fields} defaults={self.defaults}."
43
+
44
+
45
+ class IPAddressDuplicateError(IngestionIssue, SyncError):
46
+ def __str__(self):
47
+ return f"IP address {self.data.get('address')} already exists in {self.model} with coalesce_fields={self.coalesce_fields}."
48
+
49
+
50
+ def create_or_get_sync_issue(
51
+ exception: Exception,
52
+ ingestion: "IPFabricIngestion",
53
+ message: str = None,
54
+ model: str = None,
55
+ context: dict = None,
56
+ data: dict = None,
57
+ ) -> (bool, "IPFabricIngestionIssue"):
58
+ """
59
+ Helper function to handle sync errors and create IPFabricIngestionIssue if needed.
60
+ """
61
+ context = context or {}
62
+
63
+ # TODO: This is to prevent circular import issues, clean it up later.
64
+ from .models import IPFabricIngestionIssue
65
+
66
+ if not hasattr(exception, "issue_id") or not exception.issue_id:
67
+ issue = IPFabricIngestionIssue.objects.create(
68
+ ingestion=ingestion,
69
+ exception=exception.__class__.__name__,
70
+ message=message or getattr(exception, "message", str(exception)),
71
+ model=model,
72
+ coalesce_fields={k: v for k, v in context.items() if k not in ["defaults"]},
73
+ defaults=context.get("defaults", dict()),
74
+ raw_data=data or dict(),
75
+ )
76
+ if hasattr(exception, "issue_id"):
77
+ exception.issue_id = issue.id
78
+ return True, issue
79
+ else:
80
+ issue = IPFabricIngestionIssue.objects.get(id=exception.issue_id)
81
+ return False, issue
@@ -10,6 +10,7 @@ from netbox_branching.models import ChangeDiff
10
10
 
11
11
  from .models import IPFabricData
12
12
  from .models import IPFabricIngestion
13
+ from .models import IPFabricIngestionIssue
13
14
  from .models import IPFabricSnapshot
14
15
  from .models import IPFabricSource
15
16
  from .models import IPFabricSync
@@ -37,6 +38,35 @@ class IPFabricIngestionChangeFilterSet(BaseFilterSet):
37
38
  )
38
39
 
39
40
 
41
+ class IPFabricIngestionIssueFilterSet(BaseFilterSet):
42
+ q = django_filters.CharFilter(method="search")
43
+
44
+ class Meta:
45
+ model = IPFabricIngestionIssue
46
+ fields = [
47
+ "model",
48
+ "timestamp",
49
+ "raw_data",
50
+ "coalesce_fields",
51
+ "defaults",
52
+ "exception",
53
+ "message",
54
+ ]
55
+
56
+ def search(self, queryset, name, value):
57
+ if not value.strip():
58
+ return queryset
59
+ return queryset.filter(
60
+ Q(model__icontains=value)
61
+ | Q(timestamp__icontains=value)
62
+ | Q(raw_data__icontains=value)
63
+ | Q(coalesce_fields__icontains=value)
64
+ | Q(defaults__icontains=value)
65
+ | Q(exception__icontains=value)
66
+ | Q(message__icontains=value)
67
+ )
68
+
69
+
40
70
  class IPFabricDataFilterSet(BaseFilterSet):
41
71
  q = django_filters.CharFilter(method="search")
42
72
 
@@ -0,0 +1,48 @@
1
+ # Generated by Django 5.2 on 2025-07-11 19:39
2
+ import django.db.models.deletion
3
+ import django.utils.timezone
4
+ from django.db import migrations
5
+ from django.db import models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+ dependencies = [
10
+ (
11
+ "ipfabric_netbox",
12
+ "0014_ipfabrictransformmapgroup_ipfabrictransformmap_group",
13
+ ),
14
+ ]
15
+
16
+ operations = [
17
+ migrations.CreateModel(
18
+ name="IPFabricIngestionIssue",
19
+ fields=[
20
+ (
21
+ "id",
22
+ models.BigAutoField(
23
+ auto_created=True, primary_key=True, serialize=False
24
+ ),
25
+ ),
26
+ ("timestamp", models.DateTimeField(default=django.utils.timezone.now)),
27
+ ("model", models.CharField(blank=True, max_length=100, null=True)),
28
+ ("message", models.TextField()),
29
+ ("raw_data", models.TextField(blank=True, default="")),
30
+ ("coalesce_fields", models.TextField(blank=True, default="")),
31
+ ("defaults", models.TextField(blank=True, default="")),
32
+ ("exception", models.TextField()),
33
+ (
34
+ "ingestion",
35
+ models.ForeignKey(
36
+ on_delete=django.db.models.deletion.CASCADE,
37
+ related_name="issues",
38
+ to="ipfabric_netbox.ipfabricingestion",
39
+ ),
40
+ ),
41
+ ],
42
+ options={
43
+ "verbose_name": "IP Fabric Ingestion Issue",
44
+ "verbose_name_plural": "IP Fabric Ingestion Issues",
45
+ "ordering": ["timestamp"],
46
+ },
47
+ ),
48
+ ]
@@ -236,6 +236,8 @@ class IPFabricTransformMap(NetBoxModel):
236
236
  target_class = self.target_model.model_class()
237
237
  queryset = target_class.objects.using(connection_name)
238
238
 
239
+ # Don't change context since it's used in case of exception for IPFabricIngestionIssue
240
+ context = deepcopy(context)
239
241
  defaults = context.pop("defaults", {})
240
242
 
241
243
  with transaction.atomic(using=connection_name):
@@ -967,6 +969,29 @@ class IPFabricIngestion(JobsMixin, models.Model):
967
969
  )
968
970
 
969
971
 
972
+ class IPFabricIngestionIssue(models.Model):
973
+ objects = RestrictedQuerySet.as_manager()
974
+
975
+ ingestion = models.ForeignKey(
976
+ to="IPFabricIngestion", on_delete=models.CASCADE, related_name="issues"
977
+ )
978
+ timestamp = models.DateTimeField(default=timezone.now)
979
+ model = models.CharField(max_length=100, blank=True, null=True)
980
+ message = models.TextField()
981
+ raw_data = models.TextField(blank=True, default="")
982
+ coalesce_fields = models.TextField(blank=True, default="")
983
+ defaults = models.TextField(blank=True, default="")
984
+ exception = models.TextField()
985
+
986
+ class Meta:
987
+ ordering = ["timestamp"]
988
+ verbose_name = "IP Fabric Ingestion Issue"
989
+ verbose_name_plural = "IP Fabric Ingestion Issues"
990
+
991
+ def __str__(self):
992
+ return f"[{self.timestamp}] {self.message}"
993
+
994
+
970
995
  class IPFabricData(models.Model):
971
996
  snapshot_data = models.ForeignKey(
972
997
  to=IPFabricSnapshot,
@@ -8,6 +8,7 @@ from netbox_branching.models import ChangeDiff
8
8
 
9
9
  from .models import IPFabricData
10
10
  from .models import IPFabricIngestion
11
+ from .models import IPFabricIngestionIssue
11
12
  from .models import IPFabricRelationshipField
12
13
  from .models import IPFabricSnapshot
13
14
  from .models import IPFabricSource
@@ -199,6 +200,28 @@ class IPFabricIngestionChangesTable(NetBoxTable):
199
200
  default_columns = ("object", "action", "object_type", "actions")
200
201
 
201
202
 
203
+ class IPFabricIngestionIssuesTable(NetBoxTable):
204
+ id = tables.Column(verbose_name=_("ID"))
205
+ exception = tables.Column(verbose_name="Exception Type")
206
+ message = tables.Column(verbose_name="Error Message")
207
+ actions = None
208
+
209
+ class Meta(NetBoxTable.Meta):
210
+ model = IPFabricIngestionIssue
211
+ fields = (
212
+ "model",
213
+ "timestamp",
214
+ "raw_data",
215
+ "coalesce_fields",
216
+ "defaults",
217
+ "exception",
218
+ "message",
219
+ )
220
+ default_columns = ("model", "exception", "message")
221
+ empty_text = _("No Ingestion Issues found")
222
+ order_by = "id"
223
+
224
+
202
225
  class DeviceIPFTable(tables.Table):
203
226
  hostname = Column()
204
227
 
@@ -36,6 +36,11 @@
36
36
  </div>
37
37
  {% endblock control-buttons %}
38
38
 
39
+ {% block tabs %}
40
+ <div id="object_tabs" hx-swap-oob="true">
41
+ {% include 'ipfabric_netbox/partials/object_tabs.html' with object=object %}
42
+ </div>
43
+ {% endblock tabs %}
39
44
 
40
45
  {% block content %}
41
46
  <div class="row mb-3">
@@ -70,7 +75,6 @@
70
75
  <th scope="row">Ingestion Status</th>
71
76
  <td><div id="ingestion_status" hx-swap-oob="true">{% include 'ipfabric_netbox/partials/ingestion_status.html' with object=object %}</div></td>
72
77
  <!-- <td >{% badge object.sync.get_status_display bg_color=object.sync.get_status_color %}</td> -->
73
- </div>
74
78
  </tr>
75
79
  <tr>
76
80
  <th scope="row">Snapshot</th>
@@ -88,59 +92,26 @@
88
92
  {% plugin_left_page object %}
89
93
  </div>
90
94
  <div class="col col-md-6">
91
- <div class="card">
92
- <h5 class="card-header">Statistics</h5>
93
- <div class="card-body">
94
- <table class="table table-hover attr-table">
95
- <tr>
96
- <th scope="row">Created</th>
97
- <td>
98
- {% if object.num_created %}
99
- {{ object.num_created }}
100
- {% else %}
101
- {{ ''|placeholder }}
102
- {% endif %}
103
- </td>
104
- </tr>
105
- <tr>
106
- <th scope="row">Updated</th>
107
- <td>
108
- {% if object.num_updated %}
109
- {{ object.num_updated }}
110
- {% else %}
111
- {{ ''|placeholder }}
112
- {% endif %}
113
- </td>
114
- </tr>
115
- <tr>
116
- <th scope="row">Deleted</th>
117
- <td>
118
- {% if object.num_deleted %}
119
- {{ object.num_deleted }}
120
- {% else %}
121
- {{ ''|placeholder }}
122
- {% endif %}
123
- </td>
124
- </tr>
125
- </table>
126
- </div>
95
+ <div id="ingestion_statistics" hx-swap-oob="true">
96
+ {% include 'ipfabric_netbox/partials/ingestion_statistics.html' with object=object %}
127
97
  </div>
128
- <div id="ingestion_progress">
98
+ <div id="ingestion_progress" hx-swap-oob="true">
129
99
  {% include 'ipfabric_netbox/partials/ingestion_progress.html' %}
130
100
  </div>
131
101
  <!-- {% include 'inc/panels/related_objects.html' %} -->
132
102
  {% include 'inc/panels/custom_fields.html' %}
133
103
  {% plugin_right_page object %}
134
104
  </div>
135
- </div>
136
- <div class="row mb-3">
137
- <div class="col col-md-12" {% if not object.job.completed %} hx-get="{% url 'plugins:ipfabric_netbox:ipfabricingestion_logs' pk=object.pk %}?type=info"
138
- hx-trigger="every 5s" hx-target="#ingestion_logs" hx-select="#ingestion_logs" hx-select-oob="#ingestion_status:innerHTML"
139
- hx-swap="innerHTML" {% endif %}>
140
- <div id="ingestion_logs">{% include 'ipfabric_netbox/partials/job_logs.html' with job=object.job %}</div>
105
+ <div class="row mb-3">
106
+ <div class="col col-md-12" {% if not object.job.completed %} hx-get="{% url 'plugins:ipfabric_netbox:ipfabricingestion_logs' pk=object.pk %}?type=info"
107
+ hx-trigger="every 5s" hx-target="#ingestion_logs" hx-select="#ingestion_logs" hx-select-oob="#ingestion_status:innerHTML,#ingestion_progress:innerHTML,#ingestion_statistics:innerHTML,#object_tabs:innerHTML"
108
+ hx-swap="innerHTML" {% endif %}>
109
+ <div id="ingestion_logs">{% include 'ipfabric_netbox/partials/job_logs.html' with job=object.job %}</div>
110
+ </div>
141
111
  </div>
112
+ </div>
142
113
  {% endblock content %}
143
114
 
144
- {% block modals %}
145
- {% include 'inc/htmx_modal.html' with size='lg' %}
146
- {% endblock modals %}
115
+ {% block modals %}
116
+ {% include 'inc/htmx_modal.html' with size='lg' %}
117
+ {% endblock modals %}
@@ -1,4 +1,9 @@
1
-
1
+ <div id="object_tabs" hx-swap-oob="true">
2
+ {% include 'ipfabric_netbox/partials/object_tabs.html' with object=object %}
3
+ </div>
4
+ <div id="ingestion_statistics" hx-swap-oob="true">
5
+ {% include 'ipfabric_netbox/partials/ingestion_statistics.html' with object=object %}
6
+ </div>
2
7
  <div id="ingestion_status" hx-swap-oob="true">
3
8
  {% include 'ipfabric_netbox/partials/ingestion_status.html' with object=object %}
4
9
  </div>
@@ -0,0 +1,37 @@
1
+ <div class="card">
2
+ <h5 class="card-header">Statistics</h5>
3
+ <div class="card-body">
4
+ <table class="table table-hover attr-table">
5
+ <tr>
6
+ <th scope="row">Created</th>
7
+ <td>
8
+ {% if object.num_created %}
9
+ {{ object.num_created }}
10
+ {% else %}
11
+ {{ ''|placeholder }}
12
+ {% endif %}
13
+ </td>
14
+ </tr>
15
+ <tr>
16
+ <th scope="row">Updated</th>
17
+ <td>
18
+ {% if object.num_updated %}
19
+ {{ object.num_updated }}
20
+ {% else %}
21
+ {{ ''|placeholder }}
22
+ {% endif %}
23
+ </td>
24
+ </tr>
25
+ <tr>
26
+ <th scope="row">Deleted</th>
27
+ <td>
28
+ {% if object.num_deleted %}
29
+ {{ object.num_deleted }}
30
+ {% else %}
31
+ {{ ''|placeholder }}
32
+ {% endif %}
33
+ </td>
34
+ </tr>
35
+ </table>
36
+ </div>
37
+ </div>
@@ -0,0 +1 @@
1
+ {% badge object.sync.get_status_display bg_color=object.sync.get_status_color %} {% if object.errors and object.errors.count %}(with {{ object.errors.count }} issues){% endif %}
@@ -0,0 +1,12 @@
1
+ {% load helpers %}
2
+ {% load tabs %}
3
+
4
+ <ul class="nav nav-tabs" role="presentation">
5
+ {# Primary tab #}
6
+ <li class="nav-item">
7
+ <a class="nav-link{% if not tab %} active{% endif %}" href="{{ object.get_absolute_url }}">{{ object|meta:"verbose_name"|bettertitle }}</a>
8
+ </li>
9
+
10
+ {# Include tabs for registered model views #}
11
+ {% model_view_tabs object %}
12
+ </ul>
@@ -22,19 +22,14 @@ from netbox.config import get_config
22
22
  from netutils.utils import jinja2_convenience_function
23
23
 
24
24
  from ..choices import IPFabricSourceTypeChoices
25
+ from ..exceptions import create_or_get_sync_issue
26
+ from ..exceptions import IPAddressDuplicateError
25
27
  from ..exceptions import SearchError
26
28
  from ..exceptions import SyncDataError
27
29
  from .nbutils import device_serial_max_length
28
30
  from .nbutils import order_devices
29
31
  from .nbutils import order_members
30
32
 
31
- FAILED_CREATE_INSTANCE_TEMPLATE = (
32
- "Failed to create instance of '{model}': <br/>"
33
- "message: '{err}'<br/>"
34
- "raw data: '{data}'<br/>"
35
- "context: '{context}'<br/>"
36
- )
37
-
38
33
  if TYPE_CHECKING:
39
34
  from ..models import IPFabricIngestion
40
35
  from ipam.models import IPAddress
@@ -163,6 +158,9 @@ class IPFabricSyncRunner(object):
163
158
  except Exception as err:
164
159
  # Log the error to logger outside of job - console/file
165
160
  logger.error(err, exc_info=True)
161
+ if hasattr(err, "issue_id") and err.issue_id:
162
+ # The error is already logged to user, no need to log it again
163
+ return None
166
164
  # Logging section for logs inside job - facing user
167
165
  self = args[0]
168
166
  if isinstance(err, SearchError):
@@ -176,6 +174,11 @@ class IPFabricSyncRunner(object):
176
174
  f"Syncing `{err.model}` is disabled in settings, but hit above error trying to find the correct item. Please check your transform maps and/or existing data.",
177
175
  obj=self.sync,
178
176
  )
177
+ if isinstance(err, IPAddressDuplicateError):
178
+ self.logger.log_warning(
179
+ f"IP Address `{err.data.get('address')}` already exists in `{err.model}` with coalesce fields: `{err.coalesce_fields}`. Please check your transform maps and/or existing data.",
180
+ obj=self.sync,
181
+ )
179
182
  else:
180
183
  self.logger.log_failure(
181
184
  f"Syncing failed with: `{err}`. See above error for more details.",
@@ -207,13 +210,21 @@ class IPFabricSyncRunner(object):
207
210
  except Exception as err:
208
211
  message = f"Error getting context for `{model}`."
209
212
  if isinstance(err, ObjectDoesNotExist):
210
- message += " Could not find related object using on template in transform maps."
213
+ message += (
214
+ " Could not find related object using template in transform maps."
215
+ )
211
216
  elif isinstance(err, MultipleObjectsReturned):
212
217
  message += " Multiple objects returned using on template in transform maps, the template is not strict enough."
213
- self.logger.log_failure(
214
- f"<b>{message}</b><br/>data: `{data}`<br/>error: `{err}`", obj=self.sync
218
+ _, issue = create_or_get_sync_issue(
219
+ exception=err,
220
+ ingestion=self.ingestion,
221
+ message=message,
222
+ model=model,
223
+ data=data,
215
224
  )
216
- raise SyncError(err) from err
225
+ raise SearchError(
226
+ message=message, data=data, model=model, issue_id=issue.id
227
+ ) from err
217
228
 
218
229
  queryset = transform_map.target_model.model_class().objects
219
230
 
@@ -232,25 +243,50 @@ class IPFabricSyncRunner(object):
232
243
  context.pop("defaults", None)
233
244
  object = queryset.using(connection_name).get(**context)
234
245
  except queryset.model.DoesNotExist as err:
235
- self.logger.log_failure(
236
- f"<b>`{model}` with these keys not found: `{context}`.</b><br/>Original data: `{data}`.",
237
- obj=self.sync,
246
+ message = f"Instance of `{model}` not found."
247
+ _, issue = create_or_get_sync_issue(
248
+ exception=err,
249
+ ingestion=self.ingestion,
250
+ message=message,
251
+ model=model,
252
+ context=context,
253
+ data=data,
238
254
  )
239
- raise SearchError(model=model, context=context, data=data) from err
255
+ raise SearchError(
256
+ message=message,
257
+ model=model,
258
+ context=context,
259
+ data=data,
260
+ issue_id=issue.id,
261
+ ) from err
240
262
  except queryset.model.MultipleObjectsReturned as err:
241
- self.logger.log_failure(
242
- f"<b>Multiple `{model}` with these keys found: `{context}`.</b><br/>Original data: `{data}`.",
243
- obj=self.sync,
263
+ message = f"Multiple instances of `{model}` found."
264
+ _, issue = create_or_get_sync_issue(
265
+ exception=err,
266
+ ingestion=self.ingestion,
267
+ message=message,
268
+ model=model,
269
+ context=context,
270
+ data=data,
244
271
  )
245
- raise SearchError(model=model, context=context, data=data) from err
272
+ raise SearchError(
273
+ message=message,
274
+ model=model,
275
+ context=context,
276
+ data=data,
277
+ issue_id=issue.id,
278
+ ) from err
246
279
  except Exception as err:
247
- self.logger.log_failure(
248
- FAILED_CREATE_INSTANCE_TEMPLATE.format(
249
- model=model, err=err, data=data, context=context
250
- ),
251
- obj=self.sync,
280
+ _, issue = create_or_get_sync_issue(
281
+ exception=err,
282
+ ingestion=self.ingestion,
283
+ model=model,
284
+ context=context,
285
+ data=data,
252
286
  )
253
- raise SyncDataError(model=model, context=context, data=data) from err
287
+ raise SyncDataError(
288
+ model=model, context=context, data=data, issue_id=issue.id
289
+ ) from err
254
290
 
255
291
  return object
256
292
 
@@ -440,7 +476,6 @@ class IPFabricSyncRunner(object):
440
476
  f"{len(data.get('ipaddress', []))} management IP's collected",
441
477
  obj=self.sync.snapshot_data.source,
442
478
  )
443
-
444
479
  self.logger.log_info("Ordering devices", obj=self.sync)
445
480
 
446
481
  members = order_members(data.get("virtualchassis", []))
@@ -631,7 +666,7 @@ class IPFabricSyncRunner(object):
631
666
  "dcim",
632
667
  "virtualchassis",
633
668
  virtual_chassis,
634
- False,
669
+ stats=False,
635
670
  sync=self.settings.get("virtualchassis"),
636
671
  )
637
672
 
@@ -640,6 +675,7 @@ class IPFabricSyncRunner(object):
640
675
  "dcim",
641
676
  "device",
642
677
  device,
678
+ stats=False,
643
679
  sync=self.settings.get("device")
644
680
  or self.settings.get("interface")
645
681
  or self.settings.get("ipaddress")
@@ -666,7 +702,7 @@ class IPFabricSyncRunner(object):
666
702
  "dcim",
667
703
  "virtualchassis",
668
704
  virtual_chassis,
669
- False,
705
+ stats=False,
670
706
  sync=self.settings.get("virtualchassis"),
671
707
  )
672
708
 
@@ -34,6 +34,7 @@ from utilities.views import ViewTab
34
34
  from .filtersets import IPFabricDataFilterSet
35
35
  from .filtersets import IPFabricIngestionChangeFilterSet
36
36
  from .filtersets import IPFabricIngestionFilterSet
37
+ from .filtersets import IPFabricIngestionIssueFilterSet
37
38
  from .filtersets import IPFabricSnapshotFilterSet
38
39
  from .filtersets import IPFabricSourceFilterSet
39
40
  from .filtersets import IPFabricTransformMapFilterSet
@@ -52,6 +53,7 @@ from .forms import IPFabricTransformMapForm
52
53
  from .forms import IPFabricTransformMapGroupForm
53
54
  from .models import IPFabricData
54
55
  from .models import IPFabricIngestion
56
+ from .models import IPFabricIngestionIssue
55
57
  from .models import IPFabricRelationshipField
56
58
  from .models import IPFabricSnapshot
57
59
  from .models import IPFabricSource
@@ -62,6 +64,7 @@ from .models import IPFabricTransformMapGroup
62
64
  from .tables import DeviceIPFTable
63
65
  from .tables import IPFabricDataTable
64
66
  from .tables import IPFabricIngestionChangesTable
67
+ from .tables import IPFabricIngestionIssuesTable
65
68
  from .tables import IPFabricIngestionTable
66
69
  from .tables import IPFabricRelationshipFieldTable
67
70
  from .tables import IPFabricSnapshotTable
@@ -657,36 +660,8 @@ class IPFabricIngestionListView(generic.ObjectListView):
657
660
  table = IPFabricIngestionTable
658
661
 
659
662
 
660
- @register_model_view(
661
- IPFabricIngestion,
662
- name="logs",
663
- path="logs",
664
- )
665
- class IPFabricIngestionLogView(LoginRequiredMixin, View):
666
- template_name = "ipfabric_netbox/partials/ingestion_all.html"
667
-
668
- def get(self, request, **kwargs):
669
- ingestion_id = kwargs.get("pk")
670
- if request.htmx:
671
- ingestion = IPFabricIngestion.objects.get(pk=ingestion_id)
672
- data = ingestion.get_statistics()
673
- data["object"] = ingestion
674
- data["job"] = ingestion.jobs.first()
675
- response = render(
676
- request,
677
- self.template_name,
678
- data,
679
- )
680
- if ingestion.job.completed:
681
- response["HX-Refresh"] = "true"
682
- return response
683
- else:
684
- return response
685
-
686
-
687
- @register_model_view(IPFabricIngestion)
688
- class IPFabricIngestionView(generic.ObjectView):
689
- queryset = IPFabricIngestion.objects.annotate(
663
+ def annotate_statistics(queryset):
664
+ return queryset.annotate(
690
665
  num_created=models.Count(
691
666
  "branch__changediff",
692
667
  filter=models.Q(
@@ -713,6 +688,39 @@ class IPFabricIngestionView(generic.ObjectView):
713
688
  staged_changes=models.Count(models.F("branch__changediff")),
714
689
  )
715
690
 
691
+
692
+ @register_model_view(
693
+ IPFabricIngestion,
694
+ name="logs",
695
+ path="logs",
696
+ )
697
+ class IPFabricIngestionLogView(LoginRequiredMixin, View):
698
+ template_name = "ipfabric_netbox/partials/ingestion_all.html"
699
+
700
+ def get(self, request, **kwargs):
701
+ ingestion_id = kwargs.get("pk")
702
+ if request.htmx:
703
+ ingestion = annotate_statistics(IPFabricIngestion.objects).get(
704
+ pk=ingestion_id
705
+ )
706
+ data = ingestion.get_statistics()
707
+ data["object"] = ingestion
708
+ data["job"] = ingestion.jobs.first()
709
+ response = render(
710
+ request,
711
+ self.template_name,
712
+ data,
713
+ )
714
+ if ingestion.job.completed:
715
+ response["HX-Refresh"] = "true"
716
+ return response
717
+ return render(request, self.template_name)
718
+
719
+
720
+ @register_model_view(IPFabricIngestion)
721
+ class IPFabricIngestionView(generic.ObjectView):
722
+ queryset = annotate_statistics(IPFabricIngestion.objects)
723
+
716
724
  def get_extra_context(self, request, instance):
717
725
  data = instance.get_statistics()
718
726
  return data
@@ -844,6 +852,23 @@ class IPFabricIngestionChangesView(generic.ObjectChildrenView):
844
852
  return self.child_model.objects.filter(branch=parent.branch)
845
853
 
846
854
 
855
+ @register_model_view(IPFabricIngestion, "ingestion_issues")
856
+ class IPFabricIngestionIssuesView(generic.ObjectChildrenView):
857
+ queryset = IPFabricIngestion.objects.all()
858
+ child_model = IPFabricIngestionIssue
859
+ table = IPFabricIngestionIssuesTable
860
+ template_name = "generic/object_children.html"
861
+ filterset = IPFabricIngestionIssueFilterSet
862
+ tab = ViewTab(
863
+ label="Ingestion Issues",
864
+ badge=lambda obj: IPFabricIngestionIssue.objects.filter(ingestion=obj).count(),
865
+ permission="ipfabric_netbox.view_ipfabricingestionissue",
866
+ )
867
+
868
+ def get_children(self, request, parent):
869
+ return IPFabricIngestionIssue.objects.filter(ingestion=parent)
870
+
871
+
847
872
  @register_model_view(IPFabricIngestion, "delete")
848
873
  class IPFabricIngestionDeleteView(generic.ObjectDeleteView):
849
874
  queryset = IPFabricIngestion.objects.all()
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "ipfabric_netbox"
3
- version = "4.1.0b4"
3
+ version = "4.1.0b6"
4
4
  description = "NetBox plugin to sync IP Fabric data into NetBox"
5
5
  authors = ["Solution Architecture <solution.architecture@ipfabric.io>"]
6
6
  license = "MIT"
@@ -1,24 +0,0 @@
1
- from core.exceptions import SyncError
2
-
3
-
4
- class ErrorMixin(Exception):
5
- model: str = ""
6
- defaults: dict[str, str] = {}
7
- coalesce_fields: dict[str, str] = {}
8
-
9
- def __init__(self, model: str, context: dict, data: dict = None):
10
- super().__init__()
11
- self.model = model
12
- self.data = data or {}
13
- self.defaults = context.pop("defaults", {})
14
- self.coalesce_fields = context
15
-
16
-
17
- class SearchError(ErrorMixin, LookupError):
18
- def __str__(self):
19
- return f"{self.model} with these keys not found: {self.coalesce_fields}."
20
-
21
-
22
- class SyncDataError(ErrorMixin, SyncError):
23
- def __str__(self):
24
- return f"Sync failed for {self.model}: coalesce_fields={self.coalesce_fields} defaults={self.defaults}."
@@ -1 +0,0 @@
1
- {% badge object.sync.get_status_display bg_color=object.sync.get_status_color %}