tabsdata-server 2.0.1__cp312-abi3-win_amd64.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.
- tabsdata_server-2.0.1.data/purelib/tabsdatak/py.typed +0 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/__init__.py +5 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/__init__.py +11 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/dataquality/__init__.py +5 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/dataquality/driver.py +85 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/__init__.py +11 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/__init__.py +3 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/bundle.py +49 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/command.py +516 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/connection.py +19 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/endpoint.py +186 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/errors.py +265 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/function_def.py +138 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/http_request.py +163 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/initial_values.py +274 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/inputs_manifest.py +191 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/journal.py +93 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/liveness.py +114 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/message.py +45 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/mounts.py +44 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/outputs_manifest.py +121 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/plugin_loader.py +82 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/plugin_protocol.py +32 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/request.py +435 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/request_ctx.py +250 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/response.py +209 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/run_env.py +49 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/stage_backend.py +265 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/stage_layout.py +172 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/stage_manifest.py +250 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/state_manifest.py +102 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/stream_src_ctx.py +346 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/system_columns.py +180 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/system_info.py +311 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/transporter.py +116 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/transporter1.py +124 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/transporter2.py +157 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/__init__.py +12 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/begin_execution/__init__.py +74 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/begin_execution/error.py +27 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/begin_execution/runner.py +45 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/fetch_request/__init__.py +92 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/fetch_request/error.py +63 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/fetch_request/runner.py +99 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/inspect_function/__init__.py +57 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/inspect_function/runner.py +47 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/inspect_instance/__init__.py +42 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/inspect_instance/runner.py +41 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/localize_fn_bundle/__init__.py +100 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/localize_fn_bundle/error.py +61 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/localize_fn_bundle/runner.py +100 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/prep_localize_state/__init__.py +123 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/prep_localize_state/error.py +76 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/prep_localize_state/runner.py +181 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/prep_upload_state/__init__.py +76 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/prep_upload_state/error.py +52 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/prep_upload_state/runner.py +66 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/prep_upload_tables/__init__.py +92 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/prep_upload_tables/error.py +79 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/prep_upload_tables/runner.py +82 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/prepare_py_env/__init__.py +25 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/report_completion/__init__.py +88 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/report_completion/error.py +69 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/report_completion/runner.py +100 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_cron_trigger/__init__.py +101 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_cron_trigger/error.py +42 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_cron_trigger/runner.py +88 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_destination_plugin/__init__.py +106 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_destination_plugin/error.py +115 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_destination_plugin/runner.py +164 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_function/__init__.py +141 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_function/error.py +233 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_function/runner.py +523 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_manifest_trigger/__init__.py +155 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_manifest_trigger/error.py +46 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_manifest_trigger/runner.py +386 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_source_plugin/__init__.py +132 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_source_plugin/error.py +160 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_source_plugin/runner.py +371 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_streamer/__init__.py +115 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_streamer/error.py +123 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_streamer/runner.py +170 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_transporter/__init__.py +64 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_transporter/error.py +32 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_transporter/runner.py +45 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_trigger_plugin/__init__.py +25 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/stamp_inputs/__init__.py +98 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/stamp_inputs/error.py +84 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/stamp_inputs/runner.py +172 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdserver/__init__.py +11 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdserver/render/__init__.py +3 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdserver/render/check/__init__.py +168 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdserver/render/table/__init__.py +63 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdserver/render/yaml/__init__.py +44 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdserver/workers/__init__.py +4 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdserver/workers/apiready/__init__.py +173 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdserver/workers/apiready/error.py +19 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdserver/workers/conn_resources_extractor/__init__.py +169 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdserver/workers/conn_resources_extractor/error.py +19 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdserver/workers/dbready/__init__.py +152 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdserver/workers/dbready/error.py +5 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/assets/manifest/BANNER +5 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/assets/manifest/LICENSE +3 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/assets/manifest/README-PyPi.md +8 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/assets/manifest/RELEASE-NOTES +1 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/assets/manifest/THIRD-PARTY +3 -0
- tabsdata_server-2.0.1.data/purelib/tabsdatak/server/assets/manifest/VERSION +1 -0
- tabsdata_server-2.0.1.dist-info/METADATA +39 -0
- tabsdata_server-2.0.1.dist-info/RECORD +112 -0
- tabsdata_server-2.0.1.dist-info/WHEEL +5 -0
- tabsdata_server-2.0.1.dist-info/licenses/src/main/python/tabsdatak/server/assets/manifest/LICENSE +3 -0
- tabsdata_server-2.0.1.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import pkgutil
|
|
2
|
+
|
|
3
|
+
# noinspection PyUnboundLocalVariable
|
|
4
|
+
__path__ = pkgutil.extend_path(__path__, __name__)
|
|
5
|
+
|
|
6
|
+
# The lines above must appear at the top of this file to ensure
|
|
7
|
+
# PyCharm correctly recognizes namespace packages.
|
|
8
|
+
|
|
9
|
+
#
|
|
10
|
+
# Copyright 2026 Tabsdata Inc.
|
|
11
|
+
#
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2026 Tabsdata Inc.
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
"""Data-quality driver for the run_function command.
|
|
6
|
+
|
|
7
|
+
Runs the data-quality actions a function declares in ``on_tables`` against its
|
|
8
|
+
output tables. Data-quality actions act on the function's output tables: an
|
|
9
|
+
in-place action (e.g. filter) rewrites the output frame; a producing action
|
|
10
|
+
(e.g. enrich, select, summary) yields an additional derived table. The driver
|
|
11
|
+
returns the (possibly rewritten) output frames plus the derived tables, each
|
|
12
|
+
carrying the table name and the ``TableType`` the catalog registers it under.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
|
|
19
|
+
from tabsdatak.common._actions.dataquality.processor import DataQualityProcessor
|
|
20
|
+
from tabsdatak.dataquality import DataQuality
|
|
21
|
+
from tabsdatak.dataquality._symbols import TableType
|
|
22
|
+
from tabsdatak.tableframe import TableFrame
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class DerivedTable:
|
|
27
|
+
"""A data-quality derived output table and the type it registers under."""
|
|
28
|
+
|
|
29
|
+
name: str
|
|
30
|
+
frame: TableFrame
|
|
31
|
+
table_type: TableType
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class DataQualityResult:
|
|
36
|
+
"""Outcome of running data quality over a function's outputs.
|
|
37
|
+
|
|
38
|
+
``output_frames`` mirrors the input output-frame list with in-place actions
|
|
39
|
+
applied. ``derived`` holds the extra tables producing actions yielded.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
output_frames: list[TableFrame | None]
|
|
43
|
+
derived: list[DerivedTable]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def has_data_quality(on_tables) -> bool:
|
|
47
|
+
"""Whether ``on_tables`` declares any data-quality action."""
|
|
48
|
+
return any(isinstance(action, DataQuality) for action in on_tables or [])
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def run_data_quality(
|
|
52
|
+
*,
|
|
53
|
+
collection: str,
|
|
54
|
+
output_tables: list[str],
|
|
55
|
+
output_frames: list[TableFrame | None],
|
|
56
|
+
on_tables,
|
|
57
|
+
) -> DataQualityResult:
|
|
58
|
+
"""Run the function's data-quality actions over its output tables.
|
|
59
|
+
|
|
60
|
+
Returns the rewritten output frames and the derived tables. Assumes
|
|
61
|
+
``has_data_quality(on_tables)`` is true; the output-frame list is positional,
|
|
62
|
+
matching ``output_tables``.
|
|
63
|
+
"""
|
|
64
|
+
actions = [action for action in on_tables or [] if isinstance(action, DataQuality)]
|
|
65
|
+
processor = DataQualityProcessor.from_actions(
|
|
66
|
+
collection=collection,
|
|
67
|
+
output_tables=output_tables,
|
|
68
|
+
actions=actions,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
_, output_table_infos = processor.table_names()
|
|
72
|
+
|
|
73
|
+
frames = list(output_frames)
|
|
74
|
+
derived_frames: list = []
|
|
75
|
+
processor.tables_data([], frames, derived_frames)
|
|
76
|
+
|
|
77
|
+
derived = [
|
|
78
|
+
DerivedTable(
|
|
79
|
+
name=info.table,
|
|
80
|
+
frame=record.derived_tf,
|
|
81
|
+
table_type=info.kind.type,
|
|
82
|
+
)
|
|
83
|
+
for info, record in zip(output_table_infos, derived_frames)
|
|
84
|
+
]
|
|
85
|
+
return DataQualityResult(output_frames=frames, derived=derived)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import pkgutil
|
|
2
|
+
|
|
3
|
+
# noinspection PyUnboundLocalVariable
|
|
4
|
+
__path__ = pkgutil.extend_path(__path__, __name__)
|
|
5
|
+
|
|
6
|
+
# The lines above must appear at the top of this file to ensure
|
|
7
|
+
# PyCharm correctly recognizes namespace packages.
|
|
8
|
+
|
|
9
|
+
#
|
|
10
|
+
# Copyright 2026 Tabsdata Inc.
|
|
11
|
+
#
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2026 Tabsdata Inc.
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
"""Function-bundle tar extraction.
|
|
6
|
+
|
|
7
|
+
The function bundle is a `.tar.zst` (zstandard-compressed tar)
|
|
8
|
+
produced by tabsdata containing the user's code, `requirements.yaml`,
|
|
9
|
+
and any small assets. This module unpacks it into a target directory
|
|
10
|
+
using `tarfile`'s strict `data` filter (Python 3.12+), which rejects
|
|
11
|
+
path-traversal escapes, absolute paths, and links pointing outside
|
|
12
|
+
the destination.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import tarfile
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import zstandard
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def extract_bundle(archive: Path, dest_dir: Path) -> None:
|
|
24
|
+
"""Extract a `.tar.zst` bundle into `dest_dir`.
|
|
25
|
+
|
|
26
|
+
Creates `dest_dir` (and any missing parents) if it does not exist.
|
|
27
|
+
Existing contents of `dest_dir` are preserved; bundle members are
|
|
28
|
+
written alongside them.
|
|
29
|
+
|
|
30
|
+
The archive is decompressed through ``zstandard.ZstdDecompressor.
|
|
31
|
+
stream_reader`` and the resulting tar stream is read in streaming
|
|
32
|
+
mode (``mode="r|"``) so memory use stays bounded for large
|
|
33
|
+
bundles.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
FileNotFoundError: if `archive` does not exist.
|
|
37
|
+
zstandard.ZstdError: if `archive` is not valid zstandard
|
|
38
|
+
data (corrupt header, truncated frame, …).
|
|
39
|
+
tarfile.ReadError: if the inner tar stream (after successful
|
|
40
|
+
decompression) is corrupt or not a tarball.
|
|
41
|
+
tarfile.FilterError: if any member fails the `data`-filter safety
|
|
42
|
+
checks (e.g. absolute path, parent-dir escape, link pointing
|
|
43
|
+
outside the destination).
|
|
44
|
+
"""
|
|
45
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
with open(archive, "rb") as raw:
|
|
47
|
+
with zstandard.ZstdDecompressor().stream_reader(raw) as stream:
|
|
48
|
+
with tarfile.open(fileobj=stream, mode="r|") as tar:
|
|
49
|
+
tar.extractall(dest_dir, filter="data")
|
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2026 Tabsdata Inc.
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
"""Argument parsing and run orchestration for tdrunner commands.
|
|
6
|
+
|
|
7
|
+
Implements the ``--<opt> <V> | env:<VAR> | val:<V>`` shape from
|
|
8
|
+
``docs/01-CommandBasics.md`` and provides a parser builder pre-installed
|
|
9
|
+
with the spec's common options:
|
|
10
|
+
|
|
11
|
+
* ``--help`` / ``-h``
|
|
12
|
+
* ``--man`` / ``-m``
|
|
13
|
+
* ``--version`` / ``-v``
|
|
14
|
+
* ``--info`` / ``-i``
|
|
15
|
+
* ``--liveness`` / ``-l``
|
|
16
|
+
* ``--log-config`` / ``-c``
|
|
17
|
+
* ``--responder-url``
|
|
18
|
+
* ``--responder-method``
|
|
19
|
+
* ``--responder-authorization``
|
|
20
|
+
|
|
21
|
+
Also defines the :func:`command` decorator, which wraps every command
|
|
22
|
+
body in a single try/except orchestration. On exit, it logs the outcome,
|
|
23
|
+
posts a failure payload to the responder endpoint, and returns the
|
|
24
|
+
appropriate exit code. The handled cases are:
|
|
25
|
+
|
|
26
|
+
* :class:`CommandError`
|
|
27
|
+
* :class:`Exception`
|
|
28
|
+
* :class:`SystemExit`
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import argparse
|
|
34
|
+
import json
|
|
35
|
+
import logging
|
|
36
|
+
import os
|
|
37
|
+
import sys
|
|
38
|
+
import urllib.error
|
|
39
|
+
import urllib.request
|
|
40
|
+
from datetime import datetime, timezone
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
from typing import Any, Callable, NoReturn
|
|
43
|
+
|
|
44
|
+
from tenacity import retry, stop_after_delay, wait_fixed
|
|
45
|
+
|
|
46
|
+
from tabsdatak._logging import setup_logging
|
|
47
|
+
from tabsdatak.common._enums import WorkerCallbackStatus
|
|
48
|
+
from tabsdatak.common._runtime.pathenv import extend_path
|
|
49
|
+
from tabsdatak.server._tdrunner.function._lib.errors import (
|
|
50
|
+
CommandErrorCode,
|
|
51
|
+
CommandException,
|
|
52
|
+
InconsistentError,
|
|
53
|
+
PrematureError,
|
|
54
|
+
UnexpectedError,
|
|
55
|
+
)
|
|
56
|
+
from tabsdatak.server._tdrunner.function._lib.journal import Journal
|
|
57
|
+
from tabsdatak.server._tdrunner.function._lib.liveness import install_liveness_handler
|
|
58
|
+
from tabsdatak.server._tdrunner.function._lib.message import (
|
|
59
|
+
EXIT_STATUS_SUCCESS,
|
|
60
|
+
ResponseMessagePayload,
|
|
61
|
+
)
|
|
62
|
+
from tabsdatak.server._tdrunner.function.commands import tabsdata_command
|
|
63
|
+
|
|
64
|
+
logger = logging.getLogger(__name__)
|
|
65
|
+
|
|
66
|
+
_ENV_PREFIX = "env:"
|
|
67
|
+
_VAL_PREFIX = "val:"
|
|
68
|
+
|
|
69
|
+
_RESPONDER_RETRY_WAIT_SECONDS = 2
|
|
70
|
+
_RESPONDER_RETRY_TOTAL_SECONDS = 32
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def resolve_value(arg: str) -> str:
|
|
74
|
+
"""Resolve a ``--<opt> <ARG>`` value.
|
|
75
|
+
|
|
76
|
+
``arg`` may take one of three forms:
|
|
77
|
+
|
|
78
|
+
* a literal string — returned verbatim.
|
|
79
|
+
* ``env:<VAR>`` — looked up in the environment.
|
|
80
|
+
* ``val:<VALUE>`` — explicit literal form, useful when the literal
|
|
81
|
+
would otherwise look like an ``env:`` reference.
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
UndefinedEnvError: if ``env:<VAR>`` references an unset variable.
|
|
85
|
+
"""
|
|
86
|
+
if arg.startswith(_VAL_PREFIX):
|
|
87
|
+
return arg[len(_VAL_PREFIX) :]
|
|
88
|
+
if arg.startswith(_ENV_PREFIX):
|
|
89
|
+
var_name = arg[len(_ENV_PREFIX) :]
|
|
90
|
+
try:
|
|
91
|
+
return os.environ[var_name]
|
|
92
|
+
except KeyError as exc:
|
|
93
|
+
raise CommandErrorCode.CMD_2.exception(cause=exc, var=var_name)
|
|
94
|
+
return arg
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def resolve_flag(arg: str) -> bool:
|
|
98
|
+
"""Resolve a flag-shaped `--<opt> <ARG>`.
|
|
99
|
+
|
|
100
|
+
The resolved value (after applying :func:`resolve_value`) must be
|
|
101
|
+
``true`` or ``false`` (case insensitive).
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
UndefinedEnvError: if the underlying ``env:<VAR>`` is unset.
|
|
105
|
+
InvalidOptionsError: if the resolved value is neither `true` nor
|
|
106
|
+
`false`.
|
|
107
|
+
"""
|
|
108
|
+
value = resolve_value(arg)
|
|
109
|
+
lower = value.lower()
|
|
110
|
+
if lower == "true":
|
|
111
|
+
return True
|
|
112
|
+
if lower == "false":
|
|
113
|
+
return False
|
|
114
|
+
raise CommandErrorCode.CMD_1.exception(
|
|
115
|
+
detail=(
|
|
116
|
+
f"flag value must be 'true' or 'false' (case insensitive), got {value!r}"
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class _Parser(argparse.ArgumentParser):
|
|
122
|
+
"""ArgumentParser that turns parse errors into InvalidOptionsError."""
|
|
123
|
+
|
|
124
|
+
def error(self, message: str) -> NoReturn: # type: ignore[override]
|
|
125
|
+
raise CommandErrorCode.CMD_1.exception(detail=message)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _make_print_action(text: str) -> type[argparse.Action]:
|
|
129
|
+
class _Action(argparse.Action):
|
|
130
|
+
def __init__(self, option_strings, dest, **kwargs):
|
|
131
|
+
kwargs["nargs"] = 0
|
|
132
|
+
kwargs.setdefault("default", argparse.SUPPRESS)
|
|
133
|
+
super().__init__(option_strings, dest, **kwargs)
|
|
134
|
+
|
|
135
|
+
def __call__(self, parser, namespace, values, option_string=None):
|
|
136
|
+
print(text)
|
|
137
|
+
parser.exit(0)
|
|
138
|
+
|
|
139
|
+
return _Action
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def build_parser(
|
|
143
|
+
prog: str,
|
|
144
|
+
description: str,
|
|
145
|
+
*,
|
|
146
|
+
version: str = "",
|
|
147
|
+
info: str = "",
|
|
148
|
+
man: str = "",
|
|
149
|
+
) -> argparse.ArgumentParser:
|
|
150
|
+
"""Build an ArgumentParser pre-installed with the spec's common options.
|
|
151
|
+
|
|
152
|
+
The returned parser supports:
|
|
153
|
+
|
|
154
|
+
* ``--help`` / ``-h`` — auto from argparse.
|
|
155
|
+
* ``--man`` / ``-m`` — print the manual and exit 0.
|
|
156
|
+
* ``--version`` / ``-v`` — print the version and exit 0.
|
|
157
|
+
* ``--info`` / ``-i`` — print the info blurb and exit 0.
|
|
158
|
+
* ``--shared-dir`` / ``-s PATH`` — optional; shared working dir
|
|
159
|
+
(owned by the pod).
|
|
160
|
+
* ``--work-dir`` / ``-w PATH`` — optional; private working dir
|
|
161
|
+
(owned by the container).
|
|
162
|
+
* ``--log-config`` / ``-c PATH`` — optional; path to a logging
|
|
163
|
+
config YAML. Defaults to the bundled config.
|
|
164
|
+
* ``--liveness`` / ``-l PATH`` — required; the file the command
|
|
165
|
+
touches at least once a minute to signal liveness.
|
|
166
|
+
* ``--responder-url`` — required; URL of the responder endpoint.
|
|
167
|
+
* ``--responder-method`` — required; HTTP method for the responder
|
|
168
|
+
call.
|
|
169
|
+
* ``--responder-authorization`` — required; ``Authorization``
|
|
170
|
+
header value for the responder call.
|
|
171
|
+
* ``--journal-file PATH`` — required; path to the shared
|
|
172
|
+
run-metadata YAML (``Journal``) every command in the chain
|
|
173
|
+
reads/writes.
|
|
174
|
+
"""
|
|
175
|
+
parser = _Parser(prog=prog, description=description)
|
|
176
|
+
parser.add_argument(
|
|
177
|
+
"--man",
|
|
178
|
+
"-m",
|
|
179
|
+
action=_make_print_action(man),
|
|
180
|
+
help="Print the detailed user guide and exit.",
|
|
181
|
+
)
|
|
182
|
+
parser.add_argument(
|
|
183
|
+
"--version",
|
|
184
|
+
"-v",
|
|
185
|
+
action=_make_print_action(version),
|
|
186
|
+
help="Print the command build version and exit.",
|
|
187
|
+
)
|
|
188
|
+
parser.add_argument(
|
|
189
|
+
"--info",
|
|
190
|
+
"-i",
|
|
191
|
+
action=_make_print_action(info),
|
|
192
|
+
help="Print the command build info and exit.",
|
|
193
|
+
)
|
|
194
|
+
parser.add_argument(
|
|
195
|
+
"--shared-dir",
|
|
196
|
+
"-s",
|
|
197
|
+
default=None,
|
|
198
|
+
metavar="PATH",
|
|
199
|
+
help="Shared working dir (owned by the pod).",
|
|
200
|
+
)
|
|
201
|
+
parser.add_argument(
|
|
202
|
+
"--work-dir",
|
|
203
|
+
"-w",
|
|
204
|
+
default=None,
|
|
205
|
+
metavar="PATH",
|
|
206
|
+
help="Private working dir (owned by the container).",
|
|
207
|
+
)
|
|
208
|
+
parser.add_argument(
|
|
209
|
+
"--log-config",
|
|
210
|
+
"-c",
|
|
211
|
+
default=None,
|
|
212
|
+
metavar="PATH",
|
|
213
|
+
help=(
|
|
214
|
+
"Path to a logging config YAML. When omitted, the bundled default is used."
|
|
215
|
+
),
|
|
216
|
+
)
|
|
217
|
+
parser.add_argument(
|
|
218
|
+
"--liveness",
|
|
219
|
+
"-l",
|
|
220
|
+
required=True,
|
|
221
|
+
metavar="PATH",
|
|
222
|
+
help=(
|
|
223
|
+
"File the command must touch at least once a minute to signal "
|
|
224
|
+
"liveness to the framework."
|
|
225
|
+
),
|
|
226
|
+
)
|
|
227
|
+
parser.add_argument(
|
|
228
|
+
"--responder-url",
|
|
229
|
+
required=True,
|
|
230
|
+
help="Responder endpoint URL.",
|
|
231
|
+
)
|
|
232
|
+
parser.add_argument(
|
|
233
|
+
"--responder-method",
|
|
234
|
+
required=True,
|
|
235
|
+
help="Responder HTTP method.",
|
|
236
|
+
)
|
|
237
|
+
parser.add_argument(
|
|
238
|
+
"--responder-authorization",
|
|
239
|
+
required=True,
|
|
240
|
+
help="Responder Authorization header value.",
|
|
241
|
+
)
|
|
242
|
+
parser.add_argument(
|
|
243
|
+
"--journal-file",
|
|
244
|
+
required=True,
|
|
245
|
+
metavar="PATH",
|
|
246
|
+
help=(
|
|
247
|
+
"Path to the shared Journal YAML every command in the "
|
|
248
|
+
"chain reads/writes on the shared volume."
|
|
249
|
+
),
|
|
250
|
+
)
|
|
251
|
+
return parser
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def run_command(main_fn: Callable[[], int | None]) -> int:
|
|
255
|
+
"""Run a command's ``main_fn``, mapping a raised
|
|
256
|
+
:class:`CommandException` to the matching process exit code.
|
|
257
|
+
|
|
258
|
+
Each mapped exception is printed to stderr first, then the
|
|
259
|
+
declared ``exit_code`` is returned. A return value of ``None`` is
|
|
260
|
+
treated as success (0); anything else is coerced via ``int()``.
|
|
261
|
+
|
|
262
|
+
Test-only helper: production commands go through the ``command``
|
|
263
|
+
decorator's orchestration. This shape is preserved so unit tests
|
|
264
|
+
can call ``run_command(lambda: body(args))`` without standing up
|
|
265
|
+
argparse, logging, or the responder.
|
|
266
|
+
"""
|
|
267
|
+
try:
|
|
268
|
+
result = main_fn()
|
|
269
|
+
except CommandException as exc:
|
|
270
|
+
print(str(exc), file=sys.stderr)
|
|
271
|
+
return exc.exit_code
|
|
272
|
+
if result is None:
|
|
273
|
+
return 0
|
|
274
|
+
return int(result)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _notify_status(
|
|
278
|
+
args: argparse.Namespace,
|
|
279
|
+
payload: ResponseMessagePayload,
|
|
280
|
+
) -> None:
|
|
281
|
+
"""POST a pre-built payload to the responder endpoint, raising on failure.
|
|
282
|
+
|
|
283
|
+
Not meant to be called directly — use :func:`notify_running`,
|
|
284
|
+
:func:`notify_done`, or :func:`notify_failed`, which build the proper
|
|
285
|
+
payload for each case.
|
|
286
|
+
|
|
287
|
+
The HTTP call is wrapped with a tenacity retry: fixed wait of
|
|
288
|
+
:data:`_RESPONDER_RETRY_WAIT_SECONDS` between attempts, total budget
|
|
289
|
+
:data:`_RESPONDER_RETRY_TOTAL_SECONDS`. If every attempt fails the
|
|
290
|
+
error propagates, so a command body posting a status (e.g.
|
|
291
|
+
``Running`` or ``Done``) that cannot reach the responder fails like
|
|
292
|
+
any other command error and is reported to the responder as
|
|
293
|
+
``Failed`` by the :func:`command` orchestration.
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
@retry(
|
|
297
|
+
stop=stop_after_delay(_RESPONDER_RETRY_TOTAL_SECONDS),
|
|
298
|
+
wait=wait_fixed(_RESPONDER_RETRY_WAIT_SECONDS),
|
|
299
|
+
reraise=True,
|
|
300
|
+
)
|
|
301
|
+
def post(request: urllib.request.Request) -> None:
|
|
302
|
+
try:
|
|
303
|
+
with urllib.request.urlopen(request) as response:
|
|
304
|
+
response.read()
|
|
305
|
+
except urllib.error.HTTPError as error:
|
|
306
|
+
body = error.read().decode("utf-8", errors="replace")
|
|
307
|
+
error.msg = f"{error.msg}: {body}" if body else error.msg
|
|
308
|
+
raise
|
|
309
|
+
|
|
310
|
+
url = resolve_value(args.responder_url)
|
|
311
|
+
method = resolve_value(args.responder_method)
|
|
312
|
+
authorization = resolve_value(args.responder_authorization)
|
|
313
|
+
body = json.dumps(payload.to_body()).encode("utf-8")
|
|
314
|
+
request = urllib.request.Request(
|
|
315
|
+
url,
|
|
316
|
+
data=body,
|
|
317
|
+
method=method,
|
|
318
|
+
headers={
|
|
319
|
+
"Authorization": authorization,
|
|
320
|
+
"Content-Type": "application/json",
|
|
321
|
+
},
|
|
322
|
+
)
|
|
323
|
+
status = payload.status
|
|
324
|
+
exit_status = payload.exit_status
|
|
325
|
+
logger.info(f"Posting responder {status}: {method} {url} exit_status={exit_status}")
|
|
326
|
+
post(request)
|
|
327
|
+
logger.info(f"Posted responder {status}: {method} {url} exit_status={exit_status}")
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def notify_running(args: argparse.Namespace) -> None:
|
|
331
|
+
"""POST a ``Running`` status to the responder, raising on failure.
|
|
332
|
+
|
|
333
|
+
``start`` (unix-epoch milliseconds) is read back from the run's
|
|
334
|
+
``Journal`` and carried through to the apiserver callback envelope's
|
|
335
|
+
``start`` field. The read is strict: a missing or broken journal
|
|
336
|
+
fails the command and is reported as ``Failed`` by the
|
|
337
|
+
:func:`command` orchestration.
|
|
338
|
+
"""
|
|
339
|
+
journal = Journal.load(
|
|
340
|
+
Path(args.journal_file),
|
|
341
|
+
strict=True,
|
|
342
|
+
)
|
|
343
|
+
_notify_status(
|
|
344
|
+
args,
|
|
345
|
+
ResponseMessagePayload(
|
|
346
|
+
status=WorkerCallbackStatus.RUNNING,
|
|
347
|
+
start=journal.start,
|
|
348
|
+
),
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def notify_done(
|
|
353
|
+
args: argparse.Namespace,
|
|
354
|
+
*,
|
|
355
|
+
context: Any | None = None,
|
|
356
|
+
exit_code: int = EXIT_STATUS_SUCCESS,
|
|
357
|
+
) -> None:
|
|
358
|
+
"""POST a ``Done`` status to the responder, raising on failure.
|
|
359
|
+
|
|
360
|
+
``start`` and ``end`` (unix-epoch milliseconds) are read back from
|
|
361
|
+
the run's ``Journal`` and carried through to the apiserver callback
|
|
362
|
+
envelope's ``start`` / ``end`` fields. The read is strict: a missing
|
|
363
|
+
or broken journal on the success path fails the command and is
|
|
364
|
+
reported as ``Failed`` by the :func:`command` orchestration.
|
|
365
|
+
``context`` is the optional per-run payload (the worker output).
|
|
366
|
+
"""
|
|
367
|
+
journal = Journal.load(
|
|
368
|
+
Path(args.journal_file),
|
|
369
|
+
strict=True,
|
|
370
|
+
)
|
|
371
|
+
logger.debug(f"notify_done context: {context}")
|
|
372
|
+
_notify_status(
|
|
373
|
+
args,
|
|
374
|
+
ResponseMessagePayload(
|
|
375
|
+
status=WorkerCallbackStatus.DONE,
|
|
376
|
+
start=journal.start,
|
|
377
|
+
end=journal.end,
|
|
378
|
+
context=context,
|
|
379
|
+
exit_status=exit_code,
|
|
380
|
+
),
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def notify_failed(
|
|
385
|
+
args: argparse.Namespace | None,
|
|
386
|
+
exit_code: int,
|
|
387
|
+
error: BaseException,
|
|
388
|
+
) -> None:
|
|
389
|
+
"""Best-effort POST a ``Failed`` status back to the responder endpoint.
|
|
390
|
+
|
|
391
|
+
``start`` and ``end`` are read from the run's ``Journal`` — ``start``
|
|
392
|
+
falls back to ``0`` and ``end`` to the current time when it cannot be
|
|
393
|
+
read or parsed, both unix-epoch milliseconds. Any failure inside this
|
|
394
|
+
helper is swallowed and logged — this is the terminal error path and
|
|
395
|
+
must not mask the original failure, so there is nowhere left to
|
|
396
|
+
escalate a failed post to.
|
|
397
|
+
"""
|
|
398
|
+
if args is None:
|
|
399
|
+
logger.warning(
|
|
400
|
+
"Skipping responder Failed POST: command arguments not available."
|
|
401
|
+
)
|
|
402
|
+
return
|
|
403
|
+
# noinspection PyBroadException
|
|
404
|
+
try:
|
|
405
|
+
journal = Journal.load(Path(args.journal_file), strict=False)
|
|
406
|
+
end = (
|
|
407
|
+
journal.end
|
|
408
|
+
if journal.end is not None
|
|
409
|
+
else int(datetime.now(tz=timezone.utc).timestamp() * 1000)
|
|
410
|
+
)
|
|
411
|
+
start = journal.start if journal.start is not None else end
|
|
412
|
+
_notify_status(
|
|
413
|
+
args,
|
|
414
|
+
ResponseMessagePayload(
|
|
415
|
+
status=WorkerCallbackStatus.FAILED,
|
|
416
|
+
start=start,
|
|
417
|
+
end=end,
|
|
418
|
+
exit_status=exit_code,
|
|
419
|
+
error_message=repr(error),
|
|
420
|
+
),
|
|
421
|
+
)
|
|
422
|
+
except Exception:
|
|
423
|
+
logger.warning("Failed to post Failed status to responder", exc_info=True)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def command(
|
|
427
|
+
*,
|
|
428
|
+
name: str,
|
|
429
|
+
parser: Callable[[], argparse.ArgumentParser],
|
|
430
|
+
) -> Callable[[Callable[[argparse.Namespace], int | None]], Callable[[], int]]:
|
|
431
|
+
"""Decorator that wraps a command body with the standard orchestration.
|
|
432
|
+
|
|
433
|
+
The returned zero-arg ``main`` performs, in order:
|
|
434
|
+
|
|
435
|
+
1. parse argv via ``parser``,
|
|
436
|
+
2. configure logging from ``--log-config``,
|
|
437
|
+
3. install the liveness handler from ``--liveness``,
|
|
438
|
+
4. invoke the decorated body with the parsed ``Namespace``,
|
|
439
|
+
5. coerce the body's return into an exit code (a ``None`` return
|
|
440
|
+
becomes 0).
|
|
441
|
+
|
|
442
|
+
The whole run lives inside one ``try`` with three ``except`` arms,
|
|
443
|
+
so the decorated body never needs its own top-level try/except:
|
|
444
|
+
|
|
445
|
+
* :class:`CommandException` — logged at ERROR with traceback; posted to
|
|
446
|
+
the responder; ``exit_code`` is promoted to
|
|
447
|
+
:class:`InconsistentError`'s ``exit_code`` (4) if the raised
|
|
448
|
+
exception claimed ``exit_code=0``.
|
|
449
|
+
* :class:`Exception` — logged at ERROR with traceback; wrapped in
|
|
450
|
+
:class:`UnexpectedError` (exit 3); posted to the responder.
|
|
451
|
+
* :class:`SystemExit` — logged at ERROR (no traceback); wrapped in
|
|
452
|
+
:class:`PrematureError` (exit 5); posted to the responder.
|
|
453
|
+
|
|
454
|
+
The result is additionally tagged with :func:`tabsdata_command` so
|
|
455
|
+
the SDK packager picks it up as a console-script entry point.
|
|
456
|
+
|
|
457
|
+
Example::
|
|
458
|
+
|
|
459
|
+
@command(name="td_fetch_request", parser=_build_parser)
|
|
460
|
+
def main(args: argparse.Namespace) -> int:
|
|
461
|
+
url = resolve_value(args.resolver_url)
|
|
462
|
+
...
|
|
463
|
+
return 0
|
|
464
|
+
"""
|
|
465
|
+
|
|
466
|
+
# ToDo:
|
|
467
|
+
# Resolve environment variables (similar to config resolver) for
|
|
468
|
+
# parameters that represent configuration files
|
|
469
|
+
def wrap(
|
|
470
|
+
body: Callable[[argparse.Namespace], int | None],
|
|
471
|
+
) -> Callable[[], int]:
|
|
472
|
+
@tabsdata_command(name=name)
|
|
473
|
+
def main() -> int:
|
|
474
|
+
extend_path()
|
|
475
|
+
args: argparse.Namespace | None = None
|
|
476
|
+
try:
|
|
477
|
+
parsed = parser().parse_args()
|
|
478
|
+
args = parsed
|
|
479
|
+
setup_logging(parsed.log_config)
|
|
480
|
+
install_liveness_handler(Path(parsed.liveness))
|
|
481
|
+
result = body(parsed)
|
|
482
|
+
exit_code = 0 if result is None else int(result)
|
|
483
|
+
return exit_code
|
|
484
|
+
except CommandException as error:
|
|
485
|
+
exit_code = error.exit_code
|
|
486
|
+
logger.exception("Handled exception caught by command")
|
|
487
|
+
if exit_code == 0:
|
|
488
|
+
logger.error(
|
|
489
|
+
f"CommandException raised with exit_code=0; "
|
|
490
|
+
f"promoting to {InconsistentError.exit_code}"
|
|
491
|
+
)
|
|
492
|
+
exit_code = InconsistentError.exit_code
|
|
493
|
+
notify_failed(args, exit_code, error)
|
|
494
|
+
return exit_code
|
|
495
|
+
except Exception as exception:
|
|
496
|
+
exit_code = UnexpectedError.exit_code
|
|
497
|
+
logger.exception("Unhandled exception caught by command")
|
|
498
|
+
notify_failed(args, exit_code, exception)
|
|
499
|
+
return exit_code
|
|
500
|
+
except SystemExit as exit_event:
|
|
501
|
+
exit_code = PrematureError.exit_code
|
|
502
|
+
logger.error(
|
|
503
|
+
f"Exit ({exit_event.code}) intercepted by command; "
|
|
504
|
+
f"promoting to {exit_code}"
|
|
505
|
+
)
|
|
506
|
+
notify_failed(args, exit_code, exit_event)
|
|
507
|
+
return exit_code
|
|
508
|
+
except BaseException as exception:
|
|
509
|
+
exit_code = UnexpectedError.exit_code
|
|
510
|
+
logger.exception("Unhandled core exception caught by command")
|
|
511
|
+
notify_failed(args, exit_code, exception)
|
|
512
|
+
return exit_code
|
|
513
|
+
|
|
514
|
+
return main
|
|
515
|
+
|
|
516
|
+
return wrap
|