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.
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/PKG-INFO +1 -1
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/pyproject.toml +1 -1
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/_api/api.py +62 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/code/plan.py +38 -41
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/_diff.py +72 -37
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/_diffable.py +19 -7
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/_resource.py +4 -2
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/_serde.py +2 -1
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/_server_resources.py +104 -3
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/_util.py +18 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/channels.py +248 -3
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/credentials.py +148 -16
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/filters.py +1 -1
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/notification_rules.py +3 -3
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/sources.py +2 -2
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/tests/test__diff.py +99 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/tests/test__plan.py +8 -5
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/thresholds.py +1 -1
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/validators.py +355 -98
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/windows.py +5 -3
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/LICENSE +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/README_PUBLIC.md +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/__init__.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/_api/__init__.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/client/__init__.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/client/client.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/code/__init__.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/code/_import.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/code/_progress.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/code/apply.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/code/scaffold.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/code/settings.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/config.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/dbt.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/exception.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/metadata.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/py.typed +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/__init__.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/_diff_util.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/_errors.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/_resource_graph.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/_update_namespace.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/enums.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/replacement.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/segmentations.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/tags.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/tests/__init__.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/tests/assets/example_manifest.json +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/tests/assets/expected_trimmed_manifest.json +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/tests/test__dbt.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/tests/test__resource.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/tests/test__sql_validation.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/resource/tests/test_import.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/scalars.py +0 -0
- {validio_sdk-7.4.2 → validio_sdk-7.5.0}/validio_sdk/util.py +0 -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
284
|
-
if not
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
|
|
610
|
-
|
|
615
|
+
# No possible change if both are unset.
|
|
616
|
+
if manifest is None and server is None:
|
|
617
|
+
continue
|
|
611
618
|
|
|
612
|
-
|
|
613
|
-
if
|
|
614
|
-
return ResourceUpdate(
|
|
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
|
-
|
|
617
|
-
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
|
|
663
|
+
continue
|
|
633
664
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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 `
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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 =
|
|
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
|