atlan-application-sdk-api 3.51.0__py3-none-any.whl
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.
- application_sdk/__init__.py +7 -0
- application_sdk/_build_info.py +62 -0
- application_sdk/_context_errors.py +65 -0
- application_sdk/_install.py +130 -0
- application_sdk/_logging.py +171 -0
- application_sdk/_runtime/__init__.py +29 -0
- application_sdk/_runtime/offload.py +1020 -0
- application_sdk/_runtime/progress.py +757 -0
- application_sdk/clients/__init__.py +63 -0
- application_sdk/clients/_interface.py +58 -0
- application_sdk/clients/_sql_failure_classifier.py +112 -0
- application_sdk/clients/base.py +565 -0
- application_sdk/clients/http_errors.py +28 -0
- application_sdk/clients/models.py +49 -0
- application_sdk/clients/sql.py +1218 -0
- application_sdk/clients/sql_errors.py +192 -0
- application_sdk/clients/sql_typecasters.py +431 -0
- application_sdk/clients/ssl_utils.py +247 -0
- application_sdk/common/__init__.py +53 -0
- application_sdk/common/_generated_tree.py +321 -0
- application_sdk/common/_listing.py +192 -0
- application_sdk/common/aws_utils.py +522 -0
- application_sdk/common/aws_utils_errors.py +88 -0
- application_sdk/common/build_identity.py +120 -0
- application_sdk/common/concurrency.py +45 -0
- application_sdk/common/errors.py +99 -0
- application_sdk/common/filter_matching.py +261 -0
- application_sdk/common/logger.py +11 -0
- application_sdk/common/models.py +31 -0
- application_sdk/common/sql_filters.py +719 -0
- application_sdk/common/sql_filters_errors.py +17 -0
- application_sdk/common/task_queue.py +431 -0
- application_sdk/common/types.py +51 -0
- application_sdk/common/utils.py +87 -0
- application_sdk/constants.py +1120 -0
- application_sdk/contracts/__init__.py +158 -0
- application_sdk/contracts/base.py +1178 -0
- application_sdk/contracts/events.py +251 -0
- application_sdk/contracts/types.py +801 -0
- application_sdk/contracts/types_errors.py +17 -0
- application_sdk/credentials/__init__.py +326 -0
- application_sdk/credentials/errors.py +226 -0
- application_sdk/credentials/extra.py +114 -0
- application_sdk/credentials/ingress.py +283 -0
- application_sdk/credentials/ref.py +376 -0
- application_sdk/credentials/spec.py +182 -0
- application_sdk/credentials/types.py +339 -0
- application_sdk/credentials/utils.py +257 -0
- application_sdk/errors/__init__.py +224 -0
- application_sdk/errors/base.py +504 -0
- application_sdk/errors/categories.py +118 -0
- application_sdk/errors/http.py +81 -0
- application_sdk/errors/leaves.py +526 -0
- application_sdk/errors/wire.py +124 -0
- application_sdk/handler/__init__.py +97 -0
- application_sdk/handler/_warmup.py +198 -0
- application_sdk/handler/asgi.py +252 -0
- application_sdk/handler/base.py +281 -0
- application_sdk/handler/context.py +212 -0
- application_sdk/handler/contracts.py +1166 -0
- application_sdk/handler/manifest.py +81 -0
- application_sdk/handler/metadata_routes.py +699 -0
- application_sdk/handler/object_routes.py +418 -0
- application_sdk/handler/request_contract.py +91 -0
- application_sdk/handler/routes.py +1118 -0
- application_sdk/handler/service_errors.py +37 -0
- application_sdk/server/__init__.py +15 -0
- application_sdk/server/fastapi/models.py +338 -0
- application_sdk/templates/__init__.py +52 -0
- application_sdk/templates/contracts/__init__.py +111 -0
- application_sdk/templates/contracts/base_metadata_extraction.py +32 -0
- application_sdk/templates/contracts/incremental_sql.py +563 -0
- application_sdk/templates/contracts/sql_metadata.py +824 -0
- application_sdk/templates/contracts/sql_query.py +105 -0
- application_sdk/version.py +40 -0
- atlan_application_sdk_api-3.51.0.dist-info/METADATA +230 -0
- atlan_application_sdk_api-3.51.0.dist-info/RECORD +78 -0
- atlan_application_sdk_api-3.51.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""The build-identity file reader, with no dependencies beyond the stdlib.
|
|
2
|
+
|
|
3
|
+
:mod:`application_sdk.constants` exposes it as ``load_build_info``, as before.
|
|
4
|
+
It lives here so :mod:`application_sdk.common.build_identity` can read the file
|
|
5
|
+
without importing ``constants``, whose module-level imports the consolidated
|
|
6
|
+
API host's core install does not carry.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
__all__ = ["load_build_info"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_build_info(path: str | None = None) -> dict[str, str]:
|
|
18
|
+
"""Read the build-identity file the reusable CI bakes into the image.
|
|
19
|
+
|
|
20
|
+
``build-and-publish-app.yaml`` writes ``app/atlan_build.json`` into the
|
|
21
|
+
build context and the template Dockerfile's ``COPY app/ app/`` carries it
|
|
22
|
+
into the image. Keys: ``app_version`` (the exact string Global Marketplace
|
|
23
|
+
stores as ``versions.version``), ``commit_sha``, ``build_id`` (the immutable
|
|
24
|
+
image tag, the same value ``ATLAN_BUILD_ID`` carries on the e2e path),
|
|
25
|
+
``image``, ``built_at``.
|
|
26
|
+
|
|
27
|
+
Missing or malformed file → ``{}``; callers fall back to the env vars.
|
|
28
|
+
``ATLAN_BUILD_INFO_PATH`` overrides the lookup for non-template layouts.
|
|
29
|
+
|
|
30
|
+
Public because :mod:`application_sdk.app.build_identity` reads the same file
|
|
31
|
+
through it. A second parser there would be a second place for the key names
|
|
32
|
+
to drift from what CI writes.
|
|
33
|
+
"""
|
|
34
|
+
if path:
|
|
35
|
+
candidates = [path]
|
|
36
|
+
else:
|
|
37
|
+
override = os.getenv("ATLAN_BUILD_INFO_PATH", "")
|
|
38
|
+
generated_dir = os.environ.get("ATLAN_CONTRACT_GENERATED_DIR", "app/generated")
|
|
39
|
+
candidates = (
|
|
40
|
+
[override]
|
|
41
|
+
if override
|
|
42
|
+
else [
|
|
43
|
+
"app/atlan_build.json",
|
|
44
|
+
os.path.join(
|
|
45
|
+
os.path.dirname(generated_dir.rstrip("/")) or ".",
|
|
46
|
+
"atlan_build.json",
|
|
47
|
+
),
|
|
48
|
+
]
|
|
49
|
+
)
|
|
50
|
+
for candidate in candidates:
|
|
51
|
+
try:
|
|
52
|
+
with open(candidate, encoding="utf-8") as fh:
|
|
53
|
+
data = json.load(fh)
|
|
54
|
+
except (OSError, ValueError):
|
|
55
|
+
continue
|
|
56
|
+
if isinstance(data, dict):
|
|
57
|
+
return {
|
|
58
|
+
str(k): str(v)
|
|
59
|
+
for k, v in data.items()
|
|
60
|
+
if isinstance(v, (str, int, float)) and not isinstance(v, bool)
|
|
61
|
+
}
|
|
62
|
+
return {}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Context errors raised by code that ships in the api distribution.
|
|
2
|
+
|
|
3
|
+
Defined here, not in ``application_sdk.app``, so the handler surface can raise
|
|
4
|
+
them on an api-only install. ``application_sdk.app.base`` and
|
|
5
|
+
``application_sdk.app.base_errors`` re-export them under their usual names.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import ClassVar
|
|
12
|
+
|
|
13
|
+
from application_sdk.errors import APP_CONTEXT_ERROR, ErrorCode
|
|
14
|
+
from application_sdk.errors.leaves import InternalError as _InternalError
|
|
15
|
+
from application_sdk.errors.leaves import PreconditionError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(kw_only=True)
|
|
19
|
+
class ObjectStoreNotConfiguredError(PreconditionError):
|
|
20
|
+
"""Object store required by a @task but not configured in the deployment."""
|
|
21
|
+
|
|
22
|
+
code: ClassVar[str] = "PRECONDITION_OBJECT_STORE_NOT_CONFIGURED"
|
|
23
|
+
message: str = (
|
|
24
|
+
"No object store configured. "
|
|
25
|
+
"Ensure the deployment has a storage binding or APP_STORAGE_ROOT set."
|
|
26
|
+
)
|
|
27
|
+
resource: str | None = "object_store"
|
|
28
|
+
expected_state: str | None = "configured"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(kw_only=True)
|
|
32
|
+
class SecretStoreNotConfiguredError(PreconditionError):
|
|
33
|
+
"""Secret store required by get_secret / resolve_credential but not configured."""
|
|
34
|
+
|
|
35
|
+
code: ClassVar[str] = "PRECONDITION_SECRET_STORE_NOT_CONFIGURED"
|
|
36
|
+
message: str = "No secret store configured"
|
|
37
|
+
resource: str | None = "secret_store"
|
|
38
|
+
expected_state: str | None = "configured"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AppContextError(_InternalError):
|
|
42
|
+
"""Raised when App or task context is accessed outside of valid execution scope.
|
|
43
|
+
|
|
44
|
+
This is a programming error — it indicates that context-dependent methods
|
|
45
|
+
(e.g. ``self.context``, ``self.heartbeat()``) were called outside of a
|
|
46
|
+
workflow run or @task execution.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
DEFAULT_ERROR_CODE: ClassVar[ErrorCode] = APP_CONTEXT_ERROR
|
|
50
|
+
code: ClassVar[str] = "INTERNAL_APP_CONTEXT"
|
|
51
|
+
|
|
52
|
+
def __init__(self, message: str, *, error_code: ErrorCode | None = None) -> None:
|
|
53
|
+
_InternalError.__init__(self, message=message)
|
|
54
|
+
self._legacy_error_code = error_code
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def error_code(self) -> ErrorCode:
|
|
58
|
+
return (
|
|
59
|
+
self._legacy_error_code
|
|
60
|
+
if self._legacy_error_code is not None
|
|
61
|
+
else self.DEFAULT_ERROR_CODE
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def __str__(self) -> str:
|
|
65
|
+
return f"[{self.error_code.code}] {self.message}"
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Which distribution is installed: the full SDK, or only the api subset.
|
|
2
|
+
|
|
3
|
+
``atlan-application-sdk-api`` ships a listed subset of ``application_sdk/`` (see
|
|
4
|
+
``packages/api/api-files.txt``) for the consolidated API host. A few listed
|
|
5
|
+
files reach worker-only code lazily (the object store, Temporal, the worker's
|
|
6
|
+
infrastructure). Each such import is wrapped so that, on an api-only install,
|
|
7
|
+
it takes a stated fallback instead of raising at call time — and so that, with
|
|
8
|
+
the full SDK installed, a missing module is still a real error and still raises.
|
|
9
|
+
:func:`api_only_install` is the one test that tells the two apart.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import functools
|
|
15
|
+
import importlib
|
|
16
|
+
import importlib.util
|
|
17
|
+
from collections.abc import Collection, Mapping, MutableMapping
|
|
18
|
+
from types import ModuleType
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
#: A module the full SDK always ships and the api distribution never does.
|
|
22
|
+
_WORKER_SENTINEL = "application_sdk.main"
|
|
23
|
+
|
|
24
|
+
#: The modules the :func:`worker_only_missing` guards import, none of them shipped
|
|
25
|
+
#: by the api distribution: the object store (``contracts.types``,
|
|
26
|
+
#: ``credentials.utils``, ``common.utils``, ``observability.observability``), the
|
|
27
|
+
#: worker's infrastructure (``handler.context``, ``observability.observability``),
|
|
28
|
+
#: the SDK's structured logger (``_logging``) and Temporal
|
|
29
|
+
#: (``observability.utils``). A guard falls back only when one of these (or a
|
|
30
|
+
#: submodule of one) is what is missing; anything else is a real error. A new
|
|
31
|
+
#: guard over another worker-only module adds that module here.
|
|
32
|
+
_WORKER_ONLY_PREFIXES: tuple[str, ...] = (
|
|
33
|
+
"application_sdk.infrastructure",
|
|
34
|
+
"application_sdk.observability",
|
|
35
|
+
"application_sdk.storage",
|
|
36
|
+
"temporalio",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@functools.cache
|
|
41
|
+
def api_only_install() -> bool:
|
|
42
|
+
"""True when only ``atlan-application-sdk-api`` is installed (the API host)."""
|
|
43
|
+
return importlib.util.find_spec(_WORKER_SENTINEL) is None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def worker_only_missing(exc: ModuleNotFoundError) -> bool:
|
|
47
|
+
"""True when ``exc`` is an api-only install lacking a worker-only module.
|
|
48
|
+
|
|
49
|
+
Use it in ``except ModuleNotFoundError as exc: if not worker_only_missing(exc):
|
|
50
|
+
raise``. With the full SDK installed it is always False, so the error
|
|
51
|
+
propagates exactly as it did before the split. On an api-only install it is
|
|
52
|
+
True only when the missing module is one of :data:`_WORKER_ONLY_PREFIXES`;
|
|
53
|
+
a missing api file or third-party dependency still raises.
|
|
54
|
+
"""
|
|
55
|
+
if not api_only_install():
|
|
56
|
+
return False
|
|
57
|
+
missing = exc.name or ""
|
|
58
|
+
return any(
|
|
59
|
+
missing == prefix or missing.startswith(prefix + ".")
|
|
60
|
+
for prefix in _WORKER_ONLY_PREFIXES
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def import_submodule(package: str, name: str) -> ModuleType | None:
|
|
65
|
+
"""``package.name`` when it names an existing submodule, else ``None``.
|
|
66
|
+
|
|
67
|
+
A package whose ``__init__`` imports lazily (a ``__getattr__`` over a
|
|
68
|
+
``_LAZY`` map) no longer has its submodules as attributes until something
|
|
69
|
+
imports them. :func:`lazy_getattr` calls this for the submodules the package
|
|
70
|
+
used to import eagerly, so ``import pkg; pkg.sub`` keeps working for them.
|
|
71
|
+
"""
|
|
72
|
+
if not name.isidentifier() or name.startswith("__"):
|
|
73
|
+
return None
|
|
74
|
+
full = f"{package}.{name}"
|
|
75
|
+
try:
|
|
76
|
+
spec = importlib.util.find_spec(full)
|
|
77
|
+
except (ImportError, ValueError):
|
|
78
|
+
return None
|
|
79
|
+
return importlib.import_module(full) if spec is not None else None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _names_missing_module(exc: ModuleNotFoundError, module: str) -> bool:
|
|
83
|
+
"""Whether ``exc`` is *module* itself (or a parent package) being absent."""
|
|
84
|
+
missing = exc.name or ""
|
|
85
|
+
return bool(missing) and (module == missing or module.startswith(f"{missing}."))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def lazy_getattr(
|
|
89
|
+
module_name: str,
|
|
90
|
+
name: str,
|
|
91
|
+
lazy_map: Mapping[str, tuple[str, str]],
|
|
92
|
+
globals_: MutableMapping[str, Any],
|
|
93
|
+
submodules: Collection[str] = (),
|
|
94
|
+
) -> Any:
|
|
95
|
+
"""The PEP 562 ``__getattr__`` body of a package that imports lazily.
|
|
96
|
+
|
|
97
|
+
*lazy_map* maps a public name to ``(module, attribute)``; the first access
|
|
98
|
+
imports it and caches it in the package's *globals_*. *submodules* are the
|
|
99
|
+
submodules the package imported eagerly before it went lazy, so
|
|
100
|
+
``import pkg; pkg.sub`` keeps working for those and only those: any other
|
|
101
|
+
submodule is an attribute only once something imports it, as with any
|
|
102
|
+
package (so ``hasattr(pkg, "sub")`` does not import it).
|
|
103
|
+
``from pkg import sub`` needs no entry; the import system loads it.
|
|
104
|
+
|
|
105
|
+
A name whose module is not installed (on an api-only install the worker's
|
|
106
|
+
modules are absent) raises :class:`AttributeError` chained to the import
|
|
107
|
+
error, so ``hasattr`` and ``getattr(pkg, name, default)`` answer normally.
|
|
108
|
+
A module that is installed but fails to import raises its own error
|
|
109
|
+
unchanged: that is a real bug, and ``from pkg import name`` would otherwise
|
|
110
|
+
report only "cannot import name".
|
|
111
|
+
"""
|
|
112
|
+
target = lazy_map.get(name)
|
|
113
|
+
if target is not None:
|
|
114
|
+
try:
|
|
115
|
+
module = importlib.import_module(target[0])
|
|
116
|
+
except ModuleNotFoundError as exc:
|
|
117
|
+
if not _names_missing_module(exc, target[0]):
|
|
118
|
+
raise
|
|
119
|
+
raise AttributeError(
|
|
120
|
+
f"module {module_name!r} has no attribute {name!r}: "
|
|
121
|
+
f"{target[0]} is not installed"
|
|
122
|
+
) from exc
|
|
123
|
+
value = getattr(module, target[1])
|
|
124
|
+
globals_[name] = value
|
|
125
|
+
return value
|
|
126
|
+
if name in submodules:
|
|
127
|
+
submodule = import_submodule(module_name, name)
|
|
128
|
+
if submodule is not None:
|
|
129
|
+
return submodule
|
|
130
|
+
raise AttributeError(f"module {module_name!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Logger lookup for code that ships in the api distribution.
|
|
2
|
+
|
|
3
|
+
The handler surface (errors, handler, contracts, credential specs) ships in
|
|
4
|
+
``atlan-application-sdk-api``, which ``atlan-application-sdk`` depends on and
|
|
5
|
+
which the consolidated API host installs without the worker SDK. Those files log
|
|
6
|
+
through this helper. With the full SDK installed it returns the SDK's structured
|
|
7
|
+
logger (``observability.logger_adaptor.get_logger``), exactly as before; with
|
|
8
|
+
only the api distribution installed ``application_sdk.observability`` does not
|
|
9
|
+
exist, and it falls back to the standard library.
|
|
10
|
+
|
|
11
|
+
On that api-only path the stdlib ``application_sdk`` logger would otherwise have
|
|
12
|
+
no handler, so its INFO lines (route lifecycle, preflight verdicts) would be
|
|
13
|
+
dropped by the root logger's default WARNING level. The first fallback lookup
|
|
14
|
+
therefore gives it one minimal handler — INFO and above, to stderr — and sets
|
|
15
|
+
its level to INFO, but only when neither it nor the root logger has a handler
|
|
16
|
+
yet (a host that configured logging first is left alone). If the root logger
|
|
17
|
+
gains a handler later, the SDK's handler stands down and records propagate there
|
|
18
|
+
instead, so no line is written twice; the ``application_sdk`` logger stays at
|
|
19
|
+
INFO, so the SDK's INFO lines still reach those handlers unless the host sets
|
|
20
|
+
that logger's level itself. The full SDK install never takes this path: worker
|
|
21
|
+
logging is unchanged.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import functools
|
|
27
|
+
import logging
|
|
28
|
+
from collections.abc import Mapping
|
|
29
|
+
from typing import Protocol
|
|
30
|
+
|
|
31
|
+
from application_sdk._install import worker_only_missing
|
|
32
|
+
|
|
33
|
+
_ExcInfo = bool | BaseException | None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SdkLogger(Protocol):
|
|
37
|
+
"""The logging calls both loggers :func:`get_logger` returns accept.
|
|
38
|
+
|
|
39
|
+
Satisfied by the SDK's ``AtlanLoggerAdapter`` (full install) and by
|
|
40
|
+
:class:`logging.Logger` (api-only install), so code here may use only what
|
|
41
|
+
both take: ``%``-style arguments and the stdlib keywords.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def debug(
|
|
45
|
+
self,
|
|
46
|
+
msg: str,
|
|
47
|
+
/,
|
|
48
|
+
*args: object,
|
|
49
|
+
exc_info: _ExcInfo = ...,
|
|
50
|
+
stack_info: bool = ...,
|
|
51
|
+
stacklevel: int = ...,
|
|
52
|
+
extra: Mapping[str, object] | None = ...,
|
|
53
|
+
) -> None: ...
|
|
54
|
+
|
|
55
|
+
def info(
|
|
56
|
+
self,
|
|
57
|
+
msg: str,
|
|
58
|
+
/,
|
|
59
|
+
*args: object,
|
|
60
|
+
exc_info: _ExcInfo = ...,
|
|
61
|
+
stack_info: bool = ...,
|
|
62
|
+
stacklevel: int = ...,
|
|
63
|
+
extra: Mapping[str, object] | None = ...,
|
|
64
|
+
) -> None: ...
|
|
65
|
+
|
|
66
|
+
def warning(
|
|
67
|
+
self,
|
|
68
|
+
msg: str,
|
|
69
|
+
/,
|
|
70
|
+
*args: object,
|
|
71
|
+
exc_info: _ExcInfo = ...,
|
|
72
|
+
stack_info: bool = ...,
|
|
73
|
+
stacklevel: int = ...,
|
|
74
|
+
extra: Mapping[str, object] | None = ...,
|
|
75
|
+
) -> None: ...
|
|
76
|
+
|
|
77
|
+
def error(
|
|
78
|
+
self,
|
|
79
|
+
msg: str,
|
|
80
|
+
/,
|
|
81
|
+
*args: object,
|
|
82
|
+
exc_info: _ExcInfo = ...,
|
|
83
|
+
stack_info: bool = ...,
|
|
84
|
+
stacklevel: int = ...,
|
|
85
|
+
extra: Mapping[str, object] | None = ...,
|
|
86
|
+
) -> None: ...
|
|
87
|
+
|
|
88
|
+
def exception(
|
|
89
|
+
self,
|
|
90
|
+
msg: str,
|
|
91
|
+
/,
|
|
92
|
+
*args: object,
|
|
93
|
+
exc_info: _ExcInfo = ...,
|
|
94
|
+
stack_info: bool = ...,
|
|
95
|
+
stacklevel: int = ...,
|
|
96
|
+
extra: Mapping[str, object] | None = ...,
|
|
97
|
+
) -> None: ...
|
|
98
|
+
|
|
99
|
+
def critical(
|
|
100
|
+
self,
|
|
101
|
+
msg: str,
|
|
102
|
+
/,
|
|
103
|
+
*args: object,
|
|
104
|
+
exc_info: _ExcInfo = ...,
|
|
105
|
+
stack_info: bool = ...,
|
|
106
|
+
stacklevel: int = ...,
|
|
107
|
+
extra: Mapping[str, object] | None = ...,
|
|
108
|
+
) -> None: ...
|
|
109
|
+
|
|
110
|
+
def log(
|
|
111
|
+
self,
|
|
112
|
+
level: int,
|
|
113
|
+
msg: str,
|
|
114
|
+
/,
|
|
115
|
+
*args: object,
|
|
116
|
+
exc_info: _ExcInfo = ...,
|
|
117
|
+
stack_info: bool = ...,
|
|
118
|
+
stacklevel: int = ...,
|
|
119
|
+
extra: Mapping[str, object] | None = ...,
|
|
120
|
+
) -> None: ...
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def get_logger(name: str | None = None) -> SdkLogger:
|
|
124
|
+
"""The SDK logger for ``name``, or a stdlib logger on an api-only install.
|
|
125
|
+
|
|
126
|
+
Same signature as :func:`application_sdk.observability.logger_adaptor.get_logger`,
|
|
127
|
+
which the modules using this helper re-exported before.
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
from application_sdk.observability.logger_adaptor import ( # noqa: PLC0415 — absent on an api-only install
|
|
131
|
+
get_logger as _sdk_get_logger,
|
|
132
|
+
)
|
|
133
|
+
except ModuleNotFoundError as exc:
|
|
134
|
+
if not worker_only_missing(exc):
|
|
135
|
+
raise
|
|
136
|
+
_configure_api_only_logging()
|
|
137
|
+
return logging.getLogger(name or __name__)
|
|
138
|
+
return _sdk_get_logger(name)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
#: The stdlib logger every api-only ``get_logger(__name__)`` in the SDK sits under.
|
|
142
|
+
_SDK_LOGGER_NAME = "application_sdk"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class _ApiOnlyHandler(logging.StreamHandler):
|
|
146
|
+
"""stderr, unless the root logger has since been given a handler of its own."""
|
|
147
|
+
|
|
148
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
149
|
+
if logging.getLogger().handlers:
|
|
150
|
+
return # the record propagates to the host's own root handlers
|
|
151
|
+
super().emit(record)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@functools.cache
|
|
155
|
+
def _configure_api_only_logging() -> None:
|
|
156
|
+
"""Give the ``application_sdk`` stdlib logger a minimal INFO handler, once.
|
|
157
|
+
|
|
158
|
+
Only on the api-only path, and only when neither that logger nor the root
|
|
159
|
+
logger has a handler: an existing logging setup is never touched.
|
|
160
|
+
"""
|
|
161
|
+
sdk_logger = logging.getLogger(_SDK_LOGGER_NAME)
|
|
162
|
+
if sdk_logger.handlers or logging.getLogger().handlers:
|
|
163
|
+
return
|
|
164
|
+
handler = _ApiOnlyHandler() # stderr
|
|
165
|
+
handler.setLevel(logging.INFO)
|
|
166
|
+
handler.setFormatter(
|
|
167
|
+
logging.Formatter("%(asctime)s %(levelname)s %(name)s - %(message)s")
|
|
168
|
+
)
|
|
169
|
+
sdk_logger.addHandler(handler)
|
|
170
|
+
if sdk_logger.level == logging.NOTSET:
|
|
171
|
+
sdk_logger.setLevel(logging.INFO)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Dependency-neutral runtime substrate — the SDK's bottom layer (ADR-0019).
|
|
2
|
+
|
|
3
|
+
Everything in this package may be imported **at module scope from anywhere in
|
|
4
|
+
the SDK**, including from ``storage/``. That is the package's entire purpose:
|
|
5
|
+
the offload pool and the progress seam are needed by every layer, so they cannot
|
|
6
|
+
live in a layer.
|
|
7
|
+
|
|
8
|
+
**The rule for modules in here.** A ``_runtime`` module may import from the
|
|
9
|
+
standard library, from third-party packages, from
|
|
10
|
+
``application_sdk.observability.logger_adaptor``, and from its siblings in this
|
|
11
|
+
package (``offload.py`` reads ``progress.py``, which is what makes the offload
|
|
12
|
+
auto-holds work). Nothing else in ``application_sdk``: never
|
|
13
|
+
``application_sdk.contracts``, ``application_sdk.credentials``,
|
|
14
|
+
``application_sdk.storage``, ``application_sdk.execution`` or
|
|
15
|
+
``application_sdk.app`` — each of those pulls in ``storage.ops`` transitively,
|
|
16
|
+
which is exactly the cycle this package exists to remove.
|
|
17
|
+
``tests/unit/runtime/test_layering.py`` enforces this in a subprocess, so
|
|
18
|
+
breaking the rule fails CI rather than surfacing as an ``ImportError`` at some
|
|
19
|
+
unrelated call site months later.
|
|
20
|
+
|
|
21
|
+
The boundary is *upward*, not lateral — a sibling import stays inside the bottom
|
|
22
|
+
layer and cannot reach a layer above it, so it cannot re-create the cycle.
|
|
23
|
+
|
|
24
|
+
**Private on purpose.** The app-facing paths are unchanged and stay where the
|
|
25
|
+
docs point them: ``application_sdk.execution.heartbeat`` for the offload
|
|
26
|
+
primitives and ``application_sdk.execution.progress`` for the progress seam,
|
|
27
|
+
both thin re-exports over this package. SDK-internal code imports from here
|
|
28
|
+
directly; apps should not need to know this package exists.
|
|
29
|
+
"""
|