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,390 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2026 Tabsdata Inc.
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import shlex
|
|
9
|
+
from urllib.parse import urlparse
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
import click_repl._completer as _completer_mod
|
|
13
|
+
import click_repl.utils as _utils_mod
|
|
14
|
+
from click_repl import repl
|
|
15
|
+
from click_repl.exceptions import ExitReplException
|
|
16
|
+
from prompt_toolkit.application import Application
|
|
17
|
+
from prompt_toolkit.history import FileHistory
|
|
18
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
19
|
+
from prompt_toolkit.keys import Keys
|
|
20
|
+
from prompt_toolkit.styles import Style
|
|
21
|
+
from rich.panel import Panel
|
|
22
|
+
from rich.text import Text
|
|
23
|
+
|
|
24
|
+
from tabsdatak.cli.cli_utils import make_console
|
|
25
|
+
|
|
26
|
+
# noinspection PyProtectedMember
|
|
27
|
+
from tabsdatak.sdk._apiserver import APIServer
|
|
28
|
+
from tabsdatak.sdk.tabsdata_server import TabsdataServer
|
|
29
|
+
|
|
30
|
+
# Reduce the escape key flush timeout from 0.5s to 0.05s so that
|
|
31
|
+
# pressing ESC feels instant (e.g. to dismiss the completion popup).
|
|
32
|
+
_orig_app_init = Application.__init__
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _fast_esc_app_init(self, *args, **kwargs):
|
|
36
|
+
_orig_app_init(self, *args, **kwargs)
|
|
37
|
+
self.ttimeoutlen = 0.05
|
|
38
|
+
self.timeoutlen = 0.05
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
Application.__init__ = _fast_esc_app_init
|
|
42
|
+
|
|
43
|
+
# Suppress noisy asyncio debug logging (e.g. "Using selector: KqueueSelector")
|
|
44
|
+
logging.getLogger("asyncio").setLevel(logging.WARNING)
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Click 8.2+ compatibility patch for click-repl 0.3.0.
|
|
48
|
+
#
|
|
49
|
+
# Click 8.2 deprecated Context.protected_args (read-only property backed by
|
|
50
|
+
# _protected_args). click-repl 0.3.0 still writes to it, which crashes.
|
|
51
|
+
# The fix is merged upstream (click-contrib/click-repl#132) but unreleased.
|
|
52
|
+
# We apply the same approach: access _protected_args when available.
|
|
53
|
+
# Remove this patch once a fixed click-repl is released.
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_protected_args(ctx):
|
|
58
|
+
if hasattr(ctx, "_protected_args"):
|
|
59
|
+
return ctx._protected_args
|
|
60
|
+
return ctx.protected_args
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def set_protected_args(ctx, value):
|
|
64
|
+
if hasattr(ctx, "_protected_args"):
|
|
65
|
+
ctx._protected_args = value
|
|
66
|
+
else:
|
|
67
|
+
ctx.protected_args = value
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# Patch _resolve_context (used by completer and utils)
|
|
71
|
+
_original_resolve = _utils_mod._resolve_context
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def patched_resolve(args, ctx=None):
|
|
75
|
+
while args:
|
|
76
|
+
command = ctx.command
|
|
77
|
+
if isinstance(command, click.MultiCommand):
|
|
78
|
+
try:
|
|
79
|
+
if not command.chain:
|
|
80
|
+
name, cmd, args = command.resolve_command(ctx, args)
|
|
81
|
+
if cmd is None:
|
|
82
|
+
return ctx
|
|
83
|
+
ctx = cmd.make_context(
|
|
84
|
+
name, args, parent=ctx, resilient_parsing=True
|
|
85
|
+
)
|
|
86
|
+
args = get_protected_args(ctx) + ctx.args
|
|
87
|
+
else:
|
|
88
|
+
while args:
|
|
89
|
+
name, cmd, args = command.resolve_command(ctx, args)
|
|
90
|
+
if cmd is None:
|
|
91
|
+
return ctx
|
|
92
|
+
sub_ctx = cmd.make_context(
|
|
93
|
+
name,
|
|
94
|
+
args,
|
|
95
|
+
parent=ctx,
|
|
96
|
+
allow_extra_args=True,
|
|
97
|
+
allow_interspersed_args=False,
|
|
98
|
+
resilient_parsing=True,
|
|
99
|
+
)
|
|
100
|
+
args = sub_ctx.args
|
|
101
|
+
ctx = sub_ctx
|
|
102
|
+
args = [*get_protected_args(sub_ctx), *sub_ctx.args]
|
|
103
|
+
except click.UsageError:
|
|
104
|
+
return ctx
|
|
105
|
+
else:
|
|
106
|
+
break
|
|
107
|
+
return ctx
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
_utils_mod._resolve_context = patched_resolve
|
|
111
|
+
_completer_mod._resolve_context = patched_resolve
|
|
112
|
+
|
|
113
|
+
# Add the missing property setter so click-repl can write protected_args.
|
|
114
|
+
_protected_args = click.Context.__dict__.get("protected_args")
|
|
115
|
+
if isinstance(_protected_args, property) and _protected_args.fset is None:
|
|
116
|
+
click.Context.protected_args = property(
|
|
117
|
+
_protected_args.fget,
|
|
118
|
+
lambda self, value: set_protected_args(self, value),
|
|
119
|
+
_protected_args.fdel,
|
|
120
|
+
_protected_args.__doc__,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# -----------------------------------------------------------------------
|
|
125
|
+
# Shell builtin interception for !-prefixed commands.
|
|
126
|
+
#
|
|
127
|
+
# os.system() runs in a subshell, so builtins like cd and export have no
|
|
128
|
+
# lasting effect. We intercept them and translate to Python equivalents,
|
|
129
|
+
# same approach as IPython's %cd / %env magics.
|
|
130
|
+
# -----------------------------------------------------------------------
|
|
131
|
+
def handle_shell_builtin(cmd_string):
|
|
132
|
+
"""Handle shell builtins that must run in-process. Returns True if handled."""
|
|
133
|
+
try:
|
|
134
|
+
tokens = shlex.split(cmd_string)
|
|
135
|
+
except ValueError:
|
|
136
|
+
tokens = cmd_string.split()
|
|
137
|
+
if not tokens:
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
builtin = tokens[0]
|
|
141
|
+
|
|
142
|
+
if builtin == "cd":
|
|
143
|
+
target = tokens[1] if len(tokens) > 1 else os.path.expanduser("~")
|
|
144
|
+
try:
|
|
145
|
+
os.chdir(os.path.expandvars(os.path.expanduser(target)))
|
|
146
|
+
except OSError as e:
|
|
147
|
+
click.echo(f"cd: {e}")
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
if builtin in ("set", "export"):
|
|
151
|
+
for arg in tokens[1:]:
|
|
152
|
+
if "=" in arg:
|
|
153
|
+
key, value = arg.split("=", 1)
|
|
154
|
+
os.environ[key] = value
|
|
155
|
+
else:
|
|
156
|
+
click.echo(os.environ.get(arg, f"{arg}: not set"))
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
if builtin == "unset":
|
|
160
|
+
for arg in tokens[1:]:
|
|
161
|
+
os.environ.pop(arg, None)
|
|
162
|
+
return True
|
|
163
|
+
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
_original_dispatch = _utils_mod.dispatch_repl_commands
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def patched_dispatch(command):
|
|
171
|
+
if command.startswith("!"):
|
|
172
|
+
cmd_string = command[1:].strip()
|
|
173
|
+
if handle_shell_builtin(cmd_string):
|
|
174
|
+
return True
|
|
175
|
+
return _original_dispatch(command)
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
_utils_mod.dispatch_repl_commands = patched_dispatch
|
|
180
|
+
|
|
181
|
+
# Register :e as an alias for :exit
|
|
182
|
+
_utils_mod._register_internal_command(
|
|
183
|
+
["e"], _utils_mod._exit_internal, "exits the repl"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# :? will be overridden per-session in run_shell to show the banner.
|
|
187
|
+
# We remove it from click-repl's default help binding here so it doesn't
|
|
188
|
+
# conflict; it gets re-registered in run_shell with the banner callback.
|
|
189
|
+
_utils_mod._internal_commands.pop("?", None)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def show_banner(title, version):
|
|
193
|
+
"""Display the shell welcome banner."""
|
|
194
|
+
console = make_console()
|
|
195
|
+
|
|
196
|
+
body = Text()
|
|
197
|
+
body.append("\n")
|
|
198
|
+
body.append("help ", style="bold cyan")
|
|
199
|
+
body.append("Show available commands\n")
|
|
200
|
+
body.append("exit", style="bold cyan")
|
|
201
|
+
body.append(" / ", style="dim")
|
|
202
|
+
body.append("quit", style="bold cyan")
|
|
203
|
+
body.append(" Leave the shell (also Ctrl+D)\n")
|
|
204
|
+
body.append("!<command> ", style="bold cyan")
|
|
205
|
+
body.append("Run a system shell command\n")
|
|
206
|
+
body.append("!cd <path> ", style="bold cyan")
|
|
207
|
+
body.append("Change working directory\n")
|
|
208
|
+
body.append("!set VAR=val ", style="bold cyan")
|
|
209
|
+
body.append("Set an environment variable\n")
|
|
210
|
+
body.append("!unset VAR ", style="bold cyan")
|
|
211
|
+
body.append("Remove an environment variable\n")
|
|
212
|
+
body.append(":?", style="bold cyan")
|
|
213
|
+
body.append(" Show this quick reference\n")
|
|
214
|
+
body.append(":help", style="bold cyan")
|
|
215
|
+
body.append(" / ", style="dim")
|
|
216
|
+
body.append(":exit", style="bold cyan")
|
|
217
|
+
body.append(" / ", style="dim")
|
|
218
|
+
body.append(":quit", style="bold cyan")
|
|
219
|
+
body.append(" Built-in commands (also :h / :e / :q)\n")
|
|
220
|
+
body.append("\n")
|
|
221
|
+
body.append("Press ", style="dim")
|
|
222
|
+
body.append("TAB", style="bold cyan")
|
|
223
|
+
body.append(" for command autocompletion and suggestions\n", style="dim")
|
|
224
|
+
body.append("Use ", style="dim")
|
|
225
|
+
body.append("ESC", style="bold cyan")
|
|
226
|
+
body.append(" or ", style="dim")
|
|
227
|
+
body.append("Ctrl+C", style="bold cyan")
|
|
228
|
+
body.append(" to abort input prompts", style="dim")
|
|
229
|
+
|
|
230
|
+
console.print()
|
|
231
|
+
console.print(
|
|
232
|
+
Panel(
|
|
233
|
+
body,
|
|
234
|
+
title=f"[bold]{title}[/bold] v{version}",
|
|
235
|
+
title_align="left",
|
|
236
|
+
border_style="#82c369",
|
|
237
|
+
padding=(0, 1),
|
|
238
|
+
expand=False,
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
console.print()
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class SessionInfo:
|
|
245
|
+
"""Tracks login state by watching connection.json."""
|
|
246
|
+
|
|
247
|
+
def __init__(self, tabsdata_directory, tool_name):
|
|
248
|
+
self._cred_path = os.path.join(tabsdata_directory, "connection.json")
|
|
249
|
+
self._tool_name = tool_name
|
|
250
|
+
self._mtime = None
|
|
251
|
+
self.user = None
|
|
252
|
+
self.server = None
|
|
253
|
+
self.refresh()
|
|
254
|
+
|
|
255
|
+
def refresh(self):
|
|
256
|
+
try:
|
|
257
|
+
mtime = os.path.getmtime(self._cred_path)
|
|
258
|
+
except OSError:
|
|
259
|
+
self._clear()
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
if mtime == self._mtime:
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
self._mtime = mtime
|
|
266
|
+
try:
|
|
267
|
+
with open(self._cred_path) as f:
|
|
268
|
+
creds = json.load(f)
|
|
269
|
+
parsed = urlparse(creds.get("url", ""))
|
|
270
|
+
self.server = parsed.hostname or "unknown"
|
|
271
|
+
if parsed.port:
|
|
272
|
+
self.server += f":{parsed.port}"
|
|
273
|
+
|
|
274
|
+
conn = APIServer(creds.get("url", ""))
|
|
275
|
+
conn.bearer_token = creds.get("bearer_token")
|
|
276
|
+
conn.refresh_token = creds.get("refresh_token")
|
|
277
|
+
conn.token_type = creds.get("token_type")
|
|
278
|
+
conn.expires_in = creds.get("expires_in")
|
|
279
|
+
conn.expiration_time = creds.get("expiration_time")
|
|
280
|
+
tds = TabsdataServer.__new__(TabsdataServer)
|
|
281
|
+
tds.connection = conn
|
|
282
|
+
info = tds.auth_info()
|
|
283
|
+
self.user = info.get("name")
|
|
284
|
+
except Exception:
|
|
285
|
+
self._clear()
|
|
286
|
+
|
|
287
|
+
def _clear(self):
|
|
288
|
+
self._mtime = None
|
|
289
|
+
self.user = None
|
|
290
|
+
self.server = None
|
|
291
|
+
|
|
292
|
+
@property
|
|
293
|
+
def logged_in(self):
|
|
294
|
+
return self.user is not None
|
|
295
|
+
|
|
296
|
+
def build_prompt(self):
|
|
297
|
+
self.refresh()
|
|
298
|
+
if self.logged_in:
|
|
299
|
+
return [
|
|
300
|
+
("#b4d296", self.server),
|
|
301
|
+
("dim", " - "),
|
|
302
|
+
("bold #b4d296", self.user),
|
|
303
|
+
("", " "),
|
|
304
|
+
("bold", self._tool_name),
|
|
305
|
+
("", " ❯ "),
|
|
306
|
+
]
|
|
307
|
+
else:
|
|
308
|
+
return [
|
|
309
|
+
("dim", "[not logged in]"),
|
|
310
|
+
("", " "),
|
|
311
|
+
("bold", self._tool_name),
|
|
312
|
+
("", " ❯ "),
|
|
313
|
+
]
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def run_shell(ctx, cli_group, version, *, banner_title, tool_name):
|
|
317
|
+
"""Run an interactive shell (REPL)."""
|
|
318
|
+
|
|
319
|
+
history_path = os.path.join(ctx.obj["tabsdata_directory"], f".{tool_name}.history")
|
|
320
|
+
|
|
321
|
+
_utils_mod._register_internal_command(
|
|
322
|
+
["?"],
|
|
323
|
+
lambda: show_banner(banner_title, version),
|
|
324
|
+
"shows the shell quick reference",
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
show_banner(banner_title, version)
|
|
328
|
+
|
|
329
|
+
# Register exit/quit/help so they work without the ':' prefix that
|
|
330
|
+
# click-repl normally requires for internal commands.
|
|
331
|
+
@click.command(hidden=True)
|
|
332
|
+
def exit():
|
|
333
|
+
"""Exit the interactive shell."""
|
|
334
|
+
raise ExitReplException()
|
|
335
|
+
|
|
336
|
+
@click.command(hidden=True)
|
|
337
|
+
@click.pass_context
|
|
338
|
+
def help(ctx):
|
|
339
|
+
"""Show available commands."""
|
|
340
|
+
click.echo(ctx.parent.get_help())
|
|
341
|
+
|
|
342
|
+
cli_group.add_command(exit, "exit")
|
|
343
|
+
cli_group.add_command(exit, "quit")
|
|
344
|
+
cli_group.add_command(help, "help")
|
|
345
|
+
|
|
346
|
+
session_info = SessionInfo(ctx.obj["tabsdata_directory"], tool_name)
|
|
347
|
+
|
|
348
|
+
# Key bindings: when the completion menu is visible, Enter accepts
|
|
349
|
+
# the selected completion instead of executing the current input.
|
|
350
|
+
td_bindings = KeyBindings()
|
|
351
|
+
|
|
352
|
+
@td_bindings.add(Keys.Enter, filter=True)
|
|
353
|
+
def _enter_handler(event):
|
|
354
|
+
buf = event.current_buffer
|
|
355
|
+
if buf.complete_state:
|
|
356
|
+
buf.complete_state = None
|
|
357
|
+
else:
|
|
358
|
+
buf.validate_and_handle()
|
|
359
|
+
|
|
360
|
+
@td_bindings.add(Keys.Escape, filter=True)
|
|
361
|
+
def _escape_handler(event):
|
|
362
|
+
buf = event.current_buffer
|
|
363
|
+
if buf.complete_state:
|
|
364
|
+
buf.cancel_completion()
|
|
365
|
+
|
|
366
|
+
td_style = Style.from_dict({
|
|
367
|
+
"completion-menu": "bg:default #82c369",
|
|
368
|
+
"completion-menu.completion": "bg:default #b4d296",
|
|
369
|
+
"completion-menu.completion.current": "bg:#2d3b28 #82c369 bold",
|
|
370
|
+
"completion-menu.meta.completion": "bg:default #6a9e55",
|
|
371
|
+
"completion-menu.meta.completion.current": "bg:#2d3b28 #82c369",
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
try:
|
|
375
|
+
repl(
|
|
376
|
+
click.get_current_context(),
|
|
377
|
+
prompt_kwargs={
|
|
378
|
+
"message": session_info.build_prompt,
|
|
379
|
+
"history": FileHistory(history_path),
|
|
380
|
+
"style": td_style,
|
|
381
|
+
"key_bindings": td_bindings,
|
|
382
|
+
"complete_while_typing": False,
|
|
383
|
+
},
|
|
384
|
+
allow_system_commands=True,
|
|
385
|
+
allow_internal_commands=True,
|
|
386
|
+
)
|
|
387
|
+
finally:
|
|
388
|
+
cli_group.commands.pop("exit", None)
|
|
389
|
+
cli_group.commands.pop("quit", None)
|
|
390
|
+
cli_group.commands.pop("help", None)
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2026 Tabsdata Inc.
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import rich_click as click
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from tabsdatak.cli._registry import cli_command
|
|
14
|
+
from tabsdatak.cli.cli_utils import (
|
|
15
|
+
beautify_list,
|
|
16
|
+
format_option,
|
|
17
|
+
logical_prompt,
|
|
18
|
+
make_console,
|
|
19
|
+
render,
|
|
20
|
+
verify_login_or_prompt,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@cli_command
|
|
25
|
+
@click.group()
|
|
26
|
+
def auth():
|
|
27
|
+
"""User session management commands"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@auth.command()
|
|
31
|
+
@click.option("--server", "-s", help="Tabsdata Server URL")
|
|
32
|
+
@click.option(
|
|
33
|
+
"--pem",
|
|
34
|
+
help="Path to the certificate PEM file, or '-' to read it from stdin.",
|
|
35
|
+
)
|
|
36
|
+
@click.pass_context
|
|
37
|
+
def add_cert(ctx: click.Context, server: str, pem: str):
|
|
38
|
+
"""
|
|
39
|
+
Add a certificate for a Tabsdata Server.
|
|
40
|
+
Only needed for self-signed certificates.
|
|
41
|
+
"""
|
|
42
|
+
# noinspection PyProtectedMember
|
|
43
|
+
from tabsdatak.sdk._apiserver import (
|
|
44
|
+
DEFAULT_TABSDATA_CERTIFICATE_FOLDER,
|
|
45
|
+
HTTP_PROTOCOL,
|
|
46
|
+
HTTPS_PROTOCOL,
|
|
47
|
+
_obtain_certificate_file_path,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
server = server or logical_prompt(ctx, "Tabsdata Server URL")
|
|
51
|
+
pem = pem or logical_prompt(ctx, "Path to the certificate PEM file")
|
|
52
|
+
click.echo("Adding certificate")
|
|
53
|
+
click.echo("-" * 10)
|
|
54
|
+
try:
|
|
55
|
+
if not server.startswith(HTTP_PROTOCOL) and not server.startswith(
|
|
56
|
+
HTTPS_PROTOCOL
|
|
57
|
+
):
|
|
58
|
+
server = HTTPS_PROTOCOL + server
|
|
59
|
+
certificate_path = _obtain_certificate_file_path(server)
|
|
60
|
+
os.makedirs(DEFAULT_TABSDATA_CERTIFICATE_FOLDER, exist_ok=True)
|
|
61
|
+
if pem == "-":
|
|
62
|
+
Path(certificate_path).write_text(sys.stdin.read())
|
|
63
|
+
else:
|
|
64
|
+
shutil.copy(pem, certificate_path)
|
|
65
|
+
click.echo(f"Certificate added successfully for server '{server}'")
|
|
66
|
+
click.echo(f"Certificate stored at '{certificate_path}'")
|
|
67
|
+
except Exception as e:
|
|
68
|
+
raise click.ClickException(f"Failed to add certificate for server: {e}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@auth.command()
|
|
72
|
+
@click.option("--server", "-s", help="Tabsdata Server URL")
|
|
73
|
+
@click.pass_context
|
|
74
|
+
def delete_cert(ctx: click.Context, server: str):
|
|
75
|
+
"""Delete a certificate for a Tabsdata Server"""
|
|
76
|
+
# noinspection PyProtectedMember
|
|
77
|
+
from tabsdatak.sdk._apiserver import (
|
|
78
|
+
HTTP_PROTOCOL,
|
|
79
|
+
HTTPS_PROTOCOL,
|
|
80
|
+
_obtain_certificate_file_path,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
server = server or logical_prompt(ctx, "Tabsdata Server URL")
|
|
84
|
+
click.echo("Deleting certificate")
|
|
85
|
+
click.echo("-" * 10)
|
|
86
|
+
try:
|
|
87
|
+
if not server.startswith(HTTP_PROTOCOL) and not server.startswith(
|
|
88
|
+
HTTPS_PROTOCOL
|
|
89
|
+
):
|
|
90
|
+
server = HTTPS_PROTOCOL + server
|
|
91
|
+
certificate_path = _obtain_certificate_file_path(server)
|
|
92
|
+
if os.path.exists(certificate_path):
|
|
93
|
+
os.remove(certificate_path)
|
|
94
|
+
click.echo(f"Certificate deleted successfully for server '{server}'")
|
|
95
|
+
else:
|
|
96
|
+
click.echo(
|
|
97
|
+
"No certificate found for the specified server. It should be "
|
|
98
|
+
f"stored at '{certificate_path}'."
|
|
99
|
+
)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
raise click.ClickException(f"Failed to delete certificate for server: {e}")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@auth.command()
|
|
105
|
+
@format_option
|
|
106
|
+
@click.pass_context
|
|
107
|
+
def info(ctx: click.Context):
|
|
108
|
+
"""Display information of the currently authenticated user"""
|
|
109
|
+
verify_login_or_prompt(ctx)
|
|
110
|
+
try:
|
|
111
|
+
info = ctx.obj["tabsdataserver"].auth_info()
|
|
112
|
+
|
|
113
|
+
def _table():
|
|
114
|
+
table = Table(title="Current authenticated user")
|
|
115
|
+
table.add_column("Name", style="cyan", no_wrap=True)
|
|
116
|
+
table.add_column("Email")
|
|
117
|
+
table.add_column("Roles")
|
|
118
|
+
table.add_row(
|
|
119
|
+
info["name"],
|
|
120
|
+
info["email"],
|
|
121
|
+
beautify_list(info["roles"]),
|
|
122
|
+
)
|
|
123
|
+
click.echo()
|
|
124
|
+
make_console().print(table)
|
|
125
|
+
click.echo()
|
|
126
|
+
|
|
127
|
+
render(ctx, data=info, table=_table)
|
|
128
|
+
except Exception as e:
|
|
129
|
+
raise click.ClickException(f"Failed to display current user information: {e}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@auth.command()
|
|
133
|
+
@click.argument("server-url")
|
|
134
|
+
@click.option(
|
|
135
|
+
"--user",
|
|
136
|
+
"-u",
|
|
137
|
+
help="Username for the Tabsdata Server. Will be prompted for it if not provided.",
|
|
138
|
+
)
|
|
139
|
+
@click.option(
|
|
140
|
+
"--old-password",
|
|
141
|
+
"-p",
|
|
142
|
+
help=(
|
|
143
|
+
"Old password for the Tabsdata Server. It is discouraged to send the password "
|
|
144
|
+
"as a "
|
|
145
|
+
"plain argument, it should be either sent as the value of an environment "
|
|
146
|
+
"variable or written through the prompt. Will be prompted for it if not "
|
|
147
|
+
"provided."
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
@click.option(
|
|
151
|
+
"--new-password",
|
|
152
|
+
"-n",
|
|
153
|
+
help=(
|
|
154
|
+
"New password for the Tabsdata Server. It is discouraged to send the password "
|
|
155
|
+
"as a "
|
|
156
|
+
"plain argument, it should be either sent as the value of an environment "
|
|
157
|
+
"variable or written through the prompt. Will be prompted for it if not "
|
|
158
|
+
"provided."
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
@click.pass_context
|
|
162
|
+
def password_change(
|
|
163
|
+
ctx: click.Context, server_url: str, user: str, old_password: str, new_password: str
|
|
164
|
+
):
|
|
165
|
+
"""Change the password of a user in a Tabsdata Server"""
|
|
166
|
+
user = user or logical_prompt(ctx, "Username for the Tabsdata Server")
|
|
167
|
+
password = old_password or logical_prompt(
|
|
168
|
+
ctx,
|
|
169
|
+
"Old password",
|
|
170
|
+
hide_input=True,
|
|
171
|
+
)
|
|
172
|
+
if not new_password:
|
|
173
|
+
new_password = logical_prompt(
|
|
174
|
+
ctx,
|
|
175
|
+
"New password",
|
|
176
|
+
hide_input=True,
|
|
177
|
+
)
|
|
178
|
+
new_pass_verification = logical_prompt(
|
|
179
|
+
ctx, "(Repeat) New password", hide_input=True
|
|
180
|
+
)
|
|
181
|
+
if new_password != new_pass_verification:
|
|
182
|
+
raise click.ClickException(
|
|
183
|
+
"Two different new passwords were entered. "
|
|
184
|
+
"Please provide the same password twice."
|
|
185
|
+
)
|
|
186
|
+
from tabsdatak.sdk.tabsdata_server import TabsdataServer
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
# We create a new TabsdataServer instance to avoid using the current session
|
|
190
|
+
# credentials, which might result in a total deadlock if the old password is
|
|
191
|
+
# no longer valid, so the user must log in to change the password, but can't
|
|
192
|
+
# log in until the password is changed.
|
|
193
|
+
server = TabsdataServer(server_url)
|
|
194
|
+
server.change_password(user, password, new_password)
|
|
195
|
+
click.echo("Password changed successfully.")
|
|
196
|
+
except Exception as e:
|
|
197
|
+
raise click.ClickException(f"Failed to change password: {e}")
|