atlan-application-sdk-api 3.52.0__tar.gz → 3.53.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.
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/PKG-INFO +1 -1
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/credentials/ingress.py +23 -2
- atlan_application_sdk_api-3.53.1/application_sdk/credentials/spec.py +523 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/errors/__init__.py +2 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/handler/metadata_routes.py +0 -3
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/version.py +2 -2
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/pyproject.toml +1 -1
- atlan_application_sdk_api-3.52.0/application_sdk/credentials/spec.py +0 -182
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/.gitignore +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/README.md +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/api-files.txt +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/__init__.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/_build_info.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/_context_errors.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/_install.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/_logging.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/_runtime/__init__.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/_runtime/offload.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/_runtime/progress.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/clients/__init__.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/clients/_interface.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/clients/_sql_failure_classifier.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/clients/base.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/clients/http_errors.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/clients/models.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/clients/sql.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/clients/sql_errors.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/clients/sql_typecasters.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/clients/ssl_utils.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/common/__init__.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/common/_generated_tree.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/common/_listing.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/common/aws_utils.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/common/aws_utils_errors.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/common/build_identity.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/common/concurrency.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/common/errors.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/common/filter_matching.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/common/logger.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/common/models.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/common/sql_filters.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/common/sql_filters_errors.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/common/task_queue.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/common/types.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/common/utils.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/constants.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/contracts/__init__.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/contracts/base.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/contracts/events.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/contracts/types.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/contracts/types_errors.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/credentials/__init__.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/credentials/errors.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/credentials/extra.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/credentials/ref.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/credentials/types.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/credentials/utils.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/errors/base.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/errors/categories.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/errors/http.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/errors/leaves.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/errors/wire.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/handler/__init__.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/handler/_dispatch.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/handler/_warmup.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/handler/asgi.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/handler/base.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/handler/context.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/handler/contracts.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/handler/manifest.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/handler/object_routes.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/handler/request_contract.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/handler/routes.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/handler/service_errors.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/server/__init__.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/server/fastapi/models.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/templates/__init__.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/templates/contracts/__init__.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/templates/contracts/base_metadata_extraction.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/templates/contracts/incremental_sql.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/templates/contracts/sql_metadata.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/templates/contracts/sql_query.py +0 -0
- {atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/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.
|
|
3
|
+
Version: 3.53.1
|
|
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")
|
|
@@ -109,6 +109,7 @@ STORAGE_HANDOFF_INCOMPLETE = ErrorCode("STR", 9)
|
|
|
109
109
|
STORAGE_GATEWAY_AUTH = ErrorCode("STR", 10)
|
|
110
110
|
STORAGE_DISK_FULL = ErrorCode("STR", 11)
|
|
111
111
|
STORAGE_GATEWAY_NO_ROUTE = ErrorCode("STR", 12)
|
|
112
|
+
STORAGE_AUTH = ErrorCode("STR", 13)
|
|
112
113
|
|
|
113
114
|
# CTR - Contract errors
|
|
114
115
|
CONTRACT_VALIDATION = ErrorCode("CTR", 1)
|
|
@@ -197,6 +198,7 @@ __all__ = [
|
|
|
197
198
|
"STORAGE_GATEWAY_AUTH",
|
|
198
199
|
"STORAGE_DISK_FULL",
|
|
199
200
|
"STORAGE_GATEWAY_NO_ROUTE",
|
|
201
|
+
"STORAGE_AUTH",
|
|
200
202
|
"CONTRACT_VALIDATION",
|
|
201
203
|
"PAYLOAD_SAFETY",
|
|
202
204
|
"HANDLER_ERROR",
|
|
@@ -404,7 +404,6 @@ def register_metadata_routes(app: FastAPI, source: MetadataSource) -> None:
|
|
|
404
404
|
if len(candidates) > 1 and not names_entrypoint(
|
|
405
405
|
target.stem, ep_name
|
|
406
406
|
):
|
|
407
|
-
# conformance: ignore[L009] logs caller-invisible context (the rejected candidates) that no HTTP response carries.
|
|
408
407
|
log.warning(
|
|
409
408
|
"ConfigMap form chosen alphabetically for entrypoint "
|
|
410
409
|
"%s: %d generated files are eligible and none is "
|
|
@@ -442,7 +441,6 @@ def register_metadata_routes(app: FastAPI, source: MetadataSource) -> None:
|
|
|
442
441
|
)
|
|
443
442
|
)
|
|
444
443
|
|
|
445
|
-
# conformance: ignore[L009] logs caller-invisible context (the available configmaps) that the generic 404 HTTPException detail does not carry.
|
|
446
444
|
log.warning(
|
|
447
445
|
"ConfigMap not found: requested=%s available=%s",
|
|
448
446
|
config_map_id,
|
|
@@ -587,7 +585,6 @@ def register_metadata_routes(app: FastAPI, source: MetadataSource) -> None:
|
|
|
587
585
|
):
|
|
588
586
|
computed = await compute(manifest_dict, fe_inputs_decoded)
|
|
589
587
|
if not isinstance(computed, dict):
|
|
590
|
-
# conformance: ignore[L009] logs caller-invisible context (the actual returned type) that the generic 500 HTTPException detail omits.
|
|
591
588
|
log.error(
|
|
592
589
|
"compute_manifest for entrypoint %r returned %s, expected dict",
|
|
593
590
|
entrypoint_name,
|
|
@@ -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
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/__init__.py
RENAMED
|
File without changes
|
{atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/_build_info.py
RENAMED
|
File without changes
|
|
File without changes
|
{atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/_install.py
RENAMED
|
File without changes
|
{atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/_logging.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/clients/sql.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/constants.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/errors/base.py
RENAMED
|
File without changes
|
|
File without changes
|
{atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/errors/http.py
RENAMED
|
File without changes
|
|
File without changes
|
{atlan_application_sdk_api-3.52.0 → atlan_application_sdk_api-3.53.1}/application_sdk/errors/wire.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|