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,508 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2026 Tabsdata Inc.
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import textwrap
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
11
|
+
|
|
12
|
+
import rich_click as click
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
__version__ = version("tabsdata-cli")
|
|
16
|
+
except PackageNotFoundError:
|
|
17
|
+
__version__ = "0.0.0"
|
|
18
|
+
|
|
19
|
+
from tabsdatak.cli._registry import collect_cli_commands
|
|
20
|
+
from tabsdatak.cli.cli_utils import (
|
|
21
|
+
DEFAULT_TABSDATA_DIRECTORY,
|
|
22
|
+
OPTION_PROMPT,
|
|
23
|
+
RENDER_WIDTH,
|
|
24
|
+
SERVER_URL_DEFAULTS_HELP,
|
|
25
|
+
clear_pins,
|
|
26
|
+
cli_option_enabled,
|
|
27
|
+
configured_server,
|
|
28
|
+
get_credentials_file_path,
|
|
29
|
+
hint_common_solutions,
|
|
30
|
+
initialise_tabsdata_server_connection,
|
|
31
|
+
load_cli_options,
|
|
32
|
+
logical_prompt,
|
|
33
|
+
make_console,
|
|
34
|
+
store_cli_options,
|
|
35
|
+
utils_login,
|
|
36
|
+
verify_login_or_prompt,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Built-in command groups, imported for their @cli_command side effects: each
|
|
40
|
+
# module registers its top-level group, attached below via collect_cli_commands.
|
|
41
|
+
# Only registration runs at import time -- every command's execution-time imports
|
|
42
|
+
# (the server SDK, connection templates, ...) are deferred into the command body.
|
|
43
|
+
# TODO: role and stage management are temporarily removed; reintroduce their
|
|
44
|
+
# modules here (they self-register via @cli_command) in a later version.
|
|
45
|
+
from tabsdatak.cli.td import ( # noqa: F401
|
|
46
|
+
auth_group,
|
|
47
|
+
collection_group,
|
|
48
|
+
connection_group,
|
|
49
|
+
examples_group,
|
|
50
|
+
fn_group,
|
|
51
|
+
fnrun_group,
|
|
52
|
+
group_group,
|
|
53
|
+
metrics_group,
|
|
54
|
+
permission_group,
|
|
55
|
+
plan_group,
|
|
56
|
+
project_group,
|
|
57
|
+
settings_group,
|
|
58
|
+
table_group,
|
|
59
|
+
trigger_group,
|
|
60
|
+
trx_group,
|
|
61
|
+
user_group,
|
|
62
|
+
worker_group,
|
|
63
|
+
)
|
|
64
|
+
from tabsdatak.common._information.about import tdabout
|
|
65
|
+
|
|
66
|
+
CYAN = "[cyan]"
|
|
67
|
+
NO_CYAN = "[/cyan]"
|
|
68
|
+
|
|
69
|
+
BOLD = "[bold]"
|
|
70
|
+
NO_BOLD = "[/bold]"
|
|
71
|
+
|
|
72
|
+
UNDERLINE = "[underline]"
|
|
73
|
+
NO_UNDERLINE = "[underline]"
|
|
74
|
+
|
|
75
|
+
_render_width = None if sys.stdout.isatty() else RENDER_WIDTH
|
|
76
|
+
click.rich_click.WIDTH = _render_width
|
|
77
|
+
click.rich_click.MAX_WIDTH = _render_width
|
|
78
|
+
if _render_width is not None:
|
|
79
|
+
os.environ.setdefault("TERMINAL_WIDTH", str(_render_width))
|
|
80
|
+
|
|
81
|
+
console = make_console()
|
|
82
|
+
|
|
83
|
+
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
|
84
|
+
|
|
85
|
+
# Env vars each supported AI agent always sets when it runs a shell; tdk refuses
|
|
86
|
+
# to run inside them (agents must use the Tabsdata MCP server instead).
|
|
87
|
+
_AI_AGENT_ENV_VARS = ("GEMINI_CLI", "CLAUDECODE", "CODEX_THREAD_ID")
|
|
88
|
+
# Escape hatch: set to 1/true/yes to bypass the guard (warns instead of failing).
|
|
89
|
+
_AI_AGENT_BYPASS_ENV_VAR = "TDK_IN_AGENT"
|
|
90
|
+
_AI_AGENT_ERROR_MESSAGE = (
|
|
91
|
+
"Tabsdata tdk command does not support running within an AI agent, "
|
|
92
|
+
"use Tabsdata MCP instead"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _env_is_truthy(value: str | None) -> bool:
|
|
97
|
+
return (value or "").strip().lower() in {"1", "true", "yes"}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def detect_ai_agent(env: Mapping[str, str] | None = None) -> str | None:
|
|
101
|
+
"""Return the name of the first AI-agent marker env var that is set (and
|
|
102
|
+
non-empty), or ``None`` when tdk is not running inside a supported agent."""
|
|
103
|
+
env = os.environ if env is None else env
|
|
104
|
+
for var in _AI_AGENT_ENV_VARS:
|
|
105
|
+
value = env.get(var)
|
|
106
|
+
if value is not None and value.strip() != "":
|
|
107
|
+
return var
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def enforce_not_in_ai_agent(env: Mapping[str, str] | None = None) -> None:
|
|
112
|
+
"""Exit if tdk is being run inside a supported AI agent, unless the
|
|
113
|
+
``_TDK_IN_AGENT`` escape hatch is set truthy (in which case warn and continue)."""
|
|
114
|
+
env = os.environ if env is None else env
|
|
115
|
+
if detect_ai_agent(env) is None:
|
|
116
|
+
return
|
|
117
|
+
if _env_is_truthy(env.get(_AI_AGENT_BYPASS_ENV_VAR)):
|
|
118
|
+
click.echo(
|
|
119
|
+
f"WARNING: {_AI_AGENT_BYPASS_ENV_VAR} is set -- bypassing the AI-agent "
|
|
120
|
+
"guard; tdk is not supported within an AI agent, use Tabsdata MCP instead.",
|
|
121
|
+
err=True,
|
|
122
|
+
)
|
|
123
|
+
return
|
|
124
|
+
click.echo(_AI_AGENT_ERROR_MESSAGE, err=True)
|
|
125
|
+
raise SystemExit(1)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _full_help_options(command) -> list:
|
|
129
|
+
options = []
|
|
130
|
+
for param in getattr(command, "params", []):
|
|
131
|
+
if isinstance(param, click.Option):
|
|
132
|
+
options.append({
|
|
133
|
+
"opts": list(param.opts) + list(param.secondary_opts),
|
|
134
|
+
"help": param.help or "",
|
|
135
|
+
"is_flag": bool(param.is_flag),
|
|
136
|
+
"required": bool(param.required),
|
|
137
|
+
"default": (
|
|
138
|
+
param.default
|
|
139
|
+
if (param.default is not None and not param.is_flag)
|
|
140
|
+
else None
|
|
141
|
+
),
|
|
142
|
+
"type": getattr(param.type, "name", None),
|
|
143
|
+
})
|
|
144
|
+
return options
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _full_help_node(name: str, command, path: str) -> dict:
|
|
148
|
+
node = {
|
|
149
|
+
"name": name,
|
|
150
|
+
"path": path,
|
|
151
|
+
"help": (command.help or command.short_help or "").strip(),
|
|
152
|
+
"options": _full_help_options(command),
|
|
153
|
+
"arguments": [
|
|
154
|
+
{"name": param.name, "required": bool(param.required)}
|
|
155
|
+
for param in getattr(command, "params", [])
|
|
156
|
+
if isinstance(param, click.Argument)
|
|
157
|
+
],
|
|
158
|
+
}
|
|
159
|
+
if isinstance(command, click.Group):
|
|
160
|
+
node["commands"] = [
|
|
161
|
+
_full_help_node(sub, command.commands[sub], f"{path} {sub}")
|
|
162
|
+
for sub in sorted(command.commands)
|
|
163
|
+
]
|
|
164
|
+
return node
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _full_help_text(node: dict, lines: list | None = None) -> str:
|
|
168
|
+
lines = [] if lines is None else lines
|
|
169
|
+
lines.append(node["path"])
|
|
170
|
+
if node["help"]:
|
|
171
|
+
for line in node["help"].splitlines():
|
|
172
|
+
lines.append(f" {line}".rstrip())
|
|
173
|
+
if node["arguments"]:
|
|
174
|
+
lines.append(" Arguments:")
|
|
175
|
+
for arg in node["arguments"]:
|
|
176
|
+
tag = "required" if arg["required"] else "optional"
|
|
177
|
+
lines.append(f" {arg['name']} ({tag})")
|
|
178
|
+
if node["options"]:
|
|
179
|
+
lines.append(" Options:")
|
|
180
|
+
for opt in node["options"]:
|
|
181
|
+
flags = ", ".join(opt["opts"])
|
|
182
|
+
tag = "required" if opt["required"] else "optional"
|
|
183
|
+
suffix = f" {opt['help']}" if opt["help"] else ""
|
|
184
|
+
lines.append(f" {flags} ({tag}){suffix}".rstrip())
|
|
185
|
+
lines.append("")
|
|
186
|
+
for sub in node.get("commands", []):
|
|
187
|
+
_full_help_text(sub, lines)
|
|
188
|
+
return "\n".join(lines)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def build_full_help(root, output: str = "text", prog_name: str = "tdk") -> str:
|
|
192
|
+
"""Render the help for ``root`` and every command/subcommand under it.
|
|
193
|
+
|
|
194
|
+
Each option and argument is tagged ``(required)`` or ``(optional)`` in the
|
|
195
|
+
text form, and carries a ``required`` boolean in the json form.
|
|
196
|
+
|
|
197
|
+
``output`` is ``"text"`` (a flat, indented listing) or ``"json"`` (a nested
|
|
198
|
+
tree of ``{name, path, help, options, arguments, commands}`` objects)."""
|
|
199
|
+
tree = _full_help_node(prog_name, root, prog_name)
|
|
200
|
+
if output == "json":
|
|
201
|
+
# ``default=str`` coerces any non-JSON option default (e.g. a Choice or
|
|
202
|
+
# Path sentinel) to its string form rather than failing the dump.
|
|
203
|
+
return json.dumps(tree, indent=2, default=str)
|
|
204
|
+
return _full_help_text(tree)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _incoming_argv(args, kwargs) -> list:
|
|
208
|
+
if kwargs.get("args") is not None:
|
|
209
|
+
return list(kwargs["args"])
|
|
210
|
+
if args and args[0] is not None:
|
|
211
|
+
return list(args[0])
|
|
212
|
+
return list(sys.argv[1:])
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _full_output_format(argv: list) -> str:
|
|
216
|
+
fmt = "text"
|
|
217
|
+
for flag in ("--format", "-o"):
|
|
218
|
+
if flag in argv:
|
|
219
|
+
index = argv.index(flag)
|
|
220
|
+
if index + 1 < len(argv):
|
|
221
|
+
fmt = argv[index + 1]
|
|
222
|
+
if fmt not in ("text", "json"):
|
|
223
|
+
click.echo(f"Invalid --format '{fmt}'; expected 'text' or 'json'.", err=True)
|
|
224
|
+
raise SystemExit(2)
|
|
225
|
+
return fmt
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class _AiAgentGuardedGroup(click.RichGroup):
|
|
229
|
+
"""Top-level group that refuses to run inside a supported AI agent. The check
|
|
230
|
+
runs in ``main`` -- before any argument parsing -- so even ``--help`` and
|
|
231
|
+
``--version`` are gated, not just subcommands.
|
|
232
|
+
|
|
233
|
+
``main`` also intercepts ``--help --full`` (before Click's own ``--help``
|
|
234
|
+
handling) to print the recursive full help for every command and
|
|
235
|
+
subcommand. ``--full`` is only honored together with ``--help``, and
|
|
236
|
+
``--format`` only together with ``--full``:
|
|
237
|
+
``tdk --help [--full [--format FORMAT]]``."""
|
|
238
|
+
|
|
239
|
+
def main(self, *args, **kwargs):
|
|
240
|
+
enforce_not_in_ai_agent()
|
|
241
|
+
argv = _incoming_argv(args, kwargs)
|
|
242
|
+
wants_help = "--help" in argv or "-h" in argv
|
|
243
|
+
if wants_help and "--full" in argv:
|
|
244
|
+
click.echo(build_full_help(self, _full_output_format(argv)))
|
|
245
|
+
raise SystemExit(0)
|
|
246
|
+
return super().main(*args, **kwargs)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@click.group(cls=_AiAgentGuardedGroup, context_settings=CONTEXT_SETTINGS)
|
|
250
|
+
@click.version_option(version=__version__, message="Tabsdata Client %(version)s")
|
|
251
|
+
@click.option(
|
|
252
|
+
"--no-prompt",
|
|
253
|
+
is_flag=True,
|
|
254
|
+
help=(
|
|
255
|
+
"Disable all prompts for this invocation. If a prompt is required for "
|
|
256
|
+
"proper execution, the command will fail. Turn prompting off for good "
|
|
257
|
+
"with 'tdk set --prompt false'."
|
|
258
|
+
),
|
|
259
|
+
)
|
|
260
|
+
@click.option(
|
|
261
|
+
"--full",
|
|
262
|
+
is_flag=True,
|
|
263
|
+
expose_value=False,
|
|
264
|
+
help=(
|
|
265
|
+
"With --help, print the full help for every command and subcommand, "
|
|
266
|
+
"then exit. Optionally add '--format text|json' (default: text): "
|
|
267
|
+
"'tdk --help [--full [--format FORMAT]]'."
|
|
268
|
+
),
|
|
269
|
+
)
|
|
270
|
+
@click.pass_context
|
|
271
|
+
def cli(ctx: click.Context, no_prompt: bool):
|
|
272
|
+
"""Main CLI for the Tabsdata system."""
|
|
273
|
+
try:
|
|
274
|
+
os.makedirs(DEFAULT_TABSDATA_DIRECTORY, exist_ok=True)
|
|
275
|
+
ctx.obj = {
|
|
276
|
+
"tabsdata_directory": DEFAULT_TABSDATA_DIRECTORY,
|
|
277
|
+
"no_prompt": no_prompt,
|
|
278
|
+
}
|
|
279
|
+
# The server connection is set up lazily: the pinned project is applied
|
|
280
|
+
# when the connection is first built (on first use), so commands that
|
|
281
|
+
# never talk to the server don't construct it or import the server SDK.
|
|
282
|
+
initialise_tabsdata_server_connection(ctx)
|
|
283
|
+
# Every persisted setting -- hints, prompt, server, pins -- rides in
|
|
284
|
+
# the one options store.
|
|
285
|
+
load_cli_options(ctx)
|
|
286
|
+
# 'tdk set --prompt false' is a standing '--no-prompt'; the flag turns
|
|
287
|
+
# prompting off for a single invocation.
|
|
288
|
+
ctx.obj["prompt_setting"] = cli_option_enabled(ctx, OPTION_PROMPT)
|
|
289
|
+
ctx.obj["no_prompt"] = no_prompt or not ctx.obj["prompt_setting"]
|
|
290
|
+
except Exception as e:
|
|
291
|
+
hint_common_solutions(ctx, e)
|
|
292
|
+
raise click.ClickException(f"Failed to initialize CLI: {e}")
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# Attach every @cli_command-annotated group: the built-ins imported above, plus
|
|
296
|
+
# any enterprise-tier variant commands that register themselves when their
|
|
297
|
+
# package is installed. No manual add_command list to keep
|
|
298
|
+
# in sync -- a new command only needs the @cli_command annotation.
|
|
299
|
+
for _command in collect_cli_commands():
|
|
300
|
+
cli.add_command(_command)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@cli.command()
|
|
304
|
+
@click.pass_context
|
|
305
|
+
def shell(ctx: click.Context):
|
|
306
|
+
"""Start an interactive shell session"""
|
|
307
|
+
from tabsdatak.cli.shell import run_shell
|
|
308
|
+
|
|
309
|
+
run_shell(
|
|
310
|
+
ctx,
|
|
311
|
+
cli,
|
|
312
|
+
__version__,
|
|
313
|
+
banner_title="Tabsdata Interactive Shell",
|
|
314
|
+
tool_name="tdk",
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@cli.command()
|
|
319
|
+
def about():
|
|
320
|
+
"""Show build metadata and system information."""
|
|
321
|
+
tdabout()
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
MANIFEST_COLUMN_WIDTH = 80
|
|
325
|
+
|
|
326
|
+
MANIFEST_RULE_CHARACTERS = "═─"
|
|
327
|
+
|
|
328
|
+
MANIFEST_ROW_SEPARATORS = {
|
|
329
|
+
"╒": "╤",
|
|
330
|
+
"╞": "╪",
|
|
331
|
+
"├": "┼",
|
|
332
|
+
"╘": "╧",
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _narrow_manifest(content: str, width: int = MANIFEST_COLUMN_WIDTH) -> str:
|
|
337
|
+
narrowed = []
|
|
338
|
+
widths = None
|
|
339
|
+
for line in content.split("\n"):
|
|
340
|
+
if line.startswith("╒"):
|
|
341
|
+
widths = [len(segment) for segment in line[1:-1].split("╤")]
|
|
342
|
+
if widths is None or widths[-1] <= width:
|
|
343
|
+
narrowed.append(line)
|
|
344
|
+
continue
|
|
345
|
+
narrowed.extend(_narrow_manifest_line(line, widths, width))
|
|
346
|
+
return "\n".join(narrowed)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _narrow_manifest_line(line: str, widths: list[int], width: int) -> list[str]:
|
|
350
|
+
capped = widths[:-1] + [width]
|
|
351
|
+
separator = MANIFEST_ROW_SEPARATORS.get(line[:1])
|
|
352
|
+
if separator:
|
|
353
|
+
rule = line[1] if line[1] in MANIFEST_RULE_CHARACTERS else "─"
|
|
354
|
+
return [line[0] + separator.join(rule * size for size in capped) + line[-1]]
|
|
355
|
+
if not line.startswith("│"):
|
|
356
|
+
return [line]
|
|
357
|
+
cells = line[1:-1].split("│")
|
|
358
|
+
if len(cells) != len(widths):
|
|
359
|
+
return [line]
|
|
360
|
+
wrapped = [
|
|
361
|
+
textwrap.wrap(cells[-1].strip(), width - 2, break_long_words=False) or [""]
|
|
362
|
+
]
|
|
363
|
+
rows = []
|
|
364
|
+
for index, piece in enumerate(wrapped[0]):
|
|
365
|
+
head = cells[:-1] if index == 0 else [" " * size for size in widths[:-1]]
|
|
366
|
+
rows.append("│" + "│".join(head) + f"│ {piece.ljust(width - 2)} │")
|
|
367
|
+
return rows
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _show_manifest(resource: str, title: str, dedent: bool = False, wide: bool = False):
|
|
371
|
+
from tabsdatak.common._resources.reader import td_resource
|
|
372
|
+
|
|
373
|
+
path = td_resource(f"assets/manifest/{resource}")
|
|
374
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
375
|
+
content = f.read()
|
|
376
|
+
if dedent:
|
|
377
|
+
content = textwrap.dedent(content).strip()
|
|
378
|
+
if not wide:
|
|
379
|
+
content = _narrow_manifest(content)
|
|
380
|
+
console.print()
|
|
381
|
+
console.print(f"{UNDERLINE}{title}{NO_UNDERLINE}")
|
|
382
|
+
console.print()
|
|
383
|
+
console.print(f"{CYAN}{content}{NO_CYAN}")
|
|
384
|
+
console.print()
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@cli.group(no_args_is_help=True)
|
|
388
|
+
def info():
|
|
389
|
+
"""
|
|
390
|
+
Show the license, third-party dependencies and release notes of tabsdata.
|
|
391
|
+
"""
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@info.command(name="third-party")
|
|
395
|
+
@click.option(
|
|
396
|
+
"--wide",
|
|
397
|
+
is_flag=True,
|
|
398
|
+
default=False,
|
|
399
|
+
help="Show the license column at its full width, instead of wrapping it.",
|
|
400
|
+
)
|
|
401
|
+
def third_party(wide: bool):
|
|
402
|
+
"""Show the third-party dependencies of tabsdata."""
|
|
403
|
+
_show_manifest("THIRD-PARTY", "Third Party Dependencies", wide=wide)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
@info.command(name="license")
|
|
407
|
+
def license_():
|
|
408
|
+
"""Show the license of tabsdata."""
|
|
409
|
+
_show_manifest("LICENSE", "License", dedent=True)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
@info.command(name="release-notes")
|
|
413
|
+
def release_notes():
|
|
414
|
+
"""Show the release notes of the current version of tabsdata."""
|
|
415
|
+
_show_manifest("RELEASE-NOTES", "Release Notes")
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
@cli.command()
|
|
419
|
+
@click.option(
|
|
420
|
+
"--server",
|
|
421
|
+
"-s",
|
|
422
|
+
help=(
|
|
423
|
+
"URL of the Tabsdata Server. Defaults to the server set with 'tdk set "
|
|
424
|
+
"--server'; will be prompted for it if neither is provided. "
|
|
425
|
+
+ SERVER_URL_DEFAULTS_HELP
|
|
426
|
+
),
|
|
427
|
+
)
|
|
428
|
+
@click.option(
|
|
429
|
+
"--user",
|
|
430
|
+
"-u",
|
|
431
|
+
help="Username for the Tabsdata Server. Will be prompted for it if not provided.",
|
|
432
|
+
)
|
|
433
|
+
@click.option(
|
|
434
|
+
"--password",
|
|
435
|
+
"-p",
|
|
436
|
+
help=(
|
|
437
|
+
"Password for the Tabsdata Server. It is discouraged to send the password as a "
|
|
438
|
+
"plain argument, it should be either sent as the value of an environment "
|
|
439
|
+
"variable or written through the prompt. Will be prompted for it if not "
|
|
440
|
+
"provided."
|
|
441
|
+
),
|
|
442
|
+
)
|
|
443
|
+
@click.pass_context
|
|
444
|
+
def login(ctx: click.Context, server: str, user: str, password: str):
|
|
445
|
+
"""Login to the Tabsdata Server"""
|
|
446
|
+
server = (
|
|
447
|
+
server or configured_server(ctx) or logical_prompt(ctx, "Tabsdata Server URL")
|
|
448
|
+
)
|
|
449
|
+
user = user or logical_prompt(ctx, "Username for the Tabsdata Server")
|
|
450
|
+
password = password or logical_prompt(
|
|
451
|
+
ctx,
|
|
452
|
+
"Password",
|
|
453
|
+
hide_input=True,
|
|
454
|
+
)
|
|
455
|
+
utils_login(ctx, server, user, password)
|
|
456
|
+
from tabsdatak.cli.td.config_utils import execute_actions_for_command
|
|
457
|
+
|
|
458
|
+
execute_actions_for_command("login")
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
@cli.command()
|
|
462
|
+
@click.pass_context
|
|
463
|
+
def logout(ctx: click.Context):
|
|
464
|
+
"""Logout from the Tabsdata Server"""
|
|
465
|
+
try:
|
|
466
|
+
ctx.obj["tabsdataserver"].logout()
|
|
467
|
+
click.echo("Logout successful.")
|
|
468
|
+
except Exception as e:
|
|
469
|
+
hint_common_solutions(ctx, e)
|
|
470
|
+
raise click.ClickException(f"Failed to logout: {e}")
|
|
471
|
+
finally:
|
|
472
|
+
try:
|
|
473
|
+
os.remove(get_credentials_file_path(ctx))
|
|
474
|
+
except Exception:
|
|
475
|
+
pass
|
|
476
|
+
try:
|
|
477
|
+
clear_pins(ctx)
|
|
478
|
+
store_cli_options(ctx)
|
|
479
|
+
except Exception:
|
|
480
|
+
pass
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
@cli.command()
|
|
484
|
+
@click.pass_context
|
|
485
|
+
def status(ctx: click.Context):
|
|
486
|
+
"""Check the status of the server"""
|
|
487
|
+
verify_login_or_prompt(ctx)
|
|
488
|
+
click.echo("Obtaining server status")
|
|
489
|
+
click.echo("-" * 10)
|
|
490
|
+
try:
|
|
491
|
+
current_status = ctx.obj["tabsdataserver"].status
|
|
492
|
+
click.echo(str(current_status))
|
|
493
|
+
except Exception as e:
|
|
494
|
+
hint_common_solutions(ctx, e)
|
|
495
|
+
raise click.ClickException(f"Failed to get status: {e}")
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
if __name__ == "__main__":
|
|
499
|
+
from tabsdatak.common._compatibility import (
|
|
500
|
+
PackageVersionError,
|
|
501
|
+
check_sticky_version_packages,
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
try:
|
|
505
|
+
check_sticky_version_packages()
|
|
506
|
+
except PackageVersionError as error:
|
|
507
|
+
print(error.messages, file=sys.stderr)
|
|
508
|
+
cli()
|