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.
Files changed (112) hide show
  1. tabsdata_server-2.0.1.data/purelib/tabsdatak/py.typed +0 -0
  2. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/__init__.py +5 -0
  3. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/__init__.py +11 -0
  4. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/dataquality/__init__.py +5 -0
  5. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/dataquality/driver.py +85 -0
  6. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/__init__.py +11 -0
  7. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/__init__.py +3 -0
  8. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/bundle.py +49 -0
  9. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/command.py +516 -0
  10. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/connection.py +19 -0
  11. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/endpoint.py +186 -0
  12. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/errors.py +265 -0
  13. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/function_def.py +138 -0
  14. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/http_request.py +163 -0
  15. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/initial_values.py +274 -0
  16. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/inputs_manifest.py +191 -0
  17. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/journal.py +93 -0
  18. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/liveness.py +114 -0
  19. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/message.py +45 -0
  20. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/mounts.py +44 -0
  21. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/outputs_manifest.py +121 -0
  22. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/plugin_loader.py +82 -0
  23. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/plugin_protocol.py +32 -0
  24. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/request.py +435 -0
  25. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/request_ctx.py +250 -0
  26. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/response.py +209 -0
  27. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/run_env.py +49 -0
  28. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/stage_backend.py +265 -0
  29. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/stage_layout.py +172 -0
  30. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/stage_manifest.py +250 -0
  31. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/state_manifest.py +102 -0
  32. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/stream_src_ctx.py +346 -0
  33. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/system_columns.py +180 -0
  34. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/system_info.py +311 -0
  35. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/transporter.py +116 -0
  36. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/transporter1.py +124 -0
  37. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/_lib/transporter2.py +157 -0
  38. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/__init__.py +12 -0
  39. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/begin_execution/__init__.py +74 -0
  40. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/begin_execution/error.py +27 -0
  41. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/begin_execution/runner.py +45 -0
  42. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/fetch_request/__init__.py +92 -0
  43. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/fetch_request/error.py +63 -0
  44. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/fetch_request/runner.py +99 -0
  45. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/inspect_function/__init__.py +57 -0
  46. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/inspect_function/runner.py +47 -0
  47. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/inspect_instance/__init__.py +42 -0
  48. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/inspect_instance/runner.py +41 -0
  49. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/localize_fn_bundle/__init__.py +100 -0
  50. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/localize_fn_bundle/error.py +61 -0
  51. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/localize_fn_bundle/runner.py +100 -0
  52. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/prep_localize_state/__init__.py +123 -0
  53. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/prep_localize_state/error.py +76 -0
  54. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/prep_localize_state/runner.py +181 -0
  55. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/prep_upload_state/__init__.py +76 -0
  56. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/prep_upload_state/error.py +52 -0
  57. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/prep_upload_state/runner.py +66 -0
  58. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/prep_upload_tables/__init__.py +92 -0
  59. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/prep_upload_tables/error.py +79 -0
  60. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/prep_upload_tables/runner.py +82 -0
  61. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/prepare_py_env/__init__.py +25 -0
  62. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/report_completion/__init__.py +88 -0
  63. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/report_completion/error.py +69 -0
  64. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/report_completion/runner.py +100 -0
  65. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_cron_trigger/__init__.py +101 -0
  66. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_cron_trigger/error.py +42 -0
  67. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_cron_trigger/runner.py +88 -0
  68. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_destination_plugin/__init__.py +106 -0
  69. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_destination_plugin/error.py +115 -0
  70. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_destination_plugin/runner.py +164 -0
  71. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_function/__init__.py +141 -0
  72. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_function/error.py +233 -0
  73. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_function/runner.py +523 -0
  74. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_manifest_trigger/__init__.py +155 -0
  75. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_manifest_trigger/error.py +46 -0
  76. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_manifest_trigger/runner.py +386 -0
  77. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_source_plugin/__init__.py +132 -0
  78. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_source_plugin/error.py +160 -0
  79. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_source_plugin/runner.py +371 -0
  80. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_streamer/__init__.py +115 -0
  81. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_streamer/error.py +123 -0
  82. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_streamer/runner.py +170 -0
  83. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_transporter/__init__.py +64 -0
  84. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_transporter/error.py +32 -0
  85. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_transporter/runner.py +45 -0
  86. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/run_trigger_plugin/__init__.py +25 -0
  87. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/stamp_inputs/__init__.py +98 -0
  88. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/stamp_inputs/error.py +84 -0
  89. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdrunner/function/commands/stamp_inputs/runner.py +172 -0
  90. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdserver/__init__.py +11 -0
  91. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdserver/render/__init__.py +3 -0
  92. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdserver/render/check/__init__.py +168 -0
  93. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdserver/render/table/__init__.py +63 -0
  94. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdserver/render/yaml/__init__.py +44 -0
  95. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdserver/workers/__init__.py +4 -0
  96. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdserver/workers/apiready/__init__.py +173 -0
  97. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdserver/workers/apiready/error.py +19 -0
  98. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdserver/workers/conn_resources_extractor/__init__.py +169 -0
  99. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdserver/workers/conn_resources_extractor/error.py +19 -0
  100. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdserver/workers/dbready/__init__.py +152 -0
  101. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/_tdserver/workers/dbready/error.py +5 -0
  102. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/assets/manifest/BANNER +5 -0
  103. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/assets/manifest/LICENSE +3 -0
  104. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/assets/manifest/README-PyPi.md +8 -0
  105. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/assets/manifest/RELEASE-NOTES +1 -0
  106. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/assets/manifest/THIRD-PARTY +3 -0
  107. tabsdata_server-2.0.1.data/purelib/tabsdatak/server/assets/manifest/VERSION +1 -0
  108. tabsdata_server-2.0.1.dist-info/METADATA +39 -0
  109. tabsdata_server-2.0.1.dist-info/RECORD +112 -0
  110. tabsdata_server-2.0.1.dist-info/WHEEL +5 -0
  111. tabsdata_server-2.0.1.dist-info/licenses/src/main/python/tabsdatak/server/assets/manifest/LICENSE +3 -0
  112. tabsdata_server-2.0.1.dist-info/top_level.txt +1 -0
File without changes
@@ -0,0 +1,5 @@
1
+ #
2
+ # Copyright 2026 Tabsdata Inc.
3
+ #
4
+
5
+ from __future__ import annotations
@@ -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,5 @@
1
+ #
2
+ # Copyright 2026 Tabsdata Inc.
3
+ #
4
+
5
+ from tabsdatak.server._tdrunner.dataquality import driver as driver
@@ -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,3 @@
1
+ #
2
+ # Copyright 2026 Tabsdata Inc.
3
+ #
@@ -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