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.
Files changed (82) hide show
  1. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/PKG-INFO +4 -2
  2. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/api-files.txt +1 -0
  3. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/_listing.py +6 -3
  4. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/constants.py +48 -22
  5. atlan_application_sdk_api-3.51.2/application_sdk/handler/_dispatch.py +227 -0
  6. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/handler/routes.py +212 -368
  7. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/version.py +1 -1
  8. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/pyproject.toml +11 -2
  9. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/.gitignore +0 -0
  10. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/README.md +0 -0
  11. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/__init__.py +0 -0
  12. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/_build_info.py +0 -0
  13. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/_context_errors.py +0 -0
  14. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/_install.py +0 -0
  15. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/_logging.py +0 -0
  16. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/_runtime/__init__.py +0 -0
  17. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/_runtime/offload.py +0 -0
  18. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/_runtime/progress.py +0 -0
  19. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/clients/__init__.py +0 -0
  20. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/clients/_interface.py +0 -0
  21. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/clients/_sql_failure_classifier.py +0 -0
  22. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/clients/base.py +0 -0
  23. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/clients/http_errors.py +0 -0
  24. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/clients/models.py +0 -0
  25. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/clients/sql.py +0 -0
  26. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/clients/sql_errors.py +0 -0
  27. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/clients/sql_typecasters.py +0 -0
  28. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/clients/ssl_utils.py +0 -0
  29. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/__init__.py +0 -0
  30. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/_generated_tree.py +0 -0
  31. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/aws_utils.py +0 -0
  32. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/aws_utils_errors.py +0 -0
  33. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/build_identity.py +0 -0
  34. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/concurrency.py +0 -0
  35. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/errors.py +0 -0
  36. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/filter_matching.py +0 -0
  37. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/logger.py +0 -0
  38. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/models.py +0 -0
  39. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/sql_filters.py +0 -0
  40. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/sql_filters_errors.py +0 -0
  41. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/task_queue.py +0 -0
  42. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/types.py +0 -0
  43. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/common/utils.py +0 -0
  44. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/contracts/__init__.py +0 -0
  45. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/contracts/base.py +0 -0
  46. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/contracts/events.py +0 -0
  47. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/contracts/types.py +0 -0
  48. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/contracts/types_errors.py +0 -0
  49. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/credentials/__init__.py +0 -0
  50. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/credentials/errors.py +0 -0
  51. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/credentials/extra.py +0 -0
  52. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/credentials/ingress.py +0 -0
  53. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/credentials/ref.py +0 -0
  54. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/credentials/spec.py +0 -0
  55. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/credentials/types.py +0 -0
  56. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/credentials/utils.py +0 -0
  57. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/errors/__init__.py +0 -0
  58. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/errors/base.py +0 -0
  59. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/errors/categories.py +0 -0
  60. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/errors/http.py +0 -0
  61. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/errors/leaves.py +0 -0
  62. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/errors/wire.py +0 -0
  63. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/handler/__init__.py +0 -0
  64. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/handler/_warmup.py +0 -0
  65. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/handler/asgi.py +0 -0
  66. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/handler/base.py +0 -0
  67. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/handler/context.py +0 -0
  68. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/handler/contracts.py +0 -0
  69. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/handler/manifest.py +0 -0
  70. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/handler/metadata_routes.py +0 -0
  71. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/handler/object_routes.py +0 -0
  72. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/handler/request_contract.py +0 -0
  73. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/handler/service_errors.py +0 -0
  74. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/server/__init__.py +0 -0
  75. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/server/fastapi/models.py +0 -0
  76. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/templates/__init__.py +0 -0
  77. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/templates/contracts/__init__.py +0 -0
  78. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/templates/contracts/base_metadata_extraction.py +0 -0
  79. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/templates/contracts/incremental_sql.py +0 -0
  80. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/templates/contracts/sql_metadata.py +0 -0
  81. {atlan_application_sdk_api-3.51.0 → atlan_application_sdk_api-3.51.2}/application_sdk/templates/contracts/sql_query.py +0 -0
  82. {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.0
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: pyatlan<12,>=11.3; extra == 'pyatlan'
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
@@ -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.get("app_version", "") or os.getenv(
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.get("commit_sha", "") or os.getenv("ATLAN_COMMIT_SHA", "")
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
- STORAGE_LOCK_WAIT_PROGRESS_SECONDS = max(
364
- 1.0,
365
- min(
366
- float(os.getenv("ATLAN_STORAGE_LOCK_WAIT_PROGRESS_SECONDS", "30")),
367
- float(os.getenv("ATLAN_MAX_NO_PROGRESS_SECONDS", "900")) / 4.0,
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
- STORE_ASSERT_ENABLED = (
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
- USE_SERVER_SIDE_CURSOR = (
664
- os.getenv("ATLAN_SQL_USE_SERVER_SIDE_CURSOR", "true").strip().lower() == "true"
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
- SERVICE_VERSION = __version__
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