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.
Files changed (39) hide show
  1. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/__init__.py +5 -0
  2. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/_registry.py +78 -0
  3. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/assets/manifest/BANNER +5 -0
  4. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/assets/manifest/LICENSE +3 -0
  5. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/assets/manifest/README-PyPi.md +8 -0
  6. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/assets/manifest/RELEASE-NOTES +1 -0
  7. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/assets/manifest/THIRD-PARTY +3 -0
  8. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/assets/manifest/VERSION +1 -0
  9. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/cli_utils.py +993 -0
  10. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/shell.py +390 -0
  11. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/__init__.py +3 -0
  12. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/auth_group.py +197 -0
  13. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/cli.py +508 -0
  14. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/collection_group.py +458 -0
  15. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/config_utils.py +93 -0
  16. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/connection_group.py +159 -0
  17. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/examples_group.py +206 -0
  18. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/execution_utils.py +429 -0
  19. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/fn_group.py +1109 -0
  20. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/fnrun_group.py +285 -0
  21. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/group_group.py +184 -0
  22. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/metrics_group.py +323 -0
  23. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/permission_group.py +259 -0
  24. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/plan_group.py +324 -0
  25. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/project_group.py +170 -0
  26. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/role_group.py +630 -0
  27. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/settings_group.py +344 -0
  28. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/table_group.py +600 -0
  29. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/trigger_group.py +338 -0
  30. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/trx_group.py +329 -0
  31. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/user_group.py +200 -0
  32. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/td/worker_group.py +185 -0
  33. tabsdata_cli-2.0.0.data/purelib/tabsdatak/cli/template.py +781 -0
  34. tabsdata_cli-2.0.0.data/purelib/tabsdatak/py.typed +0 -0
  35. tabsdata_cli-2.0.0.dist-info/METADATA +39 -0
  36. tabsdata_cli-2.0.0.dist-info/RECORD +39 -0
  37. tabsdata_cli-2.0.0.dist-info/WHEEL +5 -0
  38. tabsdata_cli-2.0.0.dist-info/licenses/src/main/python/tabsdatak/cli/assets/manifest/LICENSE +3 -0
  39. 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,3 @@
1
+ #
2
+ # Copyright 2026 Tabsdata Inc.
3
+ #
@@ -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}")