validio-sdk 7.0.0__tar.gz → 7.3.1__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.0.0 → validio_sdk-7.3.1}/PKG-INFO +1 -1
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/pyproject.toml +1 -1
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/_api/api.py +37 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/code/plan.py +0 -2
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/exception.py +42 -7
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/_diff.py +24 -102
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/_diffable.py +33 -3
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/_errors.py +1 -5
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/_resource.py +43 -42
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/_serde.py +6 -1
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/_server_resources.py +83 -14
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/_update_namespace.py +4 -5
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/_util.py +13 -9
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/channels.py +8 -9
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/credentials.py +319 -10
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/filters.py +4 -7
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/notification_rules.py +0 -6
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/segmentations.py +5 -2
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/sources.py +77 -3
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/tests/test__diff.py +0 -151
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/tests/test__resource.py +2 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/validators.py +49 -66
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/windows.py +26 -1
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/LICENSE +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/README_PUBLIC.md +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/__init__.py +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/_api/__init__.py +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/client/__init__.py +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/client/client.py +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/code/__init__.py +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/code/_import.py +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/code/_progress.py +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/code/apply.py +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/code/scaffold.py +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/code/settings.py +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/config.py +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/dbt.py +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/metadata.py +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/py.typed +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/__init__.py +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/_diff_util.py +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/_resource_graph.py +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/enums.py +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/replacement.py +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/tags.py +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/tests/__init__.py +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/tests/assets/example_manifest.json +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/tests/assets/expected_trimmed_manifest.json +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/tests/test__dbt.py +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/tests/test__plan.py +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/tests/test__sql_validation.py +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/tests/test_import.py +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/thresholds.py +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/scalars.py +0 -0
- {validio_sdk-7.0.0 → validio_sdk-7.3.1}/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.3.1"
|
|
7
7
|
description = "SDK to interact with the Validio platform"
|
|
8
8
|
authors = ["Validio <support@validio.io>"]
|
|
9
9
|
license = "Apache-2.0"
|
|
@@ -553,6 +553,13 @@ async def get_sources(
|
|
|
553
553
|
schedule
|
|
554
554
|
}
|
|
555
555
|
}
|
|
556
|
+
... on TeradataSource {
|
|
557
|
+
config {
|
|
558
|
+
database
|
|
559
|
+
table
|
|
560
|
+
schedule
|
|
561
|
+
}
|
|
562
|
+
}
|
|
556
563
|
}
|
|
557
564
|
"""
|
|
558
565
|
|
|
@@ -962,6 +969,12 @@ async def get_credentials(
|
|
|
962
969
|
baseUrl
|
|
963
970
|
}
|
|
964
971
|
}
|
|
972
|
+
... on AnthropicCredential {
|
|
973
|
+
config {
|
|
974
|
+
model
|
|
975
|
+
optBaseUrl: baseUrl
|
|
976
|
+
}
|
|
977
|
+
}
|
|
965
978
|
... on AwsCredential {
|
|
966
979
|
config {
|
|
967
980
|
accessKey
|
|
@@ -1063,6 +1076,12 @@ async def get_credentials(
|
|
|
1063
1076
|
}
|
|
1064
1077
|
enableCatalog
|
|
1065
1078
|
}
|
|
1079
|
+
... on OmniCredential {
|
|
1080
|
+
config {
|
|
1081
|
+
baseUrl
|
|
1082
|
+
optUser: user
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1066
1085
|
... on OracleCredential {
|
|
1067
1086
|
config {
|
|
1068
1087
|
host
|
|
@@ -1160,6 +1179,21 @@ async def get_credentials(
|
|
|
1160
1179
|
}
|
|
1161
1180
|
enableCatalog
|
|
1162
1181
|
}
|
|
1182
|
+
... on TeradataCredential {
|
|
1183
|
+
config {
|
|
1184
|
+
host
|
|
1185
|
+
sslMode
|
|
1186
|
+
httpsPort
|
|
1187
|
+
tdmstPort
|
|
1188
|
+
auth {
|
|
1189
|
+
__typename
|
|
1190
|
+
... on TeradataCredentialUserPassword {
|
|
1191
|
+
user
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
enableCatalog
|
|
1196
|
+
}
|
|
1163
1197
|
}
|
|
1164
1198
|
"""
|
|
1165
1199
|
|
|
@@ -1207,6 +1241,8 @@ async def get_credentials(
|
|
|
1207
1241
|
[
|
|
1208
1242
|
("powerBiAuth", "auth"),
|
|
1209
1243
|
("databaseRequired", "database"),
|
|
1244
|
+
("optUser", "user"),
|
|
1245
|
+
("optBaseUrl", "baseUrl"),
|
|
1210
1246
|
],
|
|
1211
1247
|
)
|
|
1212
1248
|
result[i] = credential
|
|
@@ -1246,6 +1282,7 @@ async def get_channels(
|
|
|
1246
1282
|
config {
|
|
1247
1283
|
applicationLinkUrl
|
|
1248
1284
|
msTeamsChannelId
|
|
1285
|
+
tenantId
|
|
1249
1286
|
msTeamsInteractiveMessageEnabled: interactiveMessageEnabled
|
|
1250
1287
|
}
|
|
1251
1288
|
}
|
|
@@ -72,7 +72,6 @@ async def plan(
|
|
|
72
72
|
no_capture: bool,
|
|
73
73
|
show_secrets: bool,
|
|
74
74
|
targets: ResourceNames = ResourceNames(),
|
|
75
|
-
import_mode: bool = False,
|
|
76
75
|
show_progress: bool = True,
|
|
77
76
|
) -> PlanResult:
|
|
78
77
|
"""Computes a diff between the manifest program and the live server resources."""
|
|
@@ -101,7 +100,6 @@ async def plan(
|
|
|
101
100
|
show_secrets=show_secrets,
|
|
102
101
|
manifest_ctx=manifest_ctx,
|
|
103
102
|
server_ctx=server_ctx,
|
|
104
|
-
import_mode=import_mode,
|
|
105
103
|
)
|
|
106
104
|
diff.retain(targets)
|
|
107
105
|
progress_bar.update(advance=1)
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
"""Exceptions used throughout the system."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
5
|
|
|
6
6
|
from aiohttp.client_exceptions import ClientConnectorError
|
|
7
7
|
from gql.transport.exceptions import TransportQueryError, TransportServerError
|
|
8
8
|
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from validio_sdk.resource._resource import Resource
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
NUM_FIELDS_IN_GRAPHQL_ERROR = 3
|
|
14
|
+
|
|
9
15
|
|
|
10
16
|
class ValidioError(Exception):
|
|
11
17
|
"""Base exception used for every exception thrown by Validio."""
|
|
@@ -15,6 +21,29 @@ class ValidioError(Exception):
|
|
|
15
21
|
super().__init__(*args)
|
|
16
22
|
|
|
17
23
|
|
|
24
|
+
class ManifestConfigurationError(ValidioError):
|
|
25
|
+
"""Raised when there is an invalid configuration in the manifest."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ValidioResourceError(ValidioError):
|
|
29
|
+
"""Exception related to a specific resource."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, resource: "Resource", message: str):
|
|
32
|
+
"""Construct the exception.
|
|
33
|
+
|
|
34
|
+
:param resource: The resource with error.
|
|
35
|
+
:param message: The exception message.
|
|
36
|
+
"""
|
|
37
|
+
self.resource = resource
|
|
38
|
+
self.message = message
|
|
39
|
+
|
|
40
|
+
def __str__(self) -> str:
|
|
41
|
+
"""String representation for the exception."""
|
|
42
|
+
return (
|
|
43
|
+
f"{self.resource.__class__.__name__} '{self.resource.name}': {self.message}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
18
47
|
class ValidioTimeoutError(ValidioError):
|
|
19
48
|
"""Validio specific timeout error."""
|
|
20
49
|
|
|
@@ -142,14 +171,20 @@ def _parse_error(
|
|
|
142
171
|
raise UnauthorizedError(access_key_env, secret_access_key_env)
|
|
143
172
|
case "FORBIDDEN":
|
|
144
173
|
raise ForbiddenError
|
|
145
|
-
case _:
|
|
146
|
-
raise e
|
|
147
174
|
|
|
148
|
-
|
|
175
|
+
# If we only have the regular fields from a GraphQL error, only raise
|
|
176
|
+
# the message part.
|
|
177
|
+
if len(error_details) == NUM_FIELDS_IN_GRAPHQL_ERROR and all(
|
|
178
|
+
x in error_details for x in ["message", "locations", "path"]
|
|
179
|
+
):
|
|
180
|
+
raise ValidioError(error_details["message"])
|
|
181
|
+
|
|
182
|
+
raise e
|
|
183
|
+
|
|
184
|
+
if isinstance(e, TransportServerError):
|
|
149
185
|
raise ValidioError(f"🛑 Server error: {e}")
|
|
150
186
|
|
|
151
|
-
|
|
187
|
+
if isinstance(e, TimeoutError):
|
|
152
188
|
raise ValidioTimeoutError(timeout=timeout)
|
|
153
189
|
|
|
154
|
-
|
|
155
|
-
raise e
|
|
190
|
+
raise e
|
|
@@ -18,8 +18,9 @@ from validio_sdk._api.api import (
|
|
|
18
18
|
users_by_emails,
|
|
19
19
|
)
|
|
20
20
|
from validio_sdk.client import Session
|
|
21
|
-
from validio_sdk.exception import
|
|
21
|
+
from validio_sdk.exception import ValidioError, ValidioResourceError
|
|
22
22
|
from validio_sdk.resource._diffable import (
|
|
23
|
+
MAX_RESOURCE_DEPTH,
|
|
23
24
|
ApiSecretChangeNestedResource,
|
|
24
25
|
Diffable,
|
|
25
26
|
)
|
|
@@ -28,8 +29,12 @@ from validio_sdk.resource._errors import (
|
|
|
28
29
|
max_resource_depth_exceeded,
|
|
29
30
|
updated_resource_type_mismatch_exception,
|
|
30
31
|
)
|
|
31
|
-
from validio_sdk.resource._resource import
|
|
32
|
-
|
|
32
|
+
from validio_sdk.resource._resource import (
|
|
33
|
+
CREATE_ONLY_RESOURCES,
|
|
34
|
+
DiffContext,
|
|
35
|
+
Resource,
|
|
36
|
+
)
|
|
37
|
+
from validio_sdk.resource._util import SourceSchemaReinference, _sanitized_error_str
|
|
33
38
|
from validio_sdk.resource.replacement import (
|
|
34
39
|
CascadeReplacementReason,
|
|
35
40
|
ImmutableFieldReplacementReason,
|
|
@@ -52,13 +57,6 @@ NUM_CONCURRENT_INFERENCE_TASKS = 1
|
|
|
52
57
|
|
|
53
58
|
R = TypeVar("R", bound=Resource)
|
|
54
59
|
|
|
55
|
-
"""
|
|
56
|
-
When we descend into nested objects, we set a limit on how deep we go.
|
|
57
|
-
Otherwise, in a bad manifest configuration or due to a bug in our code, we
|
|
58
|
-
could enter a cycle.
|
|
59
|
-
"""
|
|
60
|
-
MAX_RESOURCE_DEPTH = 15
|
|
61
|
-
|
|
62
60
|
|
|
63
61
|
@dataclass
|
|
64
62
|
class ResourceUpdate:
|
|
@@ -175,13 +173,11 @@ async def diff_resource_graph(
|
|
|
175
173
|
show_secrets: bool,
|
|
176
174
|
manifest_ctx: DiffContext,
|
|
177
175
|
server_ctx: DiffContext,
|
|
178
|
-
import_mode: bool = False,
|
|
179
176
|
) -> GraphDiff:
|
|
180
177
|
graph_diff = _diff_resource_graph(
|
|
181
178
|
namespace=namespace,
|
|
182
179
|
manifest_ctx=manifest_ctx,
|
|
183
180
|
server_ctx=server_ctx,
|
|
184
|
-
import_mode=import_mode,
|
|
185
181
|
)
|
|
186
182
|
|
|
187
183
|
await enrich_resource_graph(
|
|
@@ -368,9 +364,9 @@ def _visit_resource_to_cascade_update(
|
|
|
368
364
|
if resource_name not in manifest_resources or resource_name not in server_resources:
|
|
369
365
|
# We expect both manifest and server object, since we've
|
|
370
366
|
# checked for create/delete operation on this resource.
|
|
371
|
-
raise
|
|
372
|
-
|
|
373
|
-
|
|
367
|
+
raise ValidioResourceError(
|
|
368
|
+
manifest_resources[resource_name],
|
|
369
|
+
"unable to cascade update: missing manifest or server object",
|
|
374
370
|
)
|
|
375
371
|
|
|
376
372
|
to_update[resource_name] = ResourceUpdate(
|
|
@@ -387,15 +383,7 @@ def _diff_resource_graph(
|
|
|
387
383
|
namespace: str,
|
|
388
384
|
manifest_ctx: DiffContext,
|
|
389
385
|
server_ctx: DiffContext,
|
|
390
|
-
import_mode: bool = False,
|
|
391
386
|
) -> GraphDiff:
|
|
392
|
-
# Backwards compatibility: Here we note the filters that
|
|
393
|
-
# are in-use by validators.
|
|
394
|
-
# See usage site for more info.
|
|
395
|
-
# Note: we do this before diff, since we lose some info (when we
|
|
396
|
-
# force a validator into filter-expression mode) during diff.
|
|
397
|
-
server_filter_references = _collect_filter_references(server_ctx)
|
|
398
|
-
|
|
399
387
|
fns = [
|
|
400
388
|
(compute_creates, DiffContext),
|
|
401
389
|
(compute_deletes, DiffContext),
|
|
@@ -424,18 +412,13 @@ def _diff_resource_graph(
|
|
|
424
412
|
),
|
|
425
413
|
)
|
|
426
414
|
|
|
427
|
-
|
|
415
|
+
return GraphDiff(
|
|
428
416
|
to_create=diffs[0],
|
|
429
417
|
to_delete=diffs[1],
|
|
430
418
|
to_update=diffs[2],
|
|
431
419
|
replacement_ctx=ReplacementContext(),
|
|
432
420
|
)
|
|
433
421
|
|
|
434
|
-
if not import_mode:
|
|
435
|
-
_retain_auto_generated_filter_resources(g, server_filter_references)
|
|
436
|
-
|
|
437
|
-
return g
|
|
438
|
-
|
|
439
422
|
|
|
440
423
|
def _collect_filter_references(
|
|
441
424
|
server_ctx: DiffContext,
|
|
@@ -450,47 +433,6 @@ def _collect_filter_references(
|
|
|
450
433
|
return live_filters
|
|
451
434
|
|
|
452
435
|
|
|
453
|
-
def _retain_auto_generated_filter_resources(
|
|
454
|
-
g: GraphDiff,
|
|
455
|
-
server_filter_references: set[tuple[str, str]],
|
|
456
|
-
) -> None:
|
|
457
|
-
"""
|
|
458
|
-
Backwards compatibility while we transition to Filter resources.
|
|
459
|
-
|
|
460
|
-
On the API side we only deal with Filter resources. While iac
|
|
461
|
-
side, we support JSONFilterExpression during the transition - on the
|
|
462
|
-
backend, the filter expression is represented by an actual Filter
|
|
463
|
-
resource that we auto generate.
|
|
464
|
-
But this behavior can is inconsistent from a diff pov since there's
|
|
465
|
-
no representation of the auto-generated filter resource in the iac
|
|
466
|
-
manifest. So that diff will flag the auto-generated resource to be
|
|
467
|
-
deleted, which is undesirable.
|
|
468
|
-
|
|
469
|
-
To avoid this behavior, we just check for this condition. If we flagged
|
|
470
|
-
to delete a Filter that is still in use by some validator, then basically
|
|
471
|
-
un-flag that filter for delete.
|
|
472
|
-
"""
|
|
473
|
-
if len(g.to_delete.filters) == 0:
|
|
474
|
-
return
|
|
475
|
-
|
|
476
|
-
for validator_name, filter_name in list(server_filter_references):
|
|
477
|
-
# If a validator is being deleted, then we don't count references
|
|
478
|
-
# towards its filters.
|
|
479
|
-
if (
|
|
480
|
-
validator_name in g.to_delete.validators
|
|
481
|
-
and validator_name not in g.to_create.validators
|
|
482
|
-
):
|
|
483
|
-
server_filter_references.remove((validator_name, filter_name))
|
|
484
|
-
|
|
485
|
-
live_filters = {filter_name for _, filter_name in server_filter_references}
|
|
486
|
-
|
|
487
|
-
for filter_name in set(g.to_delete.filters.keys()):
|
|
488
|
-
# If we were already going to re-create the filter
|
|
489
|
-
# after deleting, then nothing to do.
|
|
490
|
-
if filter_name not in g.to_create.filters and filter_name in live_filters:
|
|
491
|
-
del g.to_delete.filters[filter_name]
|
|
492
|
-
|
|
493
|
-
|
|
494
436
|
def compute_creates(
|
|
495
437
|
namespace: str, manifest_resources: dict[str, R], server_resources: dict[str, R]
|
|
496
438
|
) -> dict[str, R]:
|
|
@@ -940,7 +882,7 @@ async def check_for_secret_changes(
|
|
|
940
882
|
if not server_object:
|
|
941
883
|
continue
|
|
942
884
|
|
|
943
|
-
if not _has_secret_fields(
|
|
885
|
+
if not manifest_object._has_secret_fields(curr_depth=1):
|
|
944
886
|
continue
|
|
945
887
|
|
|
946
888
|
secret_fields_changed = await _check_secret_change(
|
|
@@ -964,28 +906,6 @@ async def check_for_secret_changes(
|
|
|
964
906
|
)
|
|
965
907
|
|
|
966
908
|
|
|
967
|
-
# Checks if the Diffable object or any of its sub objects
|
|
968
|
-
# defines any secret fields.
|
|
969
|
-
def _has_secret_fields(obj: Diffable, curr_depth: int) -> bool:
|
|
970
|
-
if curr_depth > MAX_RESOURCE_DEPTH:
|
|
971
|
-
raise ValidioError(
|
|
972
|
-
"BUG: max recursion depth exceeded while looking for secrets"
|
|
973
|
-
)
|
|
974
|
-
|
|
975
|
-
if hasattr(obj, "_secret_fields") and obj._secret_fields():
|
|
976
|
-
return True
|
|
977
|
-
|
|
978
|
-
for nested in obj._nested_objects().values():
|
|
979
|
-
if isinstance(nested, list):
|
|
980
|
-
for n in nested:
|
|
981
|
-
if _has_secret_fields(n, curr_depth=curr_depth + 1):
|
|
982
|
-
return True
|
|
983
|
-
elif nested and _has_secret_fields(nested, curr_depth + 1):
|
|
984
|
-
return True
|
|
985
|
-
|
|
986
|
-
return False
|
|
987
|
-
|
|
988
|
-
|
|
989
909
|
async def _check_secret_change(
|
|
990
910
|
session: Session,
|
|
991
911
|
show_secrets: bool,
|
|
@@ -1024,13 +944,15 @@ async def _check_secret_change(
|
|
|
1024
944
|
query_response_fields=query_response_fields,
|
|
1025
945
|
variable_values=resource._api_secret_change_input(),
|
|
1026
946
|
)
|
|
1027
|
-
except TransportQueryError as
|
|
1028
|
-
raise
|
|
947
|
+
except TransportQueryError as sanitized:
|
|
948
|
+
raise ValidioResourceError(
|
|
949
|
+
resource, _sanitized_error_str(sanitized, show_secrets)
|
|
950
|
+
)
|
|
1029
951
|
|
|
1030
952
|
if response["errors"]:
|
|
1031
|
-
raise
|
|
1032
|
-
|
|
1033
|
-
f"{response['errors']}"
|
|
953
|
+
raise ValidioResourceError(
|
|
954
|
+
resource,
|
|
955
|
+
f"failed to check for changed secrets: {response['errors']}",
|
|
1034
956
|
)
|
|
1035
957
|
|
|
1036
958
|
# map response values to secret fields changed
|
|
@@ -1058,8 +980,8 @@ def _check_namespace(namespace: str, server_resource: Resource) -> None:
|
|
|
1058
980
|
|
|
1059
981
|
server_namespace = server_resource._must_namespace()
|
|
1060
982
|
if namespace != server_namespace:
|
|
1061
|
-
raise
|
|
1062
|
-
|
|
1063
|
-
"does not belong to the current namespace; "
|
|
1064
|
-
f"resource namespace = {server_namespace}; current namespace = {namespace}"
|
|
983
|
+
raise ValidioResourceError(
|
|
984
|
+
server_resource,
|
|
985
|
+
"resource does not belong to the current namespace; "
|
|
986
|
+
f"resource namespace = {server_namespace}; current namespace = {namespace}",
|
|
1065
987
|
)
|
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
from abc import ABC, abstractmethod
|
|
3
3
|
from collections.abc import Iterator
|
|
4
|
-
from typing import TYPE_CHECKING, Any
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
5
|
|
|
6
6
|
from camel_converter import to_camel
|
|
7
7
|
|
|
8
|
+
from validio_sdk.exception import ValidioError
|
|
8
9
|
from validio_sdk.resource._serde import (
|
|
9
10
|
IGNORE_CHANGES_FIELD_NAME,
|
|
10
11
|
NODE_TYPE_FIELD_NAME,
|
|
12
|
+
SECRET_VALUE_FIXME_COMMENT,
|
|
11
13
|
_import_value_repr,
|
|
12
14
|
)
|
|
13
15
|
|
|
14
16
|
if TYPE_CHECKING:
|
|
15
17
|
from validio_sdk.code._import import ImportContext
|
|
16
18
|
|
|
19
|
+
"""
|
|
20
|
+
When we descend into nested objects, we set a limit on how deep we go.
|
|
21
|
+
Otherwise, in a bad manifest configuration or due to a bug in our code, we
|
|
22
|
+
could enter a cycle.
|
|
23
|
+
"""
|
|
24
|
+
MAX_RESOURCE_DEPTH = 15
|
|
25
|
+
|
|
17
26
|
|
|
18
27
|
class Diffable(ABC):
|
|
19
28
|
"""
|
|
@@ -44,7 +53,7 @@ class Diffable(ABC):
|
|
|
44
53
|
@abstractmethod
|
|
45
54
|
def _nested_objects(
|
|
46
55
|
self,
|
|
47
|
-
) -> dict[str,
|
|
56
|
+
) -> dict[str, "Diffable | list[Diffable] | None"]:
|
|
48
57
|
"""Returns any nested objects contained within this object.
|
|
49
58
|
|
|
50
59
|
Nested objects will be diff-ed recursively.
|
|
@@ -59,6 +68,25 @@ class Diffable(ABC):
|
|
|
59
68
|
"""Returns any fields on the object that can should not be diffed."""
|
|
60
69
|
return set({})
|
|
61
70
|
|
|
71
|
+
def _has_secret_fields(self, curr_depth: int) -> bool:
|
|
72
|
+
if curr_depth > MAX_RESOURCE_DEPTH:
|
|
73
|
+
raise ValidioError(
|
|
74
|
+
"BUG: max recursion depth exceeded while looking for secrets"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if self._secret_fields():
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
for nested in self._nested_objects().values():
|
|
81
|
+
if isinstance(nested, list):
|
|
82
|
+
for n in nested:
|
|
83
|
+
if n._has_secret_fields(curr_depth=curr_depth + 1):
|
|
84
|
+
return True
|
|
85
|
+
elif nested and nested._has_secret_fields(curr_depth + 1):
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
return False
|
|
89
|
+
|
|
62
90
|
def _all_fields(self) -> set[str]:
|
|
63
91
|
"""Return all fields of the resource."""
|
|
64
92
|
return {
|
|
@@ -83,6 +111,7 @@ class Diffable(ABC):
|
|
|
83
111
|
skip_fields: set[str] | None = None,
|
|
84
112
|
) -> str:
|
|
85
113
|
params = list(inits or [])
|
|
114
|
+
secret_fields = self._secret_fields()
|
|
86
115
|
|
|
87
116
|
for f in list(inspect.signature(self.__class__).parameters):
|
|
88
117
|
# If the field is already provided as an init arg then skip,
|
|
@@ -100,7 +129,7 @@ class Diffable(ABC):
|
|
|
100
129
|
indent_level=indent_level + 1,
|
|
101
130
|
import_ctx=import_ctx,
|
|
102
131
|
),
|
|
103
|
-
None,
|
|
132
|
+
SECRET_VALUE_FIXME_COMMENT if f in secret_fields else None,
|
|
104
133
|
)
|
|
105
134
|
)
|
|
106
135
|
|
|
@@ -131,6 +160,7 @@ class Diffable(ABC):
|
|
|
131
160
|
|
|
132
161
|
line_indent = "\n" + (" " * self._num_ident_spaces(indent_level + 1))
|
|
133
162
|
import_args = []
|
|
163
|
+
|
|
134
164
|
for field, arg, comment in sorted_params:
|
|
135
165
|
comment_str = "" if not comment else f" # {comment}"
|
|
136
166
|
import_args.append(f"{field}={arg},{comment_str}")
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
from validio_sdk.exception import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class ManifestConfigurationError(ValidioError):
|
|
5
|
-
"""Raised when there is an invalid configuration in the manifest."""
|
|
1
|
+
from validio_sdk.exception import ManifestConfigurationError
|
|
6
2
|
|
|
7
3
|
|
|
8
4
|
def updated_resource_type_mismatch_exception(
|
|
@@ -13,7 +13,7 @@ from camel_converter import to_camel, to_snake
|
|
|
13
13
|
import validio_sdk
|
|
14
14
|
from validio_sdk._api.api import APIClient, execute_mutation
|
|
15
15
|
from validio_sdk.client import Session
|
|
16
|
-
from validio_sdk.exception import ValidioError
|
|
16
|
+
from validio_sdk.exception import ValidioError, ValidioResourceError
|
|
17
17
|
from validio_sdk.resource._diffable import (
|
|
18
18
|
ApiSecretChangeNestedResource,
|
|
19
19
|
Diffable,
|
|
@@ -27,6 +27,7 @@ from validio_sdk.resource._serde import (
|
|
|
27
27
|
_import_value_repr,
|
|
28
28
|
without_skipped_internal_fields,
|
|
29
29
|
)
|
|
30
|
+
from validio_sdk.resource._util import _sanitized_error_str
|
|
30
31
|
|
|
31
32
|
if TYPE_CHECKING:
|
|
32
33
|
from validio_sdk.code._import import ImportContext
|
|
@@ -281,19 +282,13 @@ class Resource(Diffable):
|
|
|
281
282
|
|
|
282
283
|
def _must_id(self) -> str:
|
|
283
284
|
if self._id.value is None:
|
|
284
|
-
raise
|
|
285
|
-
f"resource {self.__class__.__name__}(name={self.name}) "
|
|
286
|
-
"has unresolved ID"
|
|
287
|
-
)
|
|
285
|
+
raise ValidioResourceError(self, "has unresolved ID")
|
|
288
286
|
|
|
289
287
|
return self._id.value
|
|
290
288
|
|
|
291
289
|
def _must_namespace(self) -> str:
|
|
292
290
|
if self._namespace is None:
|
|
293
|
-
raise
|
|
294
|
-
f"resource {self.__class__.__name__}(name={self.name}) "
|
|
295
|
-
"has unresolved namespace"
|
|
296
|
-
)
|
|
291
|
+
raise ValidioResourceError(self, "has unresolved namespace")
|
|
297
292
|
|
|
298
293
|
return self._namespace
|
|
299
294
|
|
|
@@ -365,14 +360,12 @@ class Resource(Diffable):
|
|
|
365
360
|
def _api_update_arguments(self) -> dict[str, str]:
|
|
366
361
|
return {"input": f"{self.__class__.__name__}UpdateInput!"}
|
|
367
362
|
|
|
368
|
-
def _api_delete_arguments(self) -> dict[str, str]:
|
|
369
|
-
return {"ids": f"[{self.resource_class_name()}Id!]!"}
|
|
370
|
-
|
|
371
363
|
async def _api_create(
|
|
372
364
|
self,
|
|
373
365
|
namespace: str,
|
|
374
366
|
ctx: "DiffContext",
|
|
375
367
|
session: Session,
|
|
368
|
+
show_secrets: bool,
|
|
376
369
|
) -> str:
|
|
377
370
|
"""
|
|
378
371
|
Create the resource, and resolve's the current instance with
|
|
@@ -381,14 +374,18 @@ class Resource(Diffable):
|
|
|
381
374
|
if self.has_user_defined_name() and not re.match(
|
|
382
375
|
r"^[a-z0-9_.-]{2,251}$", self.name, re.IGNORECASE
|
|
383
376
|
):
|
|
384
|
-
raise
|
|
385
|
-
|
|
386
|
-
"
|
|
377
|
+
raise ValidioResourceError(
|
|
378
|
+
self,
|
|
379
|
+
"invalid resource name, must be 2-253 "
|
|
380
|
+
"characters containing only a-z, A-Z, 0-9, _, - or .",
|
|
387
381
|
)
|
|
388
382
|
|
|
389
383
|
create_input = self._api_create_input(namespace, ctx)
|
|
390
384
|
payload = await self._api_create_or_update(
|
|
391
|
-
"create",
|
|
385
|
+
"create",
|
|
386
|
+
create_input,
|
|
387
|
+
session=session,
|
|
388
|
+
show_secrets=show_secrets,
|
|
392
389
|
)
|
|
393
390
|
|
|
394
391
|
id_ = payload["id"]
|
|
@@ -401,16 +398,23 @@ class Resource(Diffable):
|
|
|
401
398
|
namespace: str,
|
|
402
399
|
ctx: "DiffContext",
|
|
403
400
|
session: Session,
|
|
401
|
+
show_secrets: bool,
|
|
404
402
|
) -> None:
|
|
405
403
|
"""Perform api call to update the resource."""
|
|
406
404
|
update_input = self._api_update_input(namespace, ctx)
|
|
407
|
-
await self._api_create_or_update(
|
|
405
|
+
await self._api_create_or_update(
|
|
406
|
+
"update",
|
|
407
|
+
update_input,
|
|
408
|
+
session=session,
|
|
409
|
+
show_secrets=show_secrets,
|
|
410
|
+
)
|
|
408
411
|
|
|
409
412
|
async def _api_create_or_update(
|
|
410
413
|
self,
|
|
411
414
|
verb: str,
|
|
412
415
|
api_input: Any | None,
|
|
413
416
|
session: Session,
|
|
417
|
+
show_secrets: bool,
|
|
414
418
|
) -> Any:
|
|
415
419
|
if verb == "create":
|
|
416
420
|
method_name = self._api_create_method_name()
|
|
@@ -421,14 +425,23 @@ class Resource(Diffable):
|
|
|
421
425
|
|
|
422
426
|
response_field = self._api_create_response_field_name()
|
|
423
427
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
428
|
+
# We catch and re-throw any error with the resource context.
|
|
429
|
+
try:
|
|
430
|
+
response = await execute_mutation(
|
|
431
|
+
session,
|
|
432
|
+
method_name,
|
|
433
|
+
arguments,
|
|
434
|
+
api_input,
|
|
435
|
+
response_field,
|
|
436
|
+
returns="id",
|
|
437
|
+
)
|
|
438
|
+
except Exception as e:
|
|
439
|
+
if self._has_secret_fields(curr_depth=1):
|
|
440
|
+
error = _sanitized_error_str(e, show_secrets)
|
|
441
|
+
else:
|
|
442
|
+
error = str(e)
|
|
443
|
+
|
|
444
|
+
raise ValidioResourceError(self, error)
|
|
432
445
|
|
|
433
446
|
return self._check_graphql_response(
|
|
434
447
|
response=response,
|
|
@@ -444,23 +457,19 @@ class Resource(Diffable):
|
|
|
444
457
|
) -> Any:
|
|
445
458
|
errors = response.get("errors")
|
|
446
459
|
if errors:
|
|
447
|
-
raise
|
|
448
|
-
f"operation '{method_name}' failed
|
|
449
|
-
f"resource {self.__class__.__name__}(name={self.name}): "
|
|
450
|
-
f"{errors}"
|
|
460
|
+
raise ValidioResourceError(
|
|
461
|
+
self, f"operation '{method_name}' failed: {errors}"
|
|
451
462
|
)
|
|
452
463
|
|
|
453
464
|
if response_field is None:
|
|
454
465
|
return None
|
|
455
466
|
|
|
456
467
|
if response_field not in response:
|
|
457
|
-
raise
|
|
468
|
+
raise ValidioResourceError(self, f"Unexpected response: {response}")
|
|
458
469
|
|
|
459
470
|
if not response.get(response_field):
|
|
460
|
-
raise
|
|
461
|
-
f"operation '{method_name}' failed
|
|
462
|
-
f"resource {self.__class__.__name__}(name={self.name}): '"
|
|
463
|
-
"missing response body"
|
|
471
|
+
raise ValidioResourceError(
|
|
472
|
+
self, f"operation '{method_name}' failed: missing response body"
|
|
464
473
|
)
|
|
465
474
|
|
|
466
475
|
return response[response_field]
|
|
@@ -489,14 +498,6 @@ class Resource(Diffable):
|
|
|
489
498
|
"""
|
|
490
499
|
return _api_update_input_params(self)
|
|
491
500
|
|
|
492
|
-
def _api_delete_input(self) -> Any:
|
|
493
|
-
"""
|
|
494
|
-
Returns the graphql input(s) to delete this resource. Most APIs do bulk
|
|
495
|
-
deletes so we pass our own ID as a list but not all resources work this
|
|
496
|
-
way.
|
|
497
|
-
"""
|
|
498
|
-
return {"ids": [self._must_id()]}
|
|
499
|
-
|
|
500
501
|
def _import_str(
|
|
501
502
|
self,
|
|
502
503
|
indent_level: int,
|