tabsdata-cli 2.0.0__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_cli-2.0.0.data/purelib/tabsdatak/cli/__init__.py +5 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/_registry.py +78 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/assets/manifest/BANNER +5 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/assets/manifest/LICENSE +3 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/assets/manifest/README-PyPi.md +8 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/assets/manifest/RELEASE-NOTES +1 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/assets/manifest/THIRD-PARTY +3 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/assets/manifest/VERSION +1 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/cli_utils.py +993 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/shell.py +390 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/__init__.py +3 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/auth_group.py +197 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/cli.py +508 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/collection_group.py +458 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/config_utils.py +93 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/connection_group.py +159 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/examples_group.py +206 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/execution_utils.py +429 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/fn_group.py +1109 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/fnrun_group.py +285 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/group_group.py +184 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/metrics_group.py +323 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/permission_group.py +259 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/plan_group.py +324 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/project_group.py +170 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/role_group.py +630 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/settings_group.py +344 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/table_group.py +600 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/trigger_group.py +338 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/trx_group.py +329 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/user_group.py +200 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/worker_group.py +185 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/template.py +781 -0
- tabsdata_cli-2.0.0.data/purelib/tabsdatak/py.typed +0 -0
- tabsdata_cli-2.0.0.dist-info/METADATA +39 -0
- tabsdata_cli-2.0.0.dist-info/RECORD +39 -0
- tabsdata_cli-2.0.0.dist-info/WHEEL +5 -0
- tabsdata_cli-2.0.0.dist-info/licenses/src/main/python/tabsdatak/cli/assets/manifest/LICENSE +3 -0
- tabsdata_cli-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2026 Tabsdata Inc.
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
"""``tdk connection`` -- discover connectors and build connection documents.
|
|
6
|
+
|
|
7
|
+
These commands are offline: they introspect the connectors registered through
|
|
8
|
+
the SPI registry (``avail_srcs`` / ``avail_stream_srcs`` / ``avail_dests``) and
|
|
9
|
+
emit the connectors' own envelope dialect (``{kind, apiVersion, type, spec}``).
|
|
10
|
+
They do not talk to a server.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import rich_click as click
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
|
|
19
|
+
from tabsdatak.cli._registry import cli_command
|
|
20
|
+
from tabsdatak.cli.cli_utils import format_option, logical_prompt, make_console, render
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@cli_command
|
|
24
|
+
@click.group()
|
|
25
|
+
def connection():
|
|
26
|
+
"""Connection template commands"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@connection.command()
|
|
30
|
+
@format_option
|
|
31
|
+
@click.pass_context
|
|
32
|
+
def types(ctx):
|
|
33
|
+
"""List the connection types available from installed connectors"""
|
|
34
|
+
# noinspection PyProtectedMember
|
|
35
|
+
from tabsdatak.cli import template
|
|
36
|
+
|
|
37
|
+
rows = template.list_connectors()
|
|
38
|
+
|
|
39
|
+
def _table():
|
|
40
|
+
table = Table(title="Available connectors")
|
|
41
|
+
table.add_column("Type", style="cyan", no_wrap=True)
|
|
42
|
+
table.add_column("Mode")
|
|
43
|
+
table.add_column("System")
|
|
44
|
+
for row in rows:
|
|
45
|
+
table.add_row(row.type, row.mode, row.system)
|
|
46
|
+
click.echo()
|
|
47
|
+
make_console().print(table)
|
|
48
|
+
click.echo(f"Number of connectors: {len(rows)}")
|
|
49
|
+
click.echo()
|
|
50
|
+
|
|
51
|
+
render(
|
|
52
|
+
ctx,
|
|
53
|
+
data=[{"type": r.type, "mode": r.mode, "system": r.system} for r in rows],
|
|
54
|
+
table=_table,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _type_option(fn):
|
|
59
|
+
return click.option(
|
|
60
|
+
"--type",
|
|
61
|
+
"-t",
|
|
62
|
+
"type_",
|
|
63
|
+
help="Connector type, e.g. 'mysql-sql-in', 's3-file-out'. "
|
|
64
|
+
"See 'tdk connection types'.",
|
|
65
|
+
)(fn)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _resolve(ctx, type_):
|
|
69
|
+
# noinspection PyProtectedMember
|
|
70
|
+
from tabsdatak.cli import template
|
|
71
|
+
|
|
72
|
+
type_ = type_ or logical_prompt(ctx, "Connector type (see 'tdk connection types')")
|
|
73
|
+
try:
|
|
74
|
+
# Types are globally unique, so the direction is always inferred.
|
|
75
|
+
return template.resolve_conn(type_, None)
|
|
76
|
+
except template.TemplateError as e:
|
|
77
|
+
raise click.ClickException(str(e))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _emit(document, file):
|
|
81
|
+
# noinspection PyProtectedMember
|
|
82
|
+
from tabsdatak.cli import template
|
|
83
|
+
|
|
84
|
+
text = template.to_yaml(document)
|
|
85
|
+
if file and file != "-":
|
|
86
|
+
Path(file).write_text(text)
|
|
87
|
+
click.echo(f"Wrote connection document to {file}")
|
|
88
|
+
else:
|
|
89
|
+
click.echo(text, nl=False)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# Named "template" explicitly: the function can't be called ``template`` without
|
|
93
|
+
# shadowing the imported engine module.
|
|
94
|
+
@connection.command("template")
|
|
95
|
+
@_type_option
|
|
96
|
+
# The template is always a commented YAML document (the "# accepts:" hints are
|
|
97
|
+
# the point, and JSON can't carry them), so there is no output *format* to pick
|
|
98
|
+
# here -- ``-o/--format`` is reserved CLI-wide for that. Use ``--file``/``-f``
|
|
99
|
+
# for the output path, matching validate/table/exe.
|
|
100
|
+
@click.option(
|
|
101
|
+
"--file",
|
|
102
|
+
"-f",
|
|
103
|
+
help="Write the template to this file. '-' (or omitting it) writes to stdout.",
|
|
104
|
+
)
|
|
105
|
+
@click.pass_context
|
|
106
|
+
def template_(ctx, type_, file):
|
|
107
|
+
"""Print a fill-in connection template for a connector type"""
|
|
108
|
+
# noinspection PyProtectedMember
|
|
109
|
+
from tabsdatak.cli import template
|
|
110
|
+
|
|
111
|
+
info = _resolve(ctx, type_)
|
|
112
|
+
# Show every config: required fields as fill-ins, optional ones with their
|
|
113
|
+
# default (default-None fields commented out so the document stays valid).
|
|
114
|
+
_emit(template.build_template(info.conn_cls, all_fields=True), file)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@connection.command()
|
|
118
|
+
@click.option(
|
|
119
|
+
"--file",
|
|
120
|
+
"-f",
|
|
121
|
+
"file",
|
|
122
|
+
required=True,
|
|
123
|
+
help="Connection YAML file to validate, or '-' to read from stdin.",
|
|
124
|
+
)
|
|
125
|
+
def validate(file):
|
|
126
|
+
"""Validate a connection YAML document against its connector schema"""
|
|
127
|
+
# noinspection PyProtectedMember
|
|
128
|
+
from tabsdatak.cli import template
|
|
129
|
+
from tabsdatak.common._error import CommonException
|
|
130
|
+
from tabsdatak.common._json_serde import (
|
|
131
|
+
env_secret_ref_vars,
|
|
132
|
+
resolve_env_secret_refs,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
raw = sys.stdin.read() if file == "-" else Path(file).read_text()
|
|
136
|
+
try:
|
|
137
|
+
document = template.from_yaml(raw)
|
|
138
|
+
except Exception as e: # noqa: BLE001 -- any YAML parse failure
|
|
139
|
+
raise click.ClickException(f"not valid YAML: {e}")
|
|
140
|
+
# Resolve any `$secret:<ENV_VAR>` references from the environment first,
|
|
141
|
+
# exactly as connection create/upsert does, so a document that sources
|
|
142
|
+
# secret values from the environment validates the same way it is created
|
|
143
|
+
# (an unset referenced var is reported, not silently rejected as a bad
|
|
144
|
+
# secret value).
|
|
145
|
+
env_vars = env_secret_ref_vars(document)
|
|
146
|
+
try:
|
|
147
|
+
document = resolve_env_secret_refs(document)
|
|
148
|
+
except CommonException as e:
|
|
149
|
+
raise click.ClickException(str(e))
|
|
150
|
+
if env_vars:
|
|
151
|
+
click.echo(
|
|
152
|
+
"Connection secrets resolved from environment variable(s): "
|
|
153
|
+
+ ", ".join(env_vars)
|
|
154
|
+
)
|
|
155
|
+
try:
|
|
156
|
+
template.validate(document)
|
|
157
|
+
except template.TemplateError as e:
|
|
158
|
+
raise click.ClickException(str(e))
|
|
159
|
+
click.echo("Connection document is valid")
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2026 Tabsdata Inc.
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
"""``tdk examples`` -- discover and extract bundled example use cases.
|
|
6
|
+
|
|
7
|
+
Each example is a folder bundled under ``resources/examples`` (from the
|
|
8
|
+
``variant/resources`` tree) with the layout
|
|
9
|
+
``<name>/{input-data,connections,functions,_setup.py}``, resolved through
|
|
10
|
+
:func:`td_resource`. ``extract`` copies the folder into a target directory
|
|
11
|
+
(flattening every subfolder except the ``*-data`` ones), then runs the
|
|
12
|
+
example's ``_setup.py`` standing in that directory to finalize it.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import rich_click as click
|
|
21
|
+
from rich.table import Table
|
|
22
|
+
|
|
23
|
+
from tabsdatak.cli._registry import cli_command
|
|
24
|
+
from tabsdatak.cli.cli_utils import format_option, make_console, render, show_hint
|
|
25
|
+
|
|
26
|
+
EXAMPLES_RESOURCE = "tdresources/examples"
|
|
27
|
+
META_FILE = "_meta.yaml"
|
|
28
|
+
SETUP_FILE = "_setup.py"
|
|
29
|
+
ERROR_FILE = "error.txt"
|
|
30
|
+
DATA_DIR_SUFFIX = "-data"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@cli_command
|
|
34
|
+
@click.group()
|
|
35
|
+
def examples():
|
|
36
|
+
"""Example use case commands"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _examples_root() -> Path:
|
|
40
|
+
# noinspection PyProtectedMember
|
|
41
|
+
from tabsdatak.common._resources.reader import td_resource
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
return td_resource(EXAMPLES_RESOURCE)
|
|
45
|
+
except FileNotFoundError as error:
|
|
46
|
+
raise click.ClickException(
|
|
47
|
+
"Failed to locate examples: the examples resources are not "
|
|
48
|
+
"available. Reinstalling the tabsdata package is strongly "
|
|
49
|
+
f"recommended. ({error})"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _example_names(root: Path) -> list[str]:
|
|
54
|
+
return sorted(
|
|
55
|
+
entry.name
|
|
56
|
+
for entry in root.iterdir()
|
|
57
|
+
if entry.is_dir() and not entry.name.startswith((".", "_"))
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _read_meta(example_dir: Path) -> dict:
|
|
62
|
+
meta_path = example_dir / META_FILE
|
|
63
|
+
if not meta_path.exists():
|
|
64
|
+
return {}
|
|
65
|
+
from ruamel.yaml import YAML
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
data = YAML(typ="safe").load(meta_path.read_text())
|
|
69
|
+
except Exception:
|
|
70
|
+
return {}
|
|
71
|
+
return data if isinstance(data, dict) else {}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@examples.command("list")
|
|
75
|
+
@format_option
|
|
76
|
+
@click.pass_context
|
|
77
|
+
def list_(ctx: click.Context):
|
|
78
|
+
"""List the available examples"""
|
|
79
|
+
root = _examples_root()
|
|
80
|
+
rows = [
|
|
81
|
+
{
|
|
82
|
+
"name": name,
|
|
83
|
+
"description": str(_read_meta(root / name).get("description", "")).strip(),
|
|
84
|
+
}
|
|
85
|
+
for name in _example_names(root)
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
def _table():
|
|
89
|
+
table = Table(title="Available examples")
|
|
90
|
+
table.add_column("Name", style="cyan", no_wrap=True)
|
|
91
|
+
table.add_column("Description")
|
|
92
|
+
for row in rows:
|
|
93
|
+
table.add_row(row["name"], row["description"])
|
|
94
|
+
click.echo()
|
|
95
|
+
make_console().print(table)
|
|
96
|
+
click.echo(f"Number of examples: {len(rows)}")
|
|
97
|
+
click.echo()
|
|
98
|
+
|
|
99
|
+
render(ctx, data=rows, table=_table)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _skip(entry: Path) -> bool:
|
|
103
|
+
name = entry.name
|
|
104
|
+
return (
|
|
105
|
+
name == META_FILE
|
|
106
|
+
or name == "__pycache__"
|
|
107
|
+
or name.startswith(".")
|
|
108
|
+
or name.endswith((".pyc", ".pyo"))
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _copy_flattened(src: Path, target: Path) -> None:
|
|
113
|
+
"""Copy ``src`` into ``target``, flattening every subfolder into the target
|
|
114
|
+
root except ``*-data`` folders, which are copied verbatim. ``_meta.yaml`` and
|
|
115
|
+
build artifacts are left behind."""
|
|
116
|
+
for entry in sorted(src.iterdir()):
|
|
117
|
+
if _skip(entry):
|
|
118
|
+
continue
|
|
119
|
+
if entry.is_dir():
|
|
120
|
+
if entry.name.endswith(DATA_DIR_SUFFIX):
|
|
121
|
+
shutil.copytree(
|
|
122
|
+
entry,
|
|
123
|
+
target / entry.name,
|
|
124
|
+
ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "*.pyo"),
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
for member in sorted(entry.iterdir()):
|
|
128
|
+
if member.is_file() and not _skip(member):
|
|
129
|
+
shutil.copy2(member, target / member.name)
|
|
130
|
+
else:
|
|
131
|
+
shutil.copy2(entry, target / entry.name)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _run_setup(target: Path) -> None:
|
|
135
|
+
"""Run the example's ``_setup.py`` in ``target``. On success the script is
|
|
136
|
+
removed; on failure it is kept and the full output is dumped to
|
|
137
|
+
``error.txt`` in ``target``."""
|
|
138
|
+
setup_path = target / SETUP_FILE
|
|
139
|
+
if not setup_path.exists():
|
|
140
|
+
return
|
|
141
|
+
completed = subprocess.run(
|
|
142
|
+
[sys.executable, SETUP_FILE],
|
|
143
|
+
cwd=target,
|
|
144
|
+
capture_output=True,
|
|
145
|
+
text=True,
|
|
146
|
+
)
|
|
147
|
+
if completed.returncode == 0:
|
|
148
|
+
setup_path.unlink()
|
|
149
|
+
return
|
|
150
|
+
error_path = target / ERROR_FILE
|
|
151
|
+
error_path.write_text(
|
|
152
|
+
f"Example setup failed with exit code {completed.returncode}.\n"
|
|
153
|
+
f"command: {sys.executable} {SETUP_FILE}\n"
|
|
154
|
+
f"directory: {target}\n\n"
|
|
155
|
+
f"--- stdout ---\n{completed.stdout}\n"
|
|
156
|
+
f"--- stderr ---\n{completed.stderr}\n"
|
|
157
|
+
)
|
|
158
|
+
raise click.ClickException(
|
|
159
|
+
f"Failed to run example setup (exit code {completed.returncode}). The "
|
|
160
|
+
f"setup script was left in place and the full output was written to "
|
|
161
|
+
f"'{error_path}'."
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@examples.command()
|
|
166
|
+
@click.option(
|
|
167
|
+
"--name",
|
|
168
|
+
"-n",
|
|
169
|
+
required=True,
|
|
170
|
+
help="Name of the example to extract. See 'tdk examples list'.",
|
|
171
|
+
)
|
|
172
|
+
@click.option(
|
|
173
|
+
"--dir",
|
|
174
|
+
"-d",
|
|
175
|
+
"dir_",
|
|
176
|
+
required=True,
|
|
177
|
+
help=(
|
|
178
|
+
"Directory to generate the example in. The directory must not exist "
|
|
179
|
+
"beforehand, as it will be created by the CLI."
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
@click.pass_context
|
|
183
|
+
def extract(ctx: click.Context, name: str, dir_: str):
|
|
184
|
+
"""Extract an example into a new directory"""
|
|
185
|
+
root = _examples_root()
|
|
186
|
+
available = _example_names(root)
|
|
187
|
+
if name not in available:
|
|
188
|
+
raise click.ClickException(
|
|
189
|
+
f"Failed to extract example: unknown example '{name}'. Available "
|
|
190
|
+
f"examples: {', '.join(available) if available else '(none)'}."
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
target = Path(dir_)
|
|
194
|
+
if target.exists():
|
|
195
|
+
raise click.ClickException(f"Failed to extract example: {dir_} already exists.")
|
|
196
|
+
|
|
197
|
+
target.mkdir(parents=True)
|
|
198
|
+
_copy_flattened(root / name, target)
|
|
199
|
+
_run_setup(target)
|
|
200
|
+
|
|
201
|
+
click.echo(f"Example '{name}' generated in '{dir_}'")
|
|
202
|
+
show_hint(
|
|
203
|
+
ctx,
|
|
204
|
+
"Remember that in order to run the example, tdserver must be running in "
|
|
205
|
+
"the same host as the CLI.",
|
|
206
|
+
)
|