atlan-application-sdk-api 3.51.0__tar.gz → 3.51.2__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.51.0 → atlan_application_sdk_api-3.51.2}/PKG-INFO +4 -2
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/api-files.txt +1 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/_listing.py +6 -3
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/constants.py +48 -22
- atlan_application_sdk_api-3.51.2/application_sdk/handler/_dispatch.py +227 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/handler/routes.py +212 -368
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/version.py +1 -1
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/pyproject.toml +11 -2
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/.gitignore +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/README.md +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/__init__.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/_build_info.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/_context_errors.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/_install.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/_logging.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/_runtime/__init__.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/_runtime/offload.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/_runtime/progress.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/clients/__init__.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/clients/_interface.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/clients/_sql_failure_classifier.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/clients/base.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/clients/http_errors.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/clients/models.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/clients/sql.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/clients/sql_errors.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/clients/sql_typecasters.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/clients/ssl_utils.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/__init__.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/_generated_tree.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/aws_utils.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/aws_utils_errors.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/build_identity.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/concurrency.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/errors.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/filter_matching.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/logger.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/models.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/sql_filters.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/sql_filters_errors.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/task_queue.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/types.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/utils.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/contracts/__init__.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/contracts/base.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/contracts/events.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/contracts/types.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/contracts/types_errors.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/credentials/__init__.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/credentials/errors.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/credentials/extra.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/credentials/ingress.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/credentials/ref.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/credentials/spec.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/credentials/types.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/credentials/utils.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/errors/__init__.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/errors/base.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/errors/categories.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/errors/http.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/errors/leaves.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/errors/wire.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/handler/__init__.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/handler/_warmup.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/handler/asgi.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/handler/base.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/handler/context.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/handler/contracts.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/handler/manifest.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/handler/metadata_routes.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/handler/object_routes.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/handler/request_contract.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/handler/service_errors.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/server/__init__.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/server/fastapi/models.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/templates/__init__.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/templates/contracts/__init__.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/templates/contracts/base_metadata_extraction.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/templates/contracts/incremental_sql.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/templates/contracts/sql_metadata.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/templates/contracts/sql_query.py +0 -0
- {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/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.51.
|
|
3
|
+
Version: 3.51.2
|
|
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
|
|
@@ -15,6 +15,7 @@ Classifier: License :: OSI Approved :: Apache Software License
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
17
|
Requires-Python: >=3.11
|
|
18
|
+
Requires-Dist: certifi>=2026.10.19
|
|
18
19
|
Requires-Dist: fastapi<0.144.0,>=0.143.0
|
|
19
20
|
Requires-Dist: httpx<0.29.0,>=0.27.0
|
|
20
21
|
Requires-Dist: orjson<4.0.0,>=3.11.6
|
|
@@ -27,7 +28,8 @@ Requires-Dist: python-dotenv<2.0.0,>=1.1.0; extra == 'aws'
|
|
|
27
28
|
Provides-Extra: pandas
|
|
28
29
|
Requires-Dist: pandas<3.0.0,>=2.2.3; extra == 'pandas'
|
|
29
30
|
Provides-Extra: pyatlan
|
|
30
|
-
Requires-Dist:
|
|
31
|
+
Requires-Dist: authlib<2,>=1.9.1; extra == 'pyatlan'
|
|
32
|
+
Requires-Dist: pyatlan<12,>=11.5.0; extra == 'pyatlan'
|
|
31
33
|
Provides-Extra: sql
|
|
32
34
|
Requires-Dist: httpx<0.29.0,>=0.27.0; extra == 'sql'
|
|
33
35
|
Requires-Dist: python-dotenv<2.0.0,>=1.1.0; extra == 'sql'
|
|
@@ -56,6 +56,7 @@ application_sdk/errors/http.py
|
|
|
56
56
|
application_sdk/errors/leaves.py
|
|
57
57
|
application_sdk/errors/wire.py
|
|
58
58
|
application_sdk/handler/__init__.py
|
|
59
|
+
application_sdk/handler/_dispatch.py
|
|
59
60
|
application_sdk/handler/_warmup.py
|
|
60
61
|
application_sdk/handler/asgi.py
|
|
61
62
|
application_sdk/handler/base.py
|
|
@@ -29,6 +29,11 @@ import sys
|
|
|
29
29
|
from collections.abc import Iterator
|
|
30
30
|
from pathlib import Path
|
|
31
31
|
|
|
32
|
+
try:
|
|
33
|
+
import fcntl
|
|
34
|
+
except ImportError: # Windows has no fcntl; only the darwin barrier below uses it
|
|
35
|
+
fcntl = None # type: ignore[assignment] # — sentinel for "no fcntl on this platform": None is not a module
|
|
36
|
+
|
|
32
37
|
# Darwin-only fcntl op: flush kernel buffer + drive cache. Value from
|
|
33
38
|
# <sys/fcntl.h>; not exported by Python's fcntl module.
|
|
34
39
|
_DARWIN_F_FULLFSYNC = 51
|
|
@@ -114,9 +119,7 @@ def _flush_directory_metadata(path: Path) -> None:
|
|
|
114
119
|
except OSError:
|
|
115
120
|
return
|
|
116
121
|
try:
|
|
117
|
-
if sys.platform == "darwin":
|
|
118
|
-
import fcntl # noqa: PLC0415
|
|
119
|
-
|
|
122
|
+
if sys.platform == "darwin" and fcntl is not None:
|
|
120
123
|
try:
|
|
121
124
|
fcntl.fcntl(fd, _DARWIN_F_FULLFSYNC)
|
|
122
125
|
return
|
{atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/constants.py
RENAMED
|
@@ -117,13 +117,22 @@ from application_sdk._build_info import ( # noqa: E402 — public here as befor
|
|
|
117
117
|
|
|
118
118
|
_BUILD_INFO = load_build_info()
|
|
119
119
|
|
|
120
|
+
|
|
121
|
+
def _load_application_version(build_info: dict[str, str]) -> str:
|
|
122
|
+
return build_info.get("app_version", "") or os.getenv(
|
|
123
|
+
"ATLAN_APPLICATION_VERSION", ""
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _load_commit_sha(build_info: dict[str, str]) -> str:
|
|
128
|
+
return build_info.get("commit_sha", "") or os.getenv("ATLAN_COMMIT_SHA", "")
|
|
129
|
+
|
|
130
|
+
|
|
120
131
|
#: Version of the app release exactly as Global Marketplace stores it
|
|
121
132
|
#: (release tag for semver apps, sha7 for CD apps). Baked value first.
|
|
122
|
-
APPLICATION_VERSION = _BUILD_INFO
|
|
123
|
-
"ATLAN_APPLICATION_VERSION", ""
|
|
124
|
-
)
|
|
133
|
+
APPLICATION_VERSION = _load_application_version(_BUILD_INFO)
|
|
125
134
|
#: Git commit the image was built from. Baked only — no deployer stamps it.
|
|
126
|
-
COMMIT_SHA = _BUILD_INFO
|
|
135
|
+
COMMIT_SHA = _load_commit_sha(_BUILD_INFO)
|
|
127
136
|
#: Release UUID from Global Marketplace
|
|
128
137
|
RELEASE_ID = os.getenv("ATLAN_RELEASE_ID", "")
|
|
129
138
|
#: Release channel (all, beta, staging, specific)
|
|
@@ -351,6 +360,7 @@ STORAGE_PROGRESS_LOG_INTERVAL_SECONDS = float(
|
|
|
351
360
|
os.getenv("ATLAN_STORAGE_PROGRESS_LOG_INTERVAL_SECONDS", "30")
|
|
352
361
|
)
|
|
353
362
|
|
|
363
|
+
|
|
354
364
|
#: Interval (seconds) between progress marks while an activity waits on a
|
|
355
365
|
#: per-destination storage lock (materialise dedupe, chunked-transfer
|
|
356
366
|
#: exclusion). Must stay under the stall watchdog's no-progress budget, or a
|
|
@@ -360,13 +370,17 @@ STORAGE_PROGRESS_LOG_INTERVAL_SECONDS = float(
|
|
|
360
370
|
#: ``max_no_progress_seconds`` below that cap is the operator's explicit
|
|
361
371
|
#: exception and is not defended here). Floored at 1s so a zero cannot turn
|
|
362
372
|
#: the wait into a busy spin. (CONNECT-1126)
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
)
|
|
373
|
+
def _load_storage_lock_wait_progress_seconds() -> float:
|
|
374
|
+
return max(
|
|
375
|
+
1.0,
|
|
376
|
+
min(
|
|
377
|
+
float(os.getenv("ATLAN_STORAGE_LOCK_WAIT_PROGRESS_SECONDS", "30")),
|
|
378
|
+
float(os.getenv("ATLAN_MAX_NO_PROGRESS_SECONDS", "900")) / 4.0,
|
|
379
|
+
),
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
STORAGE_LOCK_WAIT_PROGRESS_SECONDS = _load_storage_lock_wait_progress_seconds()
|
|
370
384
|
|
|
371
385
|
#: Multipart part size for uploads (default 8 MiB). Raise this per deployment
|
|
372
386
|
#: when the destination makes *part count* expensive rather than part size.
|
|
@@ -454,13 +468,16 @@ STORAGE_WRITE_SIDECARS = (
|
|
|
454
468
|
os.getenv("ATLAN_STORAGE_WRITE_SIDECARS", "true").strip().lower() != "false"
|
|
455
469
|
)
|
|
456
470
|
|
|
471
|
+
|
|
457
472
|
#: Opt-in for the e2e ``sdk:store-assert`` node (FND-3571). The workflow is
|
|
458
473
|
#: registered on every worker so worker start-up is identical everywhere; with
|
|
459
474
|
#: this unset the node reads nothing and reports itself disabled. Only e2e
|
|
460
475
|
#: installs set it, per tenant, through ``deploy.env_overrides``.
|
|
461
|
-
|
|
462
|
-
os.getenv("ATLAN_STORE_ASSERT_ENABLED", "false").strip().lower() == "true"
|
|
463
|
-
|
|
476
|
+
def _load_store_assert_enabled() -> bool:
|
|
477
|
+
return os.getenv("ATLAN_STORE_ASSERT_ENABLED", "false").strip().lower() == "true"
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
STORE_ASSERT_ENABLED = _load_store_assert_enabled()
|
|
464
481
|
|
|
465
482
|
#: Build ID for worker versioning (injected by TWD controller via Kubernetes Downward API).
|
|
466
483
|
#: When set, workers identify themselves with this build ID so the Temporal server can
|
|
@@ -660,9 +677,13 @@ OOM_RESTART_CHECK = _load_oom_restart_check()
|
|
|
660
677
|
#: Whether to use server-side cursors for SQL operations.
|
|
661
678
|
#: Enabled by default; set ATLAN_SQL_USE_SERVER_SIDE_CURSOR to any value other
|
|
662
679
|
#: than "true" (e.g. "false") to opt out.
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
)
|
|
680
|
+
def _load_use_server_side_cursor() -> bool:
|
|
681
|
+
return (
|
|
682
|
+
os.getenv("ATLAN_SQL_USE_SERVER_SIDE_CURSOR", "true").strip().lower() == "true"
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
USE_SERVER_SIDE_CURSOR = _load_use_server_side_cursor()
|
|
666
687
|
|
|
667
688
|
# DAPR Constants
|
|
668
689
|
#: Name of the state store component in DAPR
|
|
@@ -839,12 +860,17 @@ LOG_LEVEL = (os.getenv("ATLAN_LOG_LEVEL") or os.getenv("LOG_LEVEL", "INFO")).upp
|
|
|
839
860
|
#: Used by traces_adaptor.py and metrics_adaptor.py at module level for
|
|
840
861
|
#: tracer/meter initialization. AppConfig.service_name is the runtime equivalent.
|
|
841
862
|
SERVICE_NAME: str = os.getenv("OTEL_SERVICE_NAME", "atlan-application-sdk")
|
|
842
|
-
#: Service version for OpenTelemetry
|
|
843
|
-
SERVICE_VERSION: str = os.getenv("OTEL_SERVICE_VERSION", "")
|
|
844
|
-
if not SERVICE_VERSION:
|
|
845
|
-
from application_sdk.version import __version__
|
|
846
863
|
|
|
847
|
-
|
|
864
|
+
|
|
865
|
+
from application_sdk.version import __version__ # noqa: E402
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def _load_service_version() -> str:
|
|
869
|
+
return os.getenv("OTEL_SERVICE_VERSION", "") or __version__
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
#: Service version for OpenTelemetry
|
|
873
|
+
SERVICE_VERSION: str = _load_service_version()
|
|
848
874
|
#: Additional resource attributes for OpenTelemetry
|
|
849
875
|
OTEL_RESOURCE_ATTRIBUTES: str = os.getenv("OTEL_RESOURCE_ATTRIBUTES", "")
|
|
850
876
|
#: Endpoint for the OpenTelemetry collector
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Resolve which callable answers a handler op for an entry point.
|
|
2
|
+
|
|
3
|
+
One resolution for every surface that runs a handler op: the HTTP routes
|
|
4
|
+
(``application_sdk.handler.routes``, shared by the worker's handler service and
|
|
5
|
+
the consolidated API host) and the SDR Temporal activities
|
|
6
|
+
(``application_sdk.execution._temporal.sdr``). Before this module the SDR
|
|
7
|
+
activities called ``Handler.test_auth`` / ``Handler.fetch_metadata`` directly, so
|
|
8
|
+
a per-entrypoint ``app.<segment>.handler`` hook ran over HTTP and was silently
|
|
9
|
+
skipped over SDR.
|
|
10
|
+
|
|
11
|
+
This module is part of the api slice's core: it must not import Temporal or any
|
|
12
|
+
worker-only module, so it ships in ``atlan-application-sdk-api``.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import inspect
|
|
18
|
+
import re
|
|
19
|
+
from collections.abc import Awaitable, Callable
|
|
20
|
+
from types import ModuleType
|
|
21
|
+
from typing import Any, Literal, cast, overload
|
|
22
|
+
|
|
23
|
+
from application_sdk._logging import get_logger
|
|
24
|
+
from application_sdk.errors.leaves import InvalidInputError
|
|
25
|
+
from application_sdk.handler.base import Handler
|
|
26
|
+
from application_sdk.handler.context import HandlerContext
|
|
27
|
+
from application_sdk.handler.contracts import (
|
|
28
|
+
AuthInput,
|
|
29
|
+
AuthOutput,
|
|
30
|
+
MetadataInput,
|
|
31
|
+
MetadataOutput,
|
|
32
|
+
PreflightInput,
|
|
33
|
+
PreflightOutput,
|
|
34
|
+
WarmupInput,
|
|
35
|
+
WarmupObservation,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Named for application_sdk.handler.service: discovery moved from there (via
|
|
39
|
+
# routes.py) and keeps its logger name, so its debug line is unchanged.
|
|
40
|
+
logger = get_logger("application_sdk.handler.service")
|
|
41
|
+
|
|
42
|
+
# ``\Z``, not ``$``: ``$`` also matches before a trailing newline, so
|
|
43
|
+
# ``"export\n"`` would pass and then miss discovery instead of being refused.
|
|
44
|
+
_ENTRYPOINT_NAME_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9_-]*\Z")
|
|
45
|
+
|
|
46
|
+
HandlerFn = Callable[[Any, HandlerContext], Awaitable[Any]]
|
|
47
|
+
|
|
48
|
+
HandlerOp = Literal["test_auth", "preflight_check", "fetch_metadata", "warmup"]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def entrypoint_module_segment(name: str) -> str:
|
|
52
|
+
"""Kebab-case entry-point name → its Python module segment (``a-b`` → ``a_b``).
|
|
53
|
+
|
|
54
|
+
Same conversion as ``application_sdk.app.entrypoint.entrypoint_module_segment``.
|
|
55
|
+
"""
|
|
56
|
+
return name.replace("-", "_")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _import_optional_app_module(dotted: str) -> ModuleType | None:
|
|
60
|
+
"""Import an optional consumer-owned module by dotted path.
|
|
61
|
+
|
|
62
|
+
Returns ``None`` only when the module *itself* (or one of its parent
|
|
63
|
+
packages) is absent — the expected "this app ships no such hook" case.
|
|
64
|
+
Re-raises when the module exists but fails to import: a transitive
|
|
65
|
+
``ModuleNotFoundError`` (a missing dependency named somewhere other than the
|
|
66
|
+
target's own dotted path) or any other ``ImportError`` is a real bug in the
|
|
67
|
+
connector's code and must surface, not be silently swallowed into a
|
|
68
|
+
fall-through.
|
|
69
|
+
"""
|
|
70
|
+
import importlib # noqa: PLC0415 — cold path: per-entrypoint discovery only
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
return importlib.import_module(dotted)
|
|
74
|
+
except ModuleNotFoundError as exc: # conformance: ignore[E008] re-raises if missing dep is not the target module itself
|
|
75
|
+
missing = exc.name or ""
|
|
76
|
+
# Swallow only if what's missing is the target or one of its parents
|
|
77
|
+
# (e.g. ``app``, ``app.<segment>``, ``app.<segment>.core``).
|
|
78
|
+
if missing == dotted or dotted.startswith(f"{missing}."):
|
|
79
|
+
return None
|
|
80
|
+
raise
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _discover_handler_fn(
|
|
84
|
+
entrypoint: str, app_package: str, fn_name: str
|
|
85
|
+
) -> HandlerFn | None:
|
|
86
|
+
"""Look for a per-entrypoint handler function.
|
|
87
|
+
|
|
88
|
+
Convention: ``app.<segment>.handler.<fn_name>`` where ``segment`` is
|
|
89
|
+
:func:`~application_sdk.app.entrypoint.entrypoint_module_segment` of the
|
|
90
|
+
entry-point name and ``fn_name`` is one of ``"test_auth"``,
|
|
91
|
+
``"preflight_check"``, ``"fetch_metadata"``, ``"warmup"``. Multi-entrypoint apps that
|
|
92
|
+
need *per-entrypoint* lifecycle hooks drop a ``handler.py`` next to their
|
|
93
|
+
package's hand-written code with::
|
|
94
|
+
|
|
95
|
+
async def test_auth(input: AuthInput, ctx: HandlerContext) -> AuthOutput: ...
|
|
96
|
+
async def preflight_check(input: PreflightInput, ctx: HandlerContext) -> PreflightOutput: ...
|
|
97
|
+
async def fetch_metadata(input: MetadataInput, ctx: HandlerContext) -> MetadataOutput: ...
|
|
98
|
+
async def warmup(input: WarmupInput, ctx: HandlerContext) -> WarmupObservation: ...
|
|
99
|
+
|
|
100
|
+
The dispatch is best-effort: if the per-entrypoint module / attribute
|
|
101
|
+
is absent, the caller falls through to the app-level ``Handler`` instance
|
|
102
|
+
(``DefaultHandler`` if no custom handler is configured), preserving
|
|
103
|
+
today's single-entrypoint behavior 1:1.
|
|
104
|
+
|
|
105
|
+
Precedence & silent fall-through — important when reasoning about which
|
|
106
|
+
code actually runs (mirrored in ``docs/concepts/handlers.md``):
|
|
107
|
+
|
|
108
|
+
- When a per-entrypoint ``<fn_name>`` exists, it **pre-empts** the
|
|
109
|
+
app-level ``Handler.<fn_name>`` for that entry point. Defining both
|
|
110
|
+
silently runs the module one; the class method never executes.
|
|
111
|
+
- Resolution is per-op: a module that defines only ``fetch_metadata``
|
|
112
|
+
leaves ``test_auth`` / ``preflight_check`` falling back to the
|
|
113
|
+
app-level ``Handler`` — one entry point can be split across two files.
|
|
114
|
+
- A wrong name or a non-``async`` ``def`` does not match (see the
|
|
115
|
+
``iscoroutinefunction`` check below) and **silently** falls through
|
|
116
|
+
rather than erroring — so a typo'd hook quietly does nothing.
|
|
117
|
+
|
|
118
|
+
Returns the callable or ``None`` if absent.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
segment = entrypoint_module_segment(entrypoint)
|
|
122
|
+
module = _import_optional_app_module(f"{app_package}.{segment}.handler")
|
|
123
|
+
if module is None:
|
|
124
|
+
return None
|
|
125
|
+
fn = getattr(module, fn_name, None)
|
|
126
|
+
# The dispatch ``await``s the result, so require a coroutine function — a
|
|
127
|
+
# sync ``def`` falls through to the app-level Handler rather than blowing up
|
|
128
|
+
# with a TypeError at request time.
|
|
129
|
+
if fn is not None and not inspect.iscoroutinefunction(fn):
|
|
130
|
+
logger.debug(
|
|
131
|
+
"%s.%s.%s found but not async; falling through to app-level Handler",
|
|
132
|
+
app_package,
|
|
133
|
+
segment,
|
|
134
|
+
fn_name,
|
|
135
|
+
)
|
|
136
|
+
return cast("HandlerFn | None", fn if inspect.iscoroutinefunction(fn) else None)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@overload
|
|
140
|
+
def resolve_handler_op(
|
|
141
|
+
handler: Handler,
|
|
142
|
+
op: Literal["test_auth"],
|
|
143
|
+
entrypoint: str,
|
|
144
|
+
*,
|
|
145
|
+
app_package: str = ...,
|
|
146
|
+
) -> Callable[[AuthInput, HandlerContext], Awaitable[AuthOutput]]: ...
|
|
147
|
+
@overload
|
|
148
|
+
def resolve_handler_op(
|
|
149
|
+
handler: Handler,
|
|
150
|
+
op: Literal["preflight_check"],
|
|
151
|
+
entrypoint: str,
|
|
152
|
+
*,
|
|
153
|
+
app_package: str = ...,
|
|
154
|
+
) -> Callable[[PreflightInput, HandlerContext], Awaitable[PreflightOutput]]: ...
|
|
155
|
+
@overload
|
|
156
|
+
def resolve_handler_op(
|
|
157
|
+
handler: Handler,
|
|
158
|
+
op: Literal["fetch_metadata"],
|
|
159
|
+
entrypoint: str,
|
|
160
|
+
*,
|
|
161
|
+
app_package: str = ...,
|
|
162
|
+
) -> Callable[[MetadataInput, HandlerContext], Awaitable[MetadataOutput]]: ...
|
|
163
|
+
@overload
|
|
164
|
+
def resolve_handler_op(
|
|
165
|
+
handler: Handler,
|
|
166
|
+
op: Literal["warmup"],
|
|
167
|
+
entrypoint: str,
|
|
168
|
+
*,
|
|
169
|
+
app_package: str = ...,
|
|
170
|
+
) -> Callable[[WarmupInput, HandlerContext], Awaitable[WarmupObservation]]: ...
|
|
171
|
+
def resolve_handler_op(
|
|
172
|
+
handler: Handler,
|
|
173
|
+
op: HandlerOp,
|
|
174
|
+
entrypoint: str,
|
|
175
|
+
*,
|
|
176
|
+
app_package: str = "app",
|
|
177
|
+
) -> HandlerFn:
|
|
178
|
+
"""Return the callable that answers ``op`` for ``entrypoint``.
|
|
179
|
+
|
|
180
|
+
A per-entrypoint ``<app_package>.<segment>.handler.<op>`` hook wins (see
|
|
181
|
+
:func:`_discover_handler_fn` for the precedence rules); otherwise the
|
|
182
|
+
app-level ``handler.<op>`` answers. Both come back with the hook signature
|
|
183
|
+
``(input, ctx)`` so callers invoke them the same way; the ``Handler`` method
|
|
184
|
+
ignores ``ctx`` (it reads ``self.context``, bound by the caller).
|
|
185
|
+
|
|
186
|
+
An empty ``entrypoint`` (single-entrypoint apps, older orchestrators) goes
|
|
187
|
+
straight to ``handler``.
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
InvalidInputError: ``entrypoint`` is non-empty and not a valid entry-point
|
|
191
|
+
name. The name becomes part of a module path, so a malformed one is
|
|
192
|
+
refused rather than imported. The HTTP routes reject it with a 400
|
|
193
|
+
before calling this.
|
|
194
|
+
"""
|
|
195
|
+
if entrypoint:
|
|
196
|
+
if not _ENTRYPOINT_NAME_RE.match(entrypoint):
|
|
197
|
+
raise InvalidInputError(
|
|
198
|
+
message="Invalid entrypoint name",
|
|
199
|
+
field="entrypoint",
|
|
200
|
+
constraint=_ENTRYPOINT_NAME_RE.pattern,
|
|
201
|
+
)
|
|
202
|
+
fn = _discover_handler_fn(entrypoint, app_package, op)
|
|
203
|
+
if fn is not None:
|
|
204
|
+
return fn
|
|
205
|
+
method: Callable[[Any], Awaitable[Any]] = getattr(handler, op)
|
|
206
|
+
|
|
207
|
+
async def call_handler(input: Any, ctx: HandlerContext) -> Any:
|
|
208
|
+
return await method(input)
|
|
209
|
+
|
|
210
|
+
return call_handler
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def mirror_template_key(metadata_input: MetadataInput) -> MetadataInput:
|
|
214
|
+
"""Copy the widget routing key onto ``object_filter`` when that is empty.
|
|
215
|
+
|
|
216
|
+
The widget routing key (``metadataTemplateKey`` / ``type`` on the wire) lands
|
|
217
|
+
in its documented home, ``metadata_template_key``, via the field's
|
|
218
|
+
validation alias. Per-entrypoint hooks that read the legacy ``object_filter``
|
|
219
|
+
(e.g. asset-export-advanced's tags vs connectors vs typenames widgets) keep
|
|
220
|
+
working because it is mirrored there. New hooks can read
|
|
221
|
+
``metadata_template_key`` directly. Returns a copy; the input is not mutated.
|
|
222
|
+
"""
|
|
223
|
+
if not metadata_input.object_filter and metadata_input.metadata_template_key:
|
|
224
|
+
return metadata_input.model_copy(
|
|
225
|
+
update={"object_filter": metadata_input.metadata_template_key}
|
|
226
|
+
)
|
|
227
|
+
return metadata_input
|