atlan-application-sdk-api 3.52.0__tar.gz → 3.53.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 (83) hide show
  1. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/PKG-INFO +1 -1
  2. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/credentials/ingress.py +23 -2
  3. atlan_application_sdk_api-3.53.0/application_sdk/credentials/spec.py +523 -0
  4. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/version.py +2 -2
  5. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/pyproject.toml +1 -1
  6. atlan_application_sdk_api-3.52.0/application_sdk/credentials/spec.py +0 -182
  7. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/.gitignore +0 -0
  8. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/README.md +0 -0
  9. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/api-files.txt +0 -0
  10. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/__init__.py +0 -0
  11. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/_build_info.py +0 -0
  12. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/_context_errors.py +0 -0
  13. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/_install.py +0 -0
  14. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/_logging.py +0 -0
  15. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/_runtime/__init__.py +0 -0
  16. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/_runtime/offload.py +0 -0
  17. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/_runtime/progress.py +0 -0
  18. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/clients/__init__.py +0 -0
  19. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/clients/_interface.py +0 -0
  20. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/clients/_sql_failure_classifier.py +0 -0
  21. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/clients/base.py +0 -0
  22. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/clients/http_errors.py +0 -0
  23. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/clients/models.py +0 -0
  24. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/clients/sql.py +0 -0
  25. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/clients/sql_errors.py +0 -0
  26. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/clients/sql_typecasters.py +0 -0
  27. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/clients/ssl_utils.py +0 -0
  28. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/common/__init__.py +0 -0
  29. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/common/_generated_tree.py +0 -0
  30. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/common/_listing.py +0 -0
  31. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/common/aws_utils.py +0 -0
  32. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/common/aws_utils_errors.py +0 -0
  33. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/common/build_identity.py +0 -0
  34. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/common/concurrency.py +0 -0
  35. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/common/errors.py +0 -0
  36. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/common/filter_matching.py +0 -0
  37. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/common/logger.py +0 -0
  38. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/common/models.py +0 -0
  39. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/common/sql_filters.py +0 -0
  40. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/common/sql_filters_errors.py +0 -0
  41. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/common/task_queue.py +0 -0
  42. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/common/types.py +0 -0
  43. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/common/utils.py +0 -0
  44. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/constants.py +0 -0
  45. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/contracts/__init__.py +0 -0
  46. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/contracts/base.py +0 -0
  47. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/contracts/events.py +0 -0
  48. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/contracts/types.py +0 -0
  49. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/contracts/types_errors.py +0 -0
  50. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/credentials/__init__.py +0 -0
  51. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/credentials/errors.py +0 -0
  52. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/credentials/extra.py +0 -0
  53. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/credentials/ref.py +0 -0
  54. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/credentials/types.py +0 -0
  55. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/credentials/utils.py +0 -0
  56. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/errors/__init__.py +0 -0
  57. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/errors/base.py +0 -0
  58. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/errors/categories.py +0 -0
  59. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/errors/http.py +0 -0
  60. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/errors/leaves.py +0 -0
  61. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/errors/wire.py +0 -0
  62. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/handler/__init__.py +0 -0
  63. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/handler/_dispatch.py +0 -0
  64. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/handler/_warmup.py +0 -0
  65. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/handler/asgi.py +0 -0
  66. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/handler/base.py +0 -0
  67. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/handler/context.py +0 -0
  68. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/handler/contracts.py +0 -0
  69. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/handler/manifest.py +0 -0
  70. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/handler/metadata_routes.py +0 -0
  71. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/handler/object_routes.py +0 -0
  72. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/handler/request_contract.py +0 -0
  73. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/handler/routes.py +0 -0
  74. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/handler/service_errors.py +0 -0
  75. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/server/__init__.py +0 -0
  76. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/server/fastapi/models.py +0 -0
  77. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/templates/__init__.py +0 -0
  78. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/templates/contracts/__init__.py +0 -0
  79. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/templates/contracts/base_metadata_extraction.py +0 -0
  80. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/templates/contracts/incremental_sql.py +0 -0
  81. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/templates/contracts/sql_metadata.py +0 -0
  82. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/application_sdk/templates/contracts/sql_query.py +0 -0
  83. {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.0}/hatch_build.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: atlan-application-sdk-api
3
- Version: 3.52.0
3
+ Version: 3.53.0
4
4
  Summary: Experimental: the handler surface of the Atlan Application SDK, for the consolidated API host
5
5
  Project-URL: Homepage, https://github.com/atlanhq/application-sdk
6
6
  Project-URL: Source, https://github.com/atlanhq/application-sdk/tree/main/packages/api
@@ -40,11 +40,12 @@ from __future__ import annotations
40
40
  from collections.abc import Iterator
41
41
  from typing import Any, Final, TypeVar, get_args
42
42
 
43
+ import orjson
43
44
  from pydantic import BaseModel, ValidationError
44
45
 
45
46
  from application_sdk._logging import get_logger
46
- from application_sdk.credentials.errors import CredentialError
47
- from application_sdk.credentials.spec import AgentCredentialSpec
47
+ from application_sdk.credentials.errors import CredentialError, CredentialParseError
48
+ from application_sdk.credentials.spec import DECLARED_SOURCE_KEYS, AgentCredentialSpec
48
49
 
49
50
  logger = get_logger(__name__)
50
51
 
@@ -123,6 +124,14 @@ def normalize_agent_json(
123
124
  if isinstance(exc, ValidationError)
124
125
  else str(exc)
125
126
  )
127
+ if _declares_sources(value):
128
+ # A payload that opts into declared sources is never a placeholder:
129
+ # a malformed one is a producer bug, and reading it as "no agent
130
+ # reference" would silently route the run elsewhere.
131
+ raise CredentialParseError(
132
+ f"agent_json declares its value sources but is not a valid "
133
+ f"{spec_type.__name__}: {details}"
134
+ ) from None
126
135
  logger.debug(
127
136
  "agent_json is not a valid %s; treating the request as having no "
128
137
  "agent reference (typically a marketplace-package placeholder "
@@ -133,6 +142,18 @@ def normalize_agent_json(
133
142
  return None
134
143
 
135
144
 
145
+ def _declares_sources(value: Any) -> bool:
146
+ """Whether *value* opts into declared sources (``format`` / ``value-sources``)."""
147
+ if isinstance(value, str):
148
+ try:
149
+ value = orjson.loads(value)
150
+ except (
151
+ orjson.JSONDecodeError
152
+ ): # conformance: ignore[E002] not JSON: not a declared payload
153
+ return False
154
+ return isinstance(value, dict) and any(key in value for key in DECLARED_SOURCE_KEYS)
155
+
156
+
136
157
  def declared_agent_spec_type(model_cls: type[BaseModel]) -> type[AgentCredentialSpec]:
137
158
  """The spec class *model_cls* declares for its ``agent_json`` field.
138
159
 
@@ -0,0 +1,523 @@
1
+ """AgentCredentialSpec — typed Pydantic envelope for agent-shape credential payloads.
2
+
3
+ The base accepts arbitrary connector-specific dotted keys
4
+ (``basic.username``, ``noauth.extra.security_protocol``, …) via
5
+ ``extra="allow"``. **This permissiveness will be removed in a future
6
+ breaking release.** Connectors should subclass :class:`AgentCredentialSpec`,
7
+ declare every dotted key as an explicit field with an alias, and
8
+ override ``model_config`` with ``extra="forbid"`` so unknown keys fail
9
+ validation at parse time. See :class:`AgentCredentialSpec` for an
10
+ example.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any, cast
16
+
17
+ import orjson
18
+ from pydantic import (
19
+ BaseModel,
20
+ ConfigDict,
21
+ Field,
22
+ GetJsonSchemaHandler,
23
+ SerializerFunctionWrapHandler,
24
+ field_validator,
25
+ model_serializer,
26
+ model_validator,
27
+ )
28
+ from pydantic.json_schema import JsonSchemaValue
29
+ from pydantic_core import CoreSchema
30
+
31
+ #: ``agent_json`` wire format in which every field declares its own source
32
+ #: (FND-4361). Without it, ``key-type: single-key`` has to look every value up
33
+ #: as a secret name and guess from the store's answer.
34
+ DECLARED_SOURCES_FORMAT = 2
35
+
36
+ #: Reserved key carrying the declared ``secretKeyRef`` sources of a normalized
37
+ #: format-2 payload: ``{field: {"name": ..., "key": ...}}``. Present (even
38
+ #: empty) means every field declared its source; absent means a legacy payload.
39
+ VALUE_SOURCES_KEY = "value-sources"
40
+
41
+ _FORMAT_KEY = "format"
42
+
43
+ #: Keys whose presence means a payload opted into declared sources: the wire
44
+ #: ``format`` or the normalized round-trip ``value-sources`` (either spelling).
45
+ DECLARED_SOURCE_KEYS: tuple[str, ...] = (
46
+ _FORMAT_KEY,
47
+ VALUE_SOURCES_KEY,
48
+ "value_sources",
49
+ )
50
+
51
+ #: Keys that describe the agent rather than a credential field. Format 2 takes
52
+ #: them as plain values, of any JSON type; every other key must declare a
53
+ #: source. Includes everything the v2 form's Agent widget and the vendored
54
+ #: ``atlan-connectors-agent`` configmap send alongside the credential fields.
55
+ #: ``agent-type`` must survive as-is, because Heracles and AE route SDR runs on
56
+ #: ``agent-type == "new-app-framework"``.
57
+ _ENVELOPE_KEYS: frozenset[str] = frozenset(
58
+ {
59
+ "agent-name",
60
+ "agent-type",
61
+ "agent-custom-config",
62
+ "sdk-version",
63
+ "secret-manager",
64
+ "aws-region",
65
+ "aws-auth-method",
66
+ "aws-assume-role-arn",
67
+ "azure-auth-method",
68
+ "azure-vault-name",
69
+ "auth-type",
70
+ "connectBy",
71
+ }
72
+ )
73
+
74
+
75
+ def _declared_sources_json_schema(legacy: dict[str, Any]) -> dict[str, Any]:
76
+ """JSON schema of a format-2 payload, as published in input contracts.
77
+
78
+ Heracles validates workflow inputs against the app's input contract on
79
+ create and update, so this is what lets a format-2 ``agent_json`` be saved
80
+ at all. Built fresh per call, because pydantic may mutate what it emits.
81
+
82
+ Derived from the model's own *legacy* schema, so the published contract
83
+ accepts exactly what runtime accepts:
84
+
85
+ * a declared field's ``value`` carries that field's own type
86
+ (``port: {"value": 1521}`` but not ``{"value": "x"}``);
87
+ * only a string-typed (or untyped) field may be a ``secretKeyRef`` or a
88
+ ``fileRef``. Its flat value becomes the ref's name or path, which a
89
+ typed non-string field would reject;
90
+ * a strict (``extra="forbid"``) model publishes a closed object; an open
91
+ one accepts the envelope keys and any other source-declared field.
92
+ """
93
+
94
+ def _strict_object(required: str, schema: dict[str, Any]) -> dict[str, Any]:
95
+ return {
96
+ "type": "object",
97
+ "required": [required],
98
+ "properties": {required: schema},
99
+ "additionalProperties": False,
100
+ }
101
+
102
+ def _non_empty_strings(*names: str) -> dict[str, Any]:
103
+ return {
104
+ "type": "object",
105
+ "required": list(names),
106
+ "properties": {name: {"type": "string", "minLength": 1} for name in names},
107
+ "additionalProperties": False,
108
+ }
109
+
110
+ def _field_source(value_schema: dict[str, Any], *, refs: bool) -> dict[str, Any]:
111
+ value = _strict_object("value", value_schema)
112
+ if not refs:
113
+ return value
114
+ return {
115
+ "oneOf": [
116
+ value,
117
+ _strict_object(
118
+ "valueFrom",
119
+ {
120
+ "oneOf": [
121
+ _strict_object(
122
+ "secretKeyRef", _non_empty_strings("name", "key")
123
+ ),
124
+ _strict_object("fileRef", _non_empty_strings("path")),
125
+ ]
126
+ },
127
+ ),
128
+ ]
129
+ }
130
+
131
+ def _value_schema(prop: dict[str, Any]) -> dict[str, Any]:
132
+ # The field's own type, without what only describes the flat field.
133
+ return {k: v for k, v in prop.items() if k not in ("default", "title")}
134
+
135
+ properties: dict[str, Any] = {"format": {"const": DECLARED_SOURCES_FORMAT}}
136
+ for key, prop in legacy.get("properties", {}).items():
137
+ if key in (VALUE_SOURCES_KEY, "value_sources") or key in _LEGACY_ANCHORS:
138
+ continue
139
+ if key in _ENVELOPE_KEYS:
140
+ properties[key] = _value_schema(prop)
141
+ continue
142
+ value_schema = _value_schema(prop)
143
+ properties[key] = _field_source(
144
+ value_schema, refs=value_schema.get("type", "string") == "string"
145
+ )
146
+
147
+ closed = legacy.get("additionalProperties") is False
148
+ if not closed:
149
+ for key in _ENVELOPE_KEYS:
150
+ properties.setdefault(key, {})
151
+ return {
152
+ "title": "AgentCredentialSpec (format 2)",
153
+ "type": "object",
154
+ "required": ["format"],
155
+ "properties": properties,
156
+ "not": {"anyOf": [{"required": [key]} for key in _LEGACY_ANCHORS]},
157
+ "additionalProperties": False if closed else _field_source({}, refs=True),
158
+ }
159
+
160
+
161
+ #: Legacy fetch anchors. Format 2 replaces both with per-field sources, so a
162
+ #: payload carrying either would be ambiguous.
163
+ _LEGACY_ANCHORS: tuple[str, ...] = ("key-type", "secret-path")
164
+
165
+
166
+ class AgentCredentialSpec(BaseModel):
167
+ """Typed envelope for an agent-shape credential payload.
168
+
169
+ Covers the fields that are consistent across all connectors.
170
+ Connector-specific dotted keys (``basic.username``,
171
+ ``noauth.extra.security_protocol``, …) are currently accepted as
172
+ extras via ``extra="allow"`` and are available via
173
+ :meth:`to_raw_dict`.
174
+
175
+ .. warning::
176
+
177
+ ``extra="allow"`` will be **removed in a future breaking
178
+ release**. Connectors must migrate by subclassing this class,
179
+ declaring every dotted key they expect as an explicit field
180
+ with an alias, and setting ``extra="forbid"`` in
181
+ ``model_config``. Unknown keys then fail validation at parse
182
+ time and contract drift is caught early. The resolver flow
183
+ (:meth:`to_raw_dict` → secret-bundle substitution → flat dict)
184
+ is unchanged for any subclass.
185
+
186
+ Example — connector-specific subclass::
187
+
188
+ from pydantic import ConfigDict, Field
189
+ from application_sdk.credentials import AgentCredentialSpec
190
+
191
+ class MyAppAgentSpec(AgentCredentialSpec):
192
+ model_config = ConfigDict(
193
+ extra="forbid", frozen=True, populate_by_name=True,
194
+ )
195
+
196
+ api_token: str = Field(default="", alias="api.token")
197
+ api_endpoint: str = Field(default="", alias="api.endpoint")
198
+ extra_region: str = Field(default="", alias="extra.region")
199
+
200
+ Wire it into the extraction input by overriding the field type::
201
+
202
+ from application_sdk.templates.contracts.sql_metadata import (
203
+ ExtractionInput,
204
+ )
205
+
206
+ class MyAppExtractionInput(ExtractionInput):
207
+ agent_json: MyAppAgentSpec | None = None
208
+ """
209
+
210
+ model_config = ConfigDict(
211
+ extra="allow",
212
+ frozen=True,
213
+ populate_by_name=True,
214
+ )
215
+
216
+ # ---- Envelope fields (all optional, default to empty) ----
217
+
218
+ agent_name: str = Field(default="", alias="agent-name")
219
+ """Name of the Secure Agent instance. Used by ``is_populated()`` to
220
+ determine if this spec carries a real agent payload."""
221
+
222
+ secret_manager: str = Field(default="", alias="secret-manager")
223
+ """Secret store backend: ``awssecretmanager``, ``azurekeyvault``,
224
+ ``gcpsecretmanager``, ``kubernetes``, ``custom``, etc."""
225
+
226
+ secret_path: str = Field(default="", alias="secret-path")
227
+ """Path / ARN / name of the secret in the external secret manager."""
228
+
229
+ @field_validator("secret_path", mode="before")
230
+ @classmethod
231
+ def _strip_secret_path(cls, v: Any) -> Any:
232
+ if isinstance(v, str):
233
+ return v.strip()
234
+ return v
235
+
236
+ auth_type: str = Field(default="", alias="auth-type")
237
+ """Authentication strategy: ``basic``, ``noauth``, ``gcp-wif``,
238
+ ``iam_role``, ``jwt``, ``client_credentials``, etc."""
239
+
240
+ # ---- Common optional fields ----
241
+
242
+ host: str = ""
243
+ """Database / service hostname. Required for JDBC connectors."""
244
+
245
+ port: int = 0
246
+ """Database / service port."""
247
+
248
+ connect_by: str = Field(default="", alias="connectBy")
249
+ """Connection method hint (``host``, ``url``, etc.)."""
250
+
251
+ agent_type: str = Field(default="", alias="agent-type")
252
+ """Agent framework version. ``new-app-framework`` for SA 2.0 agents."""
253
+
254
+ key_type: str = Field(default="", alias="key-type")
255
+ """Secret key layout: ``multi-key``, ``single-key``, etc."""
256
+
257
+ aws_region: str = Field(default="", alias="aws-region")
258
+ """AWS region for the secret manager."""
259
+
260
+ aws_auth_method: str = Field(default="", alias="aws-auth-method")
261
+ """AWS auth method: ``iam``, ``iam-assume-role``, ``access-key``."""
262
+
263
+ azure_auth_method: str = Field(default="", alias="azure-auth-method")
264
+ """Azure auth method: ``managed_identity``, ``service_principal``."""
265
+
266
+ value_sources: dict[str, dict[str, str]] | None = Field(
267
+ default=None, alias=VALUE_SOURCES_KEY
268
+ )
269
+ """Declared ``secretKeyRef`` sources, ``{field: {"name", "key"}}``, of a
270
+ format-2 payload. ``None`` for a legacy payload; an empty dict when every
271
+ field is a literal or a file reference. Resolution looks up only these
272
+ fields and never probes the rest."""
273
+
274
+ # ---- Wire-format acceptance ----
275
+
276
+ @model_validator(mode="before")
277
+ @classmethod
278
+ def _accept_string_or_dict(cls, data: Any) -> Any:
279
+ """Accept a JSON string, dict, or existing spec instance.
280
+
281
+ The type layer of the field's ingress, and the only one: this is what
282
+ :func:`application_sdk.credentials.ingress.normalize_agent_json` calls,
283
+ and that function is the only lenient entry point — it turns the
284
+ exception raised here into "no agent reference". Readers take the typed
285
+ field; a reader catching a parse error is a guard that should have been
286
+ an ingress call.
287
+ """
288
+ if isinstance(data, str):
289
+ if not data or data.strip() in ("", "{}"):
290
+ return {"agent-name": ""}
291
+ try:
292
+ parsed = orjson.loads(data)
293
+ # conformance: ignore[E004] convert-and-rethrow: orjson parse failure is immediately re-raised as typed CredentialParseError
294
+ except Exception as exc:
295
+ from application_sdk.credentials.errors import ( # noqa: PLC0415 — circular: credentials/__init__.py loads sibling modules
296
+ CredentialParseError,
297
+ )
298
+
299
+ raise CredentialParseError(
300
+ f"agent_json is not valid JSON: {exc}",
301
+ cause=exc,
302
+ ) from exc
303
+ if not isinstance(parsed, dict):
304
+ from application_sdk.credentials.errors import ( # noqa: PLC0415 — circular: credentials/__init__.py loads sibling modules
305
+ CredentialParseError,
306
+ )
307
+
308
+ raise CredentialParseError(
309
+ f"agent_json must be a JSON object, got {type(parsed).__name__}",
310
+ )
311
+ data = parsed
312
+ if isinstance(data, dict):
313
+ if _FORMAT_KEY in data:
314
+ return _normalize_declared(data)
315
+ _check_value_sources(data)
316
+ return data
317
+
318
+ @model_serializer(mode="wrap")
319
+ def _omit_absent_sources(self, handler: SerializerFunctionWrapHandler) -> Any:
320
+ """Leave a legacy payload's serialized form exactly as it was: apps and
321
+ older SDKs in child workflows read it, and must not see a new key."""
322
+ data = handler(self)
323
+ if self.value_sources is None and isinstance(data, dict):
324
+ data.pop(VALUE_SOURCES_KEY, None)
325
+ data.pop("value_sources", None)
326
+ return data
327
+
328
+ @classmethod
329
+ def __get_pydantic_json_schema__(
330
+ cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
331
+ ) -> JsonSchemaValue:
332
+ # The wrap serializer above returns ``Any``, which would blank the
333
+ # serialization schema of every input model embedding this spec. It
334
+ # only omits a ``None`` field, so publish the field-derived schema.
335
+ json_schema = handler(_without_serializer(core_schema))
336
+ if handler.mode != "validation":
337
+ return json_schema
338
+ # Validation (the published input contract) accepts either wire
339
+ # format: the legacy flat shape, or format 2. Typed fields such as
340
+ # ``host: str`` and ``port: int`` would otherwise reject a format-2
341
+ # ``{"value": ...}`` before the SDK ever normalizes it.
342
+ target = handler.resolve_ref_schema(json_schema)
343
+ legacy = dict(target)
344
+ target.clear()
345
+ for key in ("title", "description"):
346
+ if key in legacy:
347
+ target[key] = legacy.pop(key)
348
+ target["anyOf"] = [legacy, _declared_sources_json_schema(legacy)]
349
+ return json_schema
350
+
351
+ # ---- Convenience methods ----
352
+
353
+ def to_raw_dict(self) -> dict[str, Any]:
354
+ """Serialize to a flat dict using original hyphenated key names.
355
+
356
+ Combines typed envelope fields (by alias) with all extra
357
+ connector-specific keys. This is the dict the resolution
358
+ pipeline operates on.
359
+ """
360
+ d = self.model_dump(by_alias=True)
361
+ # model_dump with by_alias already includes extras with their
362
+ # original keys — no extra work needed.
363
+ return d
364
+
365
+ def is_populated(self) -> bool:
366
+ """True if this spec can resolve: a name plus a fetch anchor
367
+ (secret_path, key_type "single-key", or declared value sources).
368
+ Rejects the name-only AE placeholder that would otherwise resolve to
369
+ empty credentials.
370
+ """
371
+ return bool(self.agent_name) and (
372
+ bool(self.secret_path)
373
+ or self.key_type == "single-key"
374
+ or self.value_sources is not None
375
+ )
376
+
377
+ def declares_sources(self) -> bool:
378
+ """True for a format-2 payload, whose fields each declare their source."""
379
+ return self.value_sources is not None
380
+
381
+ def __hash__(self) -> int:
382
+ # pydantic's frozen hash hashes field values, and ``value_sources`` is
383
+ # a dict. Hash the canonical dump instead: equal specs dump equally.
384
+ # Computed once: ``frozen`` stops rebinding a field, not mutating a
385
+ # nested dict, and a hash that moved would strand the spec in any set
386
+ # or dict that holds it.
387
+ cached = self.__dict__.get("_hash_cache")
388
+ if cached is None:
389
+ cached = hash(
390
+ orjson.dumps(self.model_dump(mode="json"), option=orjson.OPT_SORT_KEYS)
391
+ )
392
+ object.__setattr__(self, "_hash_cache", cached)
393
+ return cached
394
+
395
+
396
+ def _without_serializer(core_schema: CoreSchema) -> CoreSchema:
397
+ """*core_schema* minus its custom serializer, at whichever level it sits."""
398
+ schema: dict[str, Any] = {
399
+ k: v for k, v in core_schema.items() if k != "serialization"
400
+ }
401
+ inner = schema.get("schema")
402
+ if isinstance(inner, dict) and "serialization" in inner:
403
+ schema["schema"] = {k: v for k, v in inner.items() if k != "serialization"}
404
+ return cast("CoreSchema", schema)
405
+
406
+
407
+ def _parse_error(message: str) -> Exception:
408
+ from application_sdk.credentials.errors import ( # noqa: PLC0415 — circular: credentials/__init__.py loads sibling modules
409
+ CredentialParseError,
410
+ )
411
+
412
+ return CredentialParseError(message)
413
+
414
+
415
+ def _source_error(field: str, problem: str) -> Exception:
416
+ # Names the field only: field names aren't secret, their values may be.
417
+ return _parse_error(f"agent_json field '{field}' {problem}")
418
+
419
+
420
+ def _non_empty_str(value: Any) -> bool:
421
+ return isinstance(value, str) and bool(value.strip())
422
+
423
+
424
+ def _secret_key_ref(field: str, ref: Any) -> dict[str, str]:
425
+ if (
426
+ not isinstance(ref, dict)
427
+ or set(ref) != {"name", "key"}
428
+ or not _non_empty_str(ref["name"])
429
+ or not _non_empty_str(ref["key"])
430
+ ):
431
+ raise _source_error(
432
+ field, "has a 'secretKeyRef' without a non-empty 'name' and 'key'"
433
+ )
434
+ return {"name": ref["name"], "key": ref["key"]}
435
+
436
+
437
+ def _read_source(field: str, declared: Any) -> tuple[Any, dict[str, str] | None]:
438
+ """``(flat value, secretKeyRef or None)`` for one format-2 field."""
439
+ one_of = "must be an object with exactly one of 'value' or 'valueFrom'"
440
+ if not isinstance(declared, dict) or len(declared) != 1:
441
+ raise _source_error(field, one_of)
442
+ ((kind, body),) = declared.items()
443
+ if kind == "value":
444
+ return body, None
445
+ if kind != "valueFrom":
446
+ raise _source_error(field, one_of)
447
+ if not isinstance(body, dict) or len(body) != 1:
448
+ raise _source_error(
449
+ field, "must hold exactly one of 'secretKeyRef' or 'fileRef' in 'valueFrom'"
450
+ )
451
+ ((source, ref),) = body.items()
452
+ if source == "secretKeyRef":
453
+ secret_ref = _secret_key_ref(field, ref)
454
+ # The flat value is the secret's name, exactly what a legacy payload
455
+ # carried in this field, until resolution replaces it.
456
+ return secret_ref["name"], secret_ref
457
+ if source == "fileRef":
458
+ if (
459
+ not isinstance(ref, dict)
460
+ or set(ref) != {"path"}
461
+ or not _non_empty_str(ref["path"])
462
+ ):
463
+ raise _source_error(field, "has a 'fileRef' without a non-empty 'path'")
464
+ return ref["path"], None
465
+ raise _source_error(
466
+ field, "has an unsupported 'valueFrom' source; use 'secretKeyRef' or 'fileRef'"
467
+ )
468
+
469
+
470
+ def _normalize_declared(data: dict[str, Any]) -> dict[str, Any]:
471
+ """Flatten a format-2 payload into the legacy shape plus ``value-sources``.
472
+
473
+ Apps keep reading the same flat keys and types (``port`` stays an int, a
474
+ typed ``extra="forbid"`` subclass validates unchanged); only resolution
475
+ reads the declared sources.
476
+ """
477
+ fmt = data[_FORMAT_KEY]
478
+ if isinstance(fmt, bool) or fmt != DECLARED_SOURCES_FORMAT:
479
+ raise _parse_error(
480
+ f"agent_json format {fmt!r} is not supported; "
481
+ f"expected {DECLARED_SOURCES_FORMAT}"
482
+ )
483
+ flat: dict[str, Any] = {}
484
+ sources: dict[str, dict[str, str]] = {}
485
+ for key, declared in data.items():
486
+ if key == _FORMAT_KEY:
487
+ continue
488
+ if key in _LEGACY_ANCHORS:
489
+ raise _parse_error(
490
+ f"agent_json format {DECLARED_SOURCES_FORMAT} does not take "
491
+ f"'{key}': each field declares its own source"
492
+ )
493
+ if key in (VALUE_SOURCES_KEY, "value_sources"):
494
+ raise _parse_error(f"agent_json key '{key}' is reserved")
495
+ if key in _ENVELOPE_KEYS:
496
+ flat[key] = declared
497
+ continue
498
+ flat[key], secret_ref = _read_source(key, declared)
499
+ if secret_ref is not None:
500
+ sources[key] = secret_ref
501
+ flat[VALUE_SOURCES_KEY] = sources
502
+ return flat
503
+
504
+
505
+ def _check_value_sources(data: dict[str, Any]) -> None:
506
+ """Validate the ``value-sources`` of an already-normalized payload.
507
+
508
+ That is the round-trip shape (``to_raw_dict``, a serialized workflow
509
+ input) that a later activity or child workflow re-parses.
510
+ """
511
+ for key in (VALUE_SOURCES_KEY, "value_sources"):
512
+ if key not in data:
513
+ continue
514
+ sources = data[key]
515
+ # A present-but-null marker is rejected, not read as "legacy": that
516
+ # would silently re-enable probing. The SDK never serializes a null
517
+ # marker (it omits an absent one), so only a hand-built payload has it.
518
+ if not isinstance(sources, dict):
519
+ raise _parse_error(f"agent_json '{key}' must be an object")
520
+ for field, ref in sources.items():
521
+ _secret_key_ref(field, ref)
522
+ if not isinstance(data.get(field), str):
523
+ raise _source_error(field, f"is named in '{key}' but carries no value")
@@ -36,5 +36,5 @@ only to reconcile ahead of a release — and deliberately, since older Dapr
36
36
  releases drop off the CDN.
37
37
  """
38
38
 
39
- __version__ = "3.52.0"
40
- __dapr_version: str = "1.18.7"
39
+ __version__ = "3.53.0"
40
+ __dapr_version: str = "1.18.9"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "atlan-application-sdk-api"
3
- version = "3.52.0"
3
+ version = "3.53.0"
4
4
  description = "Experimental: the handler surface of the Atlan Application SDK, for the consolidated API host"
5
5
  license = "Apache-2.0"
6
6
  authors = [
@@ -1,182 +0,0 @@
1
- """AgentCredentialSpec — typed Pydantic envelope for agent-shape credential payloads.
2
-
3
- The base accepts arbitrary connector-specific dotted keys
4
- (``basic.username``, ``noauth.extra.security_protocol``, …) via
5
- ``extra="allow"``. **This permissiveness will be removed in a future
6
- breaking release.** Connectors should subclass :class:`AgentCredentialSpec`,
7
- declare every dotted key as an explicit field with an alias, and
8
- override ``model_config`` with ``extra="forbid"`` so unknown keys fail
9
- validation at parse time. See :class:`AgentCredentialSpec` for an
10
- example.
11
- """
12
-
13
- from __future__ import annotations
14
-
15
- from typing import Any
16
-
17
- import orjson
18
- from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
19
-
20
-
21
- class AgentCredentialSpec(BaseModel):
22
- """Typed envelope for an agent-shape credential payload.
23
-
24
- Covers the fields that are consistent across all connectors.
25
- Connector-specific dotted keys (``basic.username``,
26
- ``noauth.extra.security_protocol``, …) are currently accepted as
27
- extras via ``extra="allow"`` and are available via
28
- :meth:`to_raw_dict`.
29
-
30
- .. warning::
31
-
32
- ``extra="allow"`` will be **removed in a future breaking
33
- release**. Connectors must migrate by subclassing this class,
34
- declaring every dotted key they expect as an explicit field
35
- with an alias, and setting ``extra="forbid"`` in
36
- ``model_config``. Unknown keys then fail validation at parse
37
- time and contract drift is caught early. The resolver flow
38
- (:meth:`to_raw_dict` → secret-bundle substitution → flat dict)
39
- is unchanged for any subclass.
40
-
41
- Example — connector-specific subclass::
42
-
43
- from pydantic import ConfigDict, Field
44
- from application_sdk.credentials import AgentCredentialSpec
45
-
46
- class MyAppAgentSpec(AgentCredentialSpec):
47
- model_config = ConfigDict(
48
- extra="forbid", frozen=True, populate_by_name=True,
49
- )
50
-
51
- api_token: str = Field(default="", alias="api.token")
52
- api_endpoint: str = Field(default="", alias="api.endpoint")
53
- extra_region: str = Field(default="", alias="extra.region")
54
-
55
- Wire it into the extraction input by overriding the field type::
56
-
57
- from application_sdk.templates.contracts.sql_metadata import (
58
- ExtractionInput,
59
- )
60
-
61
- class MyAppExtractionInput(ExtractionInput):
62
- agent_json: MyAppAgentSpec | None = None
63
- """
64
-
65
- model_config = ConfigDict(
66
- extra="allow",
67
- frozen=True,
68
- populate_by_name=True,
69
- )
70
-
71
- # ---- Envelope fields (all optional, default to empty) ----
72
-
73
- agent_name: str = Field(default="", alias="agent-name")
74
- """Name of the Secure Agent instance. Used by ``is_populated()`` to
75
- determine if this spec carries a real agent payload."""
76
-
77
- secret_manager: str = Field(default="", alias="secret-manager")
78
- """Secret store backend: ``awssecretmanager``, ``azurekeyvault``,
79
- ``gcpsecretmanager``, ``kubernetes``, ``custom``, etc."""
80
-
81
- secret_path: str = Field(default="", alias="secret-path")
82
- """Path / ARN / name of the secret in the external secret manager."""
83
-
84
- @field_validator("secret_path", mode="before")
85
- @classmethod
86
- def _strip_secret_path(cls, v: Any) -> Any:
87
- if isinstance(v, str):
88
- return v.strip()
89
- return v
90
-
91
- auth_type: str = Field(default="", alias="auth-type")
92
- """Authentication strategy: ``basic``, ``noauth``, ``gcp-wif``,
93
- ``iam_role``, ``jwt``, ``client_credentials``, etc."""
94
-
95
- # ---- Common optional fields ----
96
-
97
- host: str = ""
98
- """Database / service hostname. Required for JDBC connectors."""
99
-
100
- port: int = 0
101
- """Database / service port."""
102
-
103
- connect_by: str = Field(default="", alias="connectBy")
104
- """Connection method hint (``host``, ``url``, etc.)."""
105
-
106
- agent_type: str = Field(default="", alias="agent-type")
107
- """Agent framework version. ``new-app-framework`` for SA 2.0 agents."""
108
-
109
- key_type: str = Field(default="", alias="key-type")
110
- """Secret key layout: ``multi-key``, ``single-key``, etc."""
111
-
112
- aws_region: str = Field(default="", alias="aws-region")
113
- """AWS region for the secret manager."""
114
-
115
- aws_auth_method: str = Field(default="", alias="aws-auth-method")
116
- """AWS auth method: ``iam``, ``iam-assume-role``, ``access-key``."""
117
-
118
- azure_auth_method: str = Field(default="", alias="azure-auth-method")
119
- """Azure auth method: ``managed_identity``, ``service_principal``."""
120
-
121
- # ---- Wire-format acceptance ----
122
-
123
- @model_validator(mode="before")
124
- @classmethod
125
- def _accept_string_or_dict(cls, data: Any) -> Any:
126
- """Accept a JSON string, dict, or existing spec instance.
127
-
128
- The type layer of the field's ingress, and the only one: this is what
129
- :func:`application_sdk.credentials.ingress.normalize_agent_json` calls,
130
- and that function is the only lenient entry point — it turns the
131
- exception raised here into "no agent reference". Readers take the typed
132
- field; a reader catching a parse error is a guard that should have been
133
- an ingress call.
134
- """
135
- if isinstance(data, str):
136
- if not data or data.strip() in ("", "{}"):
137
- return {"agent-name": ""}
138
- try:
139
- parsed = orjson.loads(data)
140
- # conformance: ignore[E004] convert-and-rethrow: orjson parse failure is immediately re-raised as typed CredentialParseError
141
- except Exception as exc:
142
- from application_sdk.credentials.errors import ( # noqa: PLC0415 — circular: credentials/__init__.py loads sibling modules
143
- CredentialParseError,
144
- )
145
-
146
- raise CredentialParseError(
147
- f"agent_json is not valid JSON: {exc}",
148
- cause=exc,
149
- ) from exc
150
- if not isinstance(parsed, dict):
151
- from application_sdk.credentials.errors import ( # noqa: PLC0415 — circular: credentials/__init__.py loads sibling modules
152
- CredentialParseError,
153
- )
154
-
155
- raise CredentialParseError(
156
- f"agent_json must be a JSON object, got {type(parsed).__name__}",
157
- )
158
- return parsed
159
- return data
160
-
161
- # ---- Convenience methods ----
162
-
163
- def to_raw_dict(self) -> dict[str, Any]:
164
- """Serialize to a flat dict using original hyphenated key names.
165
-
166
- Combines typed envelope fields (by alias) with all extra
167
- connector-specific keys. This is the dict the resolution
168
- pipeline operates on.
169
- """
170
- d = self.model_dump(by_alias=True)
171
- # model_dump with by_alias already includes extras with their
172
- # original keys — no extra work needed.
173
- return d
174
-
175
- def is_populated(self) -> bool:
176
- """True if this spec can resolve: a name plus a fetch anchor
177
- (secret_path, or key_type "single-key"). Rejects the name-only AE
178
- placeholder that would otherwise resolve to empty credentials.
179
- """
180
- return bool(self.agent_name) and (
181
- bool(self.secret_path) or self.key_type == "single-key"
182
- )