validio-sdk 7.4.2__tar.gz → 7.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/PKG-INFO +1 -1
  2. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/pyproject.toml +1 -1
  3. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/_api/api.py +62 -0
  4. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/code/plan.py +38 -41
  5. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/_diff.py +72 -37
  6. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/_diffable.py +19 -7
  7. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/_resource.py +4 -2
  8. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/_serde.py +2 -1
  9. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/_server_resources.py +104 -3
  10. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/_util.py +18 -0
  11. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/channels.py +248 -3
  12. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/credentials.py +148 -16
  13. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/filters.py +1 -1
  14. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/notification_rules.py +3 -3
  15. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/sources.py +2 -2
  16. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/tests/test__diff.py +99 -0
  17. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/tests/test__plan.py +8 -5
  18. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/thresholds.py +1 -1
  19. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/validators.py +355 -98
  20. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/windows.py +5 -3
  21. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/LICENSE +0 -0
  22. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/README_PUBLIC.md +0 -0
  23. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/__init__.py +0 -0
  24. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/_api/__init__.py +0 -0
  25. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/client/__init__.py +0 -0
  26. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/client/client.py +0 -0
  27. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/code/__init__.py +0 -0
  28. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/code/_import.py +0 -0
  29. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/code/_progress.py +0 -0
  30. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/code/apply.py +0 -0
  31. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/code/scaffold.py +0 -0
  32. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/code/settings.py +0 -0
  33. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/config.py +0 -0
  34. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/dbt.py +0 -0
  35. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/exception.py +0 -0
  36. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/metadata.py +0 -0
  37. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/py.typed +0 -0
  38. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/__init__.py +0 -0
  39. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/_diff_util.py +0 -0
  40. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/_errors.py +0 -0
  41. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/_resource_graph.py +0 -0
  42. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/_update_namespace.py +0 -0
  43. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/enums.py +0 -0
  44. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/replacement.py +0 -0
  45. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/segmentations.py +0 -0
  46. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/tags.py +0 -0
  47. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/tests/__init__.py +0 -0
  48. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/tests/assets/example_manifest.json +0 -0
  49. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/tests/assets/expected_trimmed_manifest.json +0 -0
  50. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/tests/test__dbt.py +0 -0
  51. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/tests/test__resource.py +0 -0
  52. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/tests/test__sql_validation.py +0 -0
  53. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/tests/test_import.py +0 -0
  54. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/scalars.py +0 -0
  55. {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: validio-sdk
3
- Version: 7.4.2
3
+ Version: 7.5.0
4
4
  Summary: SDK to interact with the Validio platform
5
5
  Home-page: https://validio.io/
6
6
  License: Apache-2.0
@@ -3,7 +3,7 @@ name = "validio-sdk"
3
3
  # This version does not represent the released version or any tag. For each
4
4
  # release we automatically bump this before building and publishing so this
5
5
  # should be kept at 0.0.1dev1
6
- version = "7.4.2"
6
+ version = "7.5.0"
7
7
  description = "SDK to interact with the Validio platform"
8
8
  authors = ["Validio <support@validio.io>"]
9
9
  license = "Apache-2.0"
@@ -975,6 +975,19 @@ async def get_credentials(
975
975
  optBaseUrl: baseUrl
976
976
  }
977
977
  }
978
+ ... on AwsBedrockCredential {
979
+ config {
980
+ model
981
+ region
982
+ optBaseUrl: baseUrl
983
+ auth {
984
+ __typename
985
+ ... on AwsBedrockCredentialAccessKey {
986
+ accessKeyId
987
+ }
988
+ }
989
+ }
990
+ }
978
991
  ... on AwsCredential {
979
992
  config {
980
993
  accessKey
@@ -1312,6 +1325,29 @@ async def get_channels(
1312
1325
  }
1313
1326
  }
1314
1327
  }
1328
+ ... on ServiceNowChannel {
1329
+ config {
1330
+ applicationLinkUrl
1331
+ instanceUrl
1332
+ statusMapping {
1333
+ triage
1334
+ investigating
1335
+ resolved
1336
+ serviceNowActive
1337
+ resolvedCloseCode
1338
+ notAnAnomalyCloseCode
1339
+ }
1340
+ serviceNowAuth: auth {
1341
+ __typename
1342
+ ... on ServiceNowBasicAuth {
1343
+ username
1344
+ }
1345
+ ... on ServiceNowOAuth {
1346
+ clientId
1347
+ }
1348
+ }
1349
+ }
1350
+ }
1315
1351
  }
1316
1352
  """
1317
1353
 
@@ -1360,6 +1396,7 @@ async def get_channels(
1360
1396
  [
1361
1397
  ("msTeamsInteractiveMessageEnabled", "interactiveMessageEnabled"),
1362
1398
  ("emailInteractiveMessageEnabled", "interactiveMessageEnabled"),
1399
+ ("serviceNowAuth", "auth"),
1363
1400
  ],
1364
1401
  )
1365
1402
 
@@ -1952,6 +1989,31 @@ async def get_validators(
1952
1989
  }}
1953
1990
  {reference_source_config}
1954
1991
  }}
1992
+ ... on ReferentialIntegrityValidator {{
1993
+ config {{
1994
+ keyFields {{
1995
+ sourceField
1996
+ referenceField
1997
+ }}
1998
+ valueFields {{
1999
+ sourceField
2000
+ referenceField
2001
+ }}
2002
+ referenceSource {{
2003
+ resourceName
2004
+ }}
2005
+ referenceWindow {{
2006
+ resourceName
2007
+ }}
2008
+ referenceFilter {{
2009
+ resourceName
2010
+ }}
2011
+ initializeWithBackfill
2012
+ threshold {{
2013
+ ...ThresholdDetails
2014
+ }}
2015
+ }}
2016
+ }}
1955
2017
  ... on SqlValidator {{
1956
2018
  config {{
1957
2019
  query
@@ -24,7 +24,6 @@ from validio_sdk.resource._resource import (
24
24
  )
25
25
  from validio_sdk.resource._server_resources import load_resources
26
26
  from validio_sdk.resource._util import SourceSchemaReinference
27
- from validio_sdk.resource.channels import WebhookChannel
28
27
  from validio_sdk.resource.tags import Tag
29
28
 
30
29
 
@@ -228,7 +227,7 @@ def _create_resource_diff_object(
228
227
  show_secrets: bool,
229
228
  rewrites: dict[str, Any] | None = None,
230
229
  secret_fields_changed: dict[str, Any] | None = None,
231
- is_manifest: bool = False,
230
+ manifest: Resource | Diffable | dict | None = None,
232
231
  ) -> dict[str, object]:
233
232
  if rewrites is None:
234
233
  rewrites = {}
@@ -236,13 +235,24 @@ def _create_resource_diff_object(
236
235
  if secret_fields_changed is None:
237
236
  secret_fields_changed = {}
238
237
 
238
+ is_manifest = manifest is None
239
+
239
240
  data = dict(r) if not isinstance(r, dict) else r
241
+ manifest_data = (
242
+ dict(manifest)
243
+ if not isinstance(manifest, dict) and manifest is not None
244
+ else manifest
245
+ )
240
246
 
241
247
  diff_object = {}
242
248
  for k, v in data.items():
243
249
  if k.startswith("_"):
244
250
  continue
245
251
 
252
+ nested_manifest_data = (
253
+ manifest_data.get(k) if isinstance(manifest_data, dict) else None
254
+ )
255
+
246
256
  if k in rewrites:
247
257
  diff_object[k] = rewrites[k]
248
258
  elif isinstance(v, Resource | Diffable | dict):
@@ -251,7 +261,7 @@ def _create_resource_diff_object(
251
261
  v,
252
262
  show_secrets,
253
263
  secret_fields_changed=ch if not isinstance(ch, bool) else {},
254
- is_manifest=is_manifest,
264
+ manifest=nested_manifest_data,
255
265
  )
256
266
  elif hasattr(v, "__dict__") and not isinstance(v, Enum):
257
267
  ch = secret_fields_changed.get(k)
@@ -259,7 +269,7 @@ def _create_resource_diff_object(
259
269
  v.__dict__,
260
270
  show_secrets,
261
271
  secret_fields_changed=ch if not isinstance(ch, bool) else {},
262
- is_manifest=is_manifest,
272
+ manifest=nested_manifest_data,
263
273
  )
264
274
  elif isinstance(v, list):
265
275
  if len(v) == 0 or not isinstance(v[0], Diffable):
@@ -272,7 +282,7 @@ def _create_resource_diff_object(
272
282
  _create_resource_diff_object(
273
283
  item.__dict__,
274
284
  show_secrets,
275
- is_manifest=is_manifest,
285
+ manifest=None,
276
286
  )
277
287
  )
278
288
 
@@ -280,41 +290,28 @@ def _create_resource_diff_object(
280
290
  else:
281
291
  diff_object[k] = v
282
292
 
283
- # Mask any sensitive info if requested.
284
- if not show_secrets and hasattr(r, "_secret_fields"):
285
- secret_fields = r._secret_fields()
286
-
287
- if secret_fields:
288
- for field in secret_fields:
289
- # We want to avoid overriding webhook_url if it is None already
290
- # which may give a misleading impression that it is set to a value.
291
- #
292
- # For any channel that is not the WebhookChannel, we will always
293
- # flag it as changed if any secrets have changed, which is consistent
294
- # with existing behavior.
295
- #
296
- # This workaround until webhook URL is deprecated fully from some
297
- # channels (VR-4047).
298
- flag_webhook_url_as_changed = False
299
- if not isinstance(r, WebhookChannel) and field == "webhook_url":
300
- if hasattr(r, "webhook_url") and not r.webhook_url:
301
- continue
302
- if any(
303
- secret_fields_changed[secret_field]
304
- for secret_field in secret_fields_changed
305
- ):
306
- flag_webhook_url_as_changed = True
307
-
308
- if (
309
- # Special case handling for webhook URL
310
- flag_webhook_url_as_changed
311
- or
312
- # If the field does not exist in the secrets changed dict then
313
- # don't flag it as changed
314
- secret_fields_changed.get(field)
315
- ) and not is_manifest:
316
- diff_object[field] = "REDACTED-PREVIOUS"
317
- else:
318
- diff_object[field] = "REDACTED"
293
+ # Nothing to mask, no secrets
294
+ if not hasattr(r, "_secret_fields"):
295
+ return diff_object
296
+
297
+ for secret_field in r._secret_fields():
298
+ # The field can either be in the map set to False, or absent, to be
299
+ # treated as unchanged.
300
+ field_changed = (
301
+ secret_field in secret_fields_changed
302
+ and secret_fields_changed[secret_field]
303
+ )
304
+
305
+ if is_manifest:
306
+ if not show_secrets:
307
+ diff_object[secret_field] = "REDACTED"
308
+ elif field_changed:
309
+ diff_object[secret_field] = (
310
+ "UNKNOWN" if show_secrets else "REDACTED-PREVIOUS"
311
+ )
312
+ elif not show_secrets:
313
+ diff_object[secret_field] = "REDACTED"
314
+ elif manifest_data and secret_field in manifest_data:
315
+ diff_object[secret_field] = manifest_data[secret_field]
319
316
 
320
317
  return diff_object
@@ -594,52 +594,87 @@ def diff(
594
594
 
595
595
  # No changes yet. Next, descend into the nested fields. If we find any
596
596
  # changes, then mark _this_ resource as changed.
597
- server_nested_objects = server_object._nested_objects()
598
- for field, manifest in manifest_object._nested_objects().items():
599
- server = server_nested_objects[field]
600
-
601
- # No possible change if both are unset.
602
- if manifest is None and server is None:
603
- continue
604
-
605
- # If exactly one of them is None, then we definitely have a change.
606
- if manifest is None or server is None:
607
- return ResourceUpdate(manifest_resource, server_resource)
597
+ # For immutable nested objects, changes require the resource to be recreated.
598
+ nested_object_sources = [
599
+ (
600
+ manifest_object._mutable_nested_objects(),
601
+ server_object._mutable_nested_objects(),
602
+ False,
603
+ ),
604
+ (
605
+ manifest_object._immutable_nested_objects(),
606
+ server_object._immutable_nested_objects(),
607
+ True,
608
+ ),
609
+ ]
610
+ for manifest_nested, server_nested, is_immutable in nested_object_sources:
611
+ for field, manifest in manifest_nested.items():
612
+ server = server_nested[field]
613
+ replacement_field = field if is_immutable else None
608
614
 
609
- if isinstance(server, list) != isinstance(manifest, list):
610
- return ResourceUpdate(manifest_resource, server_resource)
615
+ # No possible change if both are unset.
616
+ if manifest is None and server is None:
617
+ continue
611
618
 
612
- if isinstance(server, list) and isinstance(manifest, list):
613
- if len(server) != len(manifest):
614
- return ResourceUpdate(manifest_resource, server_resource)
619
+ # If exactly one of them is None, then we definitely have a change.
620
+ if manifest is None or server is None:
621
+ return ResourceUpdate(
622
+ manifest_resource,
623
+ server_resource,
624
+ replacement_field=replacement_field,
625
+ )
615
626
 
616
- for i in range(len(server)):
617
- # Descend into both objects to diff.
618
- collected_diff = diff(
619
- manifest[i],
620
- server[i],
621
- curr_depth + 1,
627
+ if isinstance(server, list) != isinstance(manifest, list):
628
+ return ResourceUpdate(
622
629
  manifest_resource,
623
630
  server_resource,
631
+ replacement_field=replacement_field,
624
632
  )
625
633
 
626
- if collected_diff:
627
- # The returned diff is that of the full resource (not
628
- # just the nested object that had a change). So we can
629
- # exit with it.
630
- return collected_diff
634
+ if isinstance(server, list) and isinstance(manifest, list):
635
+ if len(server) != len(manifest):
636
+ return ResourceUpdate(
637
+ manifest_resource,
638
+ server_resource,
639
+ replacement_field=replacement_field,
640
+ )
641
+
642
+ for i in range(len(server)):
643
+ # Descend into both objects to diff.
644
+ collected_diff = diff(
645
+ manifest[i],
646
+ server[i],
647
+ curr_depth + 1,
648
+ manifest_resource,
649
+ server_resource,
650
+ )
651
+
652
+ if collected_diff:
653
+ # For immutable, flag for replacement. For mutable,
654
+ # return the collected diff directly.
655
+ if is_immutable:
656
+ return ResourceUpdate(
657
+ manifest_resource,
658
+ server_resource,
659
+ replacement_field=replacement_field,
660
+ )
661
+ return collected_diff
631
662
 
632
- continue
663
+ continue
633
664
 
634
- # Descend into both objects to diff.
635
- collected_diff = diff(
636
- manifest, server, curr_depth + 1, manifest_resource, server_resource
637
- )
638
- if collected_diff:
639
- # The returned diff is that of the full resource (not
640
- # just the nested object that had a change). So we can
641
- # exit with it.
642
- return collected_diff
665
+ collected_diff = diff(
666
+ manifest, server, curr_depth + 1, manifest_resource, server_resource
667
+ )
668
+ if collected_diff:
669
+ # For immutable, flag for replacement. For mutable,
670
+ # return the collected diff directly.
671
+ if is_immutable:
672
+ return ResourceUpdate(
673
+ manifest_resource,
674
+ server_resource,
675
+ replacement_field=replacement_field,
676
+ )
677
+ return collected_diff
643
678
 
644
679
  # No update to make
645
680
  return None
@@ -41,7 +41,7 @@ class Diffable(ABC):
41
41
  within a resource be sure to account for how that field should be diffed.
42
42
  Every field reachable from a resource, should be returned by exactly one
43
43
  of the listed APIs here, with the exception of `_replace_on_type_change_fields`
44
- which can overlap with `_nested_objects`.
44
+ which can overlap with `_mutable_nested_objects`.
45
45
  """
46
46
 
47
47
  @abstractmethod
@@ -57,16 +57,27 @@ class Diffable(ABC):
57
57
  return set({})
58
58
 
59
59
  @abstractmethod
60
- def _nested_objects(
60
+ def _mutable_nested_objects(
61
61
  self,
62
62
  ) -> dict[str, "Diffable | list[Diffable] | None"]:
63
- """Returns any nested objects contained within this object.
63
+ """Returns any mutable nested objects contained within this object.
64
64
 
65
- Nested objects will be diff-ed recursively.
65
+ Mutable nested objects will be diff-ed recursively.
66
66
  ...
67
67
  :returns dict[field, Optional[object]].
68
68
  """
69
69
 
70
+ def _immutable_nested_objects(
71
+ self,
72
+ ) -> dict[str, "Diffable | list[Diffable] | None"]:
73
+ """Returns any immutable nested objects contained within this object.
74
+
75
+ Immutable nested objects cannot be updated after creation.
76
+ ...
77
+ :returns dict[field, Optional[object]].
78
+ """
79
+ return {}
80
+
70
81
  def __iter__(self) -> Iterator[Any]:
71
82
  yield from self.__dict__.items()
72
83
 
@@ -83,7 +94,7 @@ class Diffable(ABC):
83
94
  if self._secret_fields():
84
95
  return True
85
96
 
86
- for nested in self._nested_objects().values():
97
+ for nested in self._mutable_nested_objects().values():
87
98
  if isinstance(nested, list):
88
99
  for n in nested:
89
100
  if n._has_secret_fields(curr_depth=curr_depth + 1):
@@ -98,7 +109,8 @@ class Diffable(ABC):
98
109
  return {
99
110
  *self._immutable_fields(),
100
111
  *self._mutable_fields(),
101
- *self._nested_objects().keys(),
112
+ *self._mutable_nested_objects().keys(),
113
+ *self._immutable_nested_objects().keys(),
102
114
  *self._ignored_fields(),
103
115
  }
104
116
 
@@ -239,7 +251,7 @@ class ApiSecretChangeNestedResource(Diffable):
239
251
  data[NODE_TYPE_FIELD_NAME] = self.__class__.__name__
240
252
  return data
241
253
 
242
- def _nested_objects(
254
+ def _mutable_nested_objects(
243
255
  self,
244
256
  ) -> dict[str, "Diffable | list[Diffable] | None"]:
245
257
  return {}
@@ -323,7 +323,9 @@ class Resource(Diffable):
323
323
 
324
324
  return {"_children": children}
325
325
 
326
- def _nested_objects(self) -> dict[str, Optional["Diffable | list[Diffable]"]]:
326
+ def _mutable_nested_objects(
327
+ self,
328
+ ) -> dict[str, Optional["Diffable | list[Diffable]"]]:
327
329
  return {}
328
330
 
329
331
  def _nested_mutable_parents(self) -> dict[str, str | None]:
@@ -568,7 +570,7 @@ class Resource(Diffable):
568
570
 
569
571
  def _nested_secret_objects(self) -> dict[str, ApiSecretChangeNestedResource]:
570
572
  objects = {}
571
- for f in self._nested_objects():
573
+ for f in self._mutable_nested_objects():
572
574
  obj = getattr(self, f)
573
575
  if isinstance(obj, ApiSecretChangeNestedResource):
574
576
  objects[f] = obj
@@ -206,7 +206,8 @@ def _api_input(
206
206
 
207
207
  skip_fields = {
208
208
  *set(overrides.keys()),
209
- *set(resource._nested_objects().keys()),
209
+ *set(resource._mutable_nested_objects().keys()),
210
+ *set(resource._immutable_nested_objects().keys()),
210
211
  *skip_fields,
211
212
  "name",
212
213
  "resourceName",
@@ -29,11 +29,17 @@ from validio_sdk.resource._diff_util import (
29
29
  must_find_window,
30
30
  )
31
31
  from validio_sdk.resource._resource import Resource, ResourceGraph
32
- from validio_sdk.resource._util import _rename_dict_key, _sanitized_error_str
32
+ from validio_sdk.resource._util import (
33
+ _rename_dict_key,
34
+ _sanitized_error_str,
35
+ _to_snake_recursive,
36
+ )
33
37
  from validio_sdk.resource.channels import (
34
38
  Channel,
35
39
  EmailChannel,
36
40
  MsTeamsChannel,
41
+ ServiceNowChannel,
42
+ ServiceNowStatusMapping,
37
43
  SlackChannel,
38
44
  WebhookChannel,
39
45
  )
@@ -43,6 +49,10 @@ from validio_sdk.resource.credentials import (
43
49
  AnthropicCredentialAuth,
44
50
  AtlanCredential,
45
51
  AwsAthenaCredential,
52
+ AwsBedrockCredential,
53
+ AwsBedrockCredentialAccessKey,
54
+ AwsBedrockCredentialApiKey,
55
+ AwsBedrockCredentialAuth,
46
56
  AwsCredential,
47
57
  AwsRedshiftCredential,
48
58
  AzureSynapseEntraIdCredential,
@@ -90,7 +100,13 @@ from validio_sdk.resource.segmentations import Segmentation
90
100
  from validio_sdk.resource.sources import AzureSynapseSource, Source, SqlSource
91
101
  from validio_sdk.resource.tags import Tag
92
102
  from validio_sdk.resource.thresholds import Threshold
93
- from validio_sdk.resource.validators import Reference, SqlValidator, Validator
103
+ from validio_sdk.resource.validators import (
104
+ FieldMapping,
105
+ Reference,
106
+ ReferentialIntegrityValidator,
107
+ SqlValidator,
108
+ Validator,
109
+ )
94
110
 
95
111
  # Some credentials depend on other credentials, i.e. wrapping credentials. This
96
112
  # list contains all of those and can be used when sorting to ensure they always
@@ -227,6 +243,34 @@ async def load_credentials(
227
243
  display_name=display_name,
228
244
  __internal__=g,
229
245
  )
246
+ case "AwsBedrockCredential":
247
+ match c["config"]["auth"]["__typename"]:
248
+ case "AwsBedrockCredentialAccessKey":
249
+ bedrock_auth: AwsBedrockCredentialAuth = (
250
+ AwsBedrockCredentialAccessKey(
251
+ access_key_id=c["config"]["auth"]["accessKeyId"],
252
+ secret_access_key="UNSET",
253
+ )
254
+ )
255
+ case "AwsBedrockCredentialApiKey":
256
+ bedrock_auth = AwsBedrockCredentialApiKey(
257
+ api_key="UNSET",
258
+ )
259
+ case _:
260
+ raise ValidioBugError(
261
+ f"Unknown AWS Bedrock auth type on {name}: "
262
+ f"'{c['config']['auth']['__typename']}'"
263
+ )
264
+
265
+ credential = AwsBedrockCredential(
266
+ name=name,
267
+ model=c["config"]["model"],
268
+ region=c["config"]["region"],
269
+ base_url=c["config"]["baseUrl"],
270
+ auth=bedrock_auth,
271
+ display_name=display_name,
272
+ __internal__=g,
273
+ )
230
274
  case "AtlanCredential":
231
275
  credential = AtlanCredential(
232
276
  name=name,
@@ -646,6 +690,28 @@ async def load_channels(
646
690
  "__internal__": g,
647
691
  }
648
692
  )
693
+ case "ServiceNowChannel":
694
+ auth = cfg["auth"]
695
+ auth_cls = eval(
696
+ f"validio_sdk.resource.channels.{auth.pop('__typename')}"
697
+ )
698
+
699
+ channel = ServiceNowChannel(
700
+ **{
701
+ **_to_snake_recursive(cfg),
702
+ "name": name,
703
+ "application_link_url": application_link_url,
704
+ "display_name": display_name,
705
+ "webhook_secret": "UNSET",
706
+ "auth": auth_cls(
707
+ **{
708
+ f: auth.get(to_camel(f), "UNSET")
709
+ for f in inspect.signature(auth_cls).parameters
710
+ }
711
+ ),
712
+ "__internal__": g,
713
+ }
714
+ )
649
715
  case _:
650
716
  raise ValidioError(f"unsupported channel '{name}' of type '{type(ch)}'")
651
717
 
@@ -838,6 +904,26 @@ def convert_reference(ctx: DiffContext, r: dict[str, Any]) -> Reference:
838
904
  )
839
905
 
840
906
 
907
+ def extract_reference_resources(
908
+ ctx: DiffContext, config: dict[str, Any]
909
+ ) -> dict[str, Any]:
910
+ out = {
911
+ "reference_source": must_find_source(
912
+ ctx, config["referenceSource"]["resourceName"]
913
+ ),
914
+ "reference_window": must_find_window(
915
+ ctx, config["referenceWindow"]["resourceName"]
916
+ ),
917
+ }
918
+
919
+ if config.get("referenceFilter"):
920
+ out["reference_filter"] = must_find_filter(
921
+ ctx, config["referenceFilter"]["resourceName"]
922
+ )
923
+
924
+ return out
925
+
926
+
841
927
  async def load_validators(
842
928
  namespace: str,
843
929
  ctx: DiffContext,
@@ -880,13 +966,19 @@ async def load_validators(
880
966
  else {}
881
967
  )
882
968
 
969
+ maybe_reference_resources = (
970
+ extract_reference_resources(ctx, config)
971
+ if "referenceSource" in config
972
+ else {}
973
+ )
974
+
883
975
  # Volume validator still have a deprecated field that we use. It's
884
976
  # called sourceField in the API still but `optional_source_field` on
885
977
  # the resource class so we rename it here.
886
978
  if v["__typename"] == "VolumeValidator":
887
979
  _rename_dict_key(config, "sourceField", "optionalSourceField")
888
980
 
889
- config = {to_snake(k): v for k, v in config.items() if k != "threshold"}
981
+ config = _to_snake_recursive(config)
890
982
 
891
983
  cls = eval(f"validio_sdk.resource.validators.{v['__typename']}")
892
984
 
@@ -895,6 +987,7 @@ async def load_validators(
895
987
  **config,
896
988
  **maybe_reference,
897
989
  **maybe_filter,
990
+ **maybe_reference_resources,
898
991
  "threshold": threshold,
899
992
  "name": name,
900
993
  "window": window,
@@ -1028,6 +1121,14 @@ async def apply_deletes(
1028
1121
  list(deletes.notification_rules.values()), session, progress_bar
1029
1122
  )
1030
1123
 
1124
+ # Delete any validators with cross source references before sources since
1125
+ # they might prevent source deletes.
1126
+ await _delete_resources(
1127
+ [v for v in deletes.validators.values() if v.has_cross_source_references()],
1128
+ session,
1129
+ progress_bar,
1130
+ )
1131
+
1031
1132
  # For pipeline resources, start with sources (This cascades deletes,
1032
1133
  # so we don't have to individually delete child resources).
1033
1134
  await _delete_resources(list(deletes.sources.values()), session, progress_bar)
@@ -1,6 +1,7 @@
1
1
  from dataclasses import dataclass
2
2
  from typing import Any
3
3
 
4
+ from camel_converter import to_snake
4
5
  from gql.transport.exceptions import TransportServerError
5
6
 
6
7
 
@@ -46,3 +47,20 @@ def _rename_dict_key(d: dict[str, Any], from_key: str, to_key: str) -> None:
46
47
 
47
48
  d[to_key] = d[from_key]
48
49
  del d[from_key]
50
+
51
+
52
+ def _to_snake_recursive(obj: Any, max_depth: int = 10, _depth: int = 0) -> Any:
53
+ """Recursively convert dictionary keys to snake_case."""
54
+ if _depth > max_depth:
55
+ return obj
56
+
57
+ if isinstance(obj, dict):
58
+ return {
59
+ to_snake(k): _to_snake_recursive(v, max_depth, _depth + 1)
60
+ for k, v in obj.items()
61
+ }
62
+
63
+ if isinstance(obj, list):
64
+ return [_to_snake_recursive(item, max_depth, _depth + 1) for item in obj]
65
+
66
+ return obj