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.
Files changed (55) hide show
  1. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/PKG-INFO +1 -1
  2. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/pyproject.toml +1 -1
  3. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/_api/api.py +37 -0
  4. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/code/plan.py +0 -2
  5. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/exception.py +42 -7
  6. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/_diff.py +24 -102
  7. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/_diffable.py +33 -3
  8. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/_errors.py +1 -5
  9. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/_resource.py +43 -42
  10. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/_serde.py +6 -1
  11. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/_server_resources.py +83 -14
  12. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/_update_namespace.py +4 -5
  13. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/_util.py +13 -9
  14. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/channels.py +8 -9
  15. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/credentials.py +319 -10
  16. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/filters.py +4 -7
  17. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/notification_rules.py +0 -6
  18. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/segmentations.py +5 -2
  19. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/sources.py +77 -3
  20. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/tests/test__diff.py +0 -151
  21. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/tests/test__resource.py +2 -0
  22. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/validators.py +49 -66
  23. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/windows.py +26 -1
  24. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/LICENSE +0 -0
  25. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/README_PUBLIC.md +0 -0
  26. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/__init__.py +0 -0
  27. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/_api/__init__.py +0 -0
  28. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/client/__init__.py +0 -0
  29. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/client/client.py +0 -0
  30. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/code/__init__.py +0 -0
  31. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/code/_import.py +0 -0
  32. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/code/_progress.py +0 -0
  33. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/code/apply.py +0 -0
  34. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/code/scaffold.py +0 -0
  35. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/code/settings.py +0 -0
  36. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/config.py +0 -0
  37. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/dbt.py +0 -0
  38. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/metadata.py +0 -0
  39. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/py.typed +0 -0
  40. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/__init__.py +0 -0
  41. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/_diff_util.py +0 -0
  42. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/_resource_graph.py +0 -0
  43. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/enums.py +0 -0
  44. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/replacement.py +0 -0
  45. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/tags.py +0 -0
  46. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/tests/__init__.py +0 -0
  47. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/tests/assets/example_manifest.json +0 -0
  48. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/tests/assets/expected_trimmed_manifest.json +0 -0
  49. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/tests/test__dbt.py +0 -0
  50. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/tests/test__plan.py +0 -0
  51. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/tests/test__sql_validation.py +0 -0
  52. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/tests/test_import.py +0 -0
  53. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/resource/thresholds.py +0 -0
  54. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/scalars.py +0 -0
  55. {validio_sdk-7.0.0 → validio_sdk-7.3.1}/validio_sdk/util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: validio-sdk
3
- Version: 7.0.0
3
+ Version: 7.3.1
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.0.0"
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
- elif isinstance(e, TransportServerError):
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
- elif isinstance(e, TimeoutError):
187
+ if isinstance(e, TimeoutError):
152
188
  raise ValidioTimeoutError(timeout=timeout)
153
189
 
154
- else:
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 ValidioBugError, ValidioError
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 CREATE_ONLY_RESOURCES, DiffContext, Resource
32
- from validio_sdk.resource._util import SourceSchemaReinference, _sanitize_error
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 ValidioBugError(
372
- f"unable to cascade update {resource_type} '{resource_name}': "
373
- f"missing manifest or server object"
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
- g = GraphDiff(
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(manifest_object, curr_depth=1):
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 e:
1028
- raise _sanitize_error(e, show_secrets)
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 ValidioError(
1032
- f"failed to check for changed secrets for resource {resource.name}: "
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 ManifestConfigurationError(
1062
- f"resource {server_resource.__class__.__name__} '{server_resource.name}' "
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, Optional
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, Optional["Diffable | list[Diffable]"] | None]:
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 ValidioError
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 ValidioError(
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 ValidioError(
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 ValidioError(
385
- f"invalid resource name '{self.name}': must be 2-253 "
386
- "characters containing only a-z, A-Z, 0-9, _, - or ."
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", create_input, session=session
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("update", update_input, session=session)
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
- response = await execute_mutation(
425
- session,
426
- method_name,
427
- arguments,
428
- api_input,
429
- response_field,
430
- returns="id",
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 ValidioError(
448
- f"operation '{method_name}' failed for "
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 Exception(f"Unexpected response: {response}")
468
+ raise ValidioResourceError(self, f"Unexpected response: {response}")
458
469
 
459
470
  if not response.get(response_field):
460
- raise ValidioError(
461
- f"operation '{method_name}' failed for '"
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,