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,429 @@
1
+ #
2
+ # Copyright 2026 Tabsdata Inc.
3
+ #
4
+
5
+ """Shared helpers for the execution-family command groups
6
+ (``plan``, ``trx``, ``run``, ``worker``)."""
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from itertools import islice
12
+ from time import sleep
13
+ from typing import TYPE_CHECKING, Callable, List
14
+
15
+ import rich_click as click
16
+ from rich.live import Live
17
+ from rich.table import Table
18
+
19
+ from tabsdatak.cli.cli_utils import (
20
+ beautify_time,
21
+ collection_label,
22
+ is_valid_id,
23
+ logical_prompt,
24
+ make_console,
25
+ output_format,
26
+ render,
27
+ take,
28
+ )
29
+ from tabsdatak.sdk.status_utils.execution import user_execution_status_to_api
30
+ from tabsdatak.sdk.status_utils.function_run import user_function_run_status_to_api
31
+ from tabsdatak.sdk.status_utils.transaction import user_transaction_status_to_api
32
+ from tabsdatak.sdk.status_utils.worker import user_worker_status_to_api
33
+
34
+ if TYPE_CHECKING:
35
+ from tabsdatak.sdk.tabsdata_server import TabsdataServer
36
+
37
+
38
+ def span_projects(server: TabsdataServer) -> None:
39
+ """The execution-family commands are id-keyed and their listings always
40
+ span every visible project; a pinned or default project must not narrow
41
+ them (narrowing is done with an explicit ``project:eq:`` filter)."""
42
+ server.connection.project = None
43
+
44
+
45
+ def _append_at_filter(request_filter: List[str], at: str) -> None:
46
+ # noinspection PyProtectedMember
47
+ from tabsdatak.sdk.tabsdata_server import _top_and_convert_to_timestamp
48
+
49
+ try:
50
+ at_timestamp = _top_and_convert_to_timestamp(at)
51
+ request_filter.append(f"triggered_on:le:{at_timestamp}")
52
+ except ValueError as e:
53
+ raise click.ClickException(f"Invalid date-time format for 'at' option: {e}")
54
+
55
+
56
+ def obtain_list_exec_filters(
57
+ status: List[str] = None,
58
+ exe_name: str = None,
59
+ fn: str = None,
60
+ collection: str = None,
61
+ at: str = None,
62
+ project: str = None,
63
+ ) -> List[str]:
64
+ """
65
+ Helper function to obtain the filters for listing plans.
66
+ """
67
+ request_filter = []
68
+ if status:
69
+ request_filter.extend(
70
+ f"status:eq:{user_execution_status_to_api(s)}" for s in status
71
+ )
72
+ if exe_name:
73
+ request_filter.append(f"name:lk:{exe_name}")
74
+ if fn:
75
+ request_filter.append(f"function:eq:{fn}")
76
+ if collection:
77
+ request_filter.append(f"collection:eq:{collection}")
78
+ if at:
79
+ _append_at_filter(request_filter, at)
80
+ if project:
81
+ request_filter.append(f"project:eq:{project}")
82
+ return request_filter
83
+
84
+
85
+ def obtain_list_trx_filters(
86
+ status: List[str] = None,
87
+ exe_name: str = None,
88
+ execution_id: str = None,
89
+ collection: str = None,
90
+ at: str = None,
91
+ project: str = None,
92
+ ) -> List[str]:
93
+ """
94
+ Helper function to obtain the filters for listing transactions.
95
+ """
96
+ request_filter = []
97
+ if status:
98
+ request_filter.extend(
99
+ f"status:eq:{user_transaction_status_to_api(s)}" for s in status
100
+ )
101
+ if exe_name:
102
+ request_filter.append(f"execution:lk:{exe_name}")
103
+ if execution_id:
104
+ request_filter.append(f"execution_id:eq:{execution_id}")
105
+ if collection:
106
+ request_filter.append(f"collection:eq:{collection}")
107
+ if at:
108
+ _append_at_filter(request_filter, at)
109
+ if project:
110
+ request_filter.append(f"project:eq:{project}")
111
+ return request_filter
112
+
113
+
114
+ def obtain_list_fn_run_filters(
115
+ status: List[str] = None,
116
+ exe_name: str = None,
117
+ execution_id: str = None,
118
+ fn: str = None,
119
+ collection: str = None,
120
+ trx: str = None,
121
+ project: str = None,
122
+ ) -> List[str]:
123
+ """
124
+ Helper function to obtain the filters for listing function runs.
125
+ """
126
+ request_filter = []
127
+ if status:
128
+ request_filter.extend(
129
+ f"status:eq:{user_function_run_status_to_api(s)}" for s in status
130
+ )
131
+ if exe_name:
132
+ request_filter.append(f"execution:lk:{exe_name}")
133
+ if execution_id:
134
+ request_filter.append(f"execution_id:eq:{execution_id}")
135
+ if fn:
136
+ request_filter.append(f"name:eq:{fn}")
137
+ if collection:
138
+ request_filter.append(f"collection:eq:{collection}")
139
+ if trx:
140
+ request_filter.append(f"transaction_id:eq:{trx}")
141
+ if project:
142
+ request_filter.append(f"project:eq:{project}")
143
+ return request_filter
144
+
145
+
146
+ def obtain_list_worker_filters(
147
+ status: List[str] = None,
148
+ exe_name: str = None,
149
+ execution_id: str = None,
150
+ fn: str = None,
151
+ fnrun: str = None,
152
+ collection: str = None,
153
+ trx: str = None,
154
+ worker_type: str = None,
155
+ project: str = None,
156
+ ) -> List[str]:
157
+ """
158
+ Helper function to obtain the filters for listing workers.
159
+ """
160
+ request_filter = []
161
+ if status:
162
+ request_filter.extend(
163
+ f"status:eq:{user_worker_status_to_api(s)}" for s in status
164
+ )
165
+ if exe_name:
166
+ request_filter.append(f"execution:lk:{exe_name}")
167
+ if execution_id:
168
+ request_filter.append(f"execution_id:eq:{execution_id}")
169
+ if fn:
170
+ request_filter.append(f"function:eq:{fn}")
171
+ if fnrun:
172
+ request_filter.append(f"logical_function_run_id:eq:{fnrun}")
173
+ if collection:
174
+ request_filter.append(f"collection:eq:{collection}")
175
+ if trx:
176
+ request_filter.append(f"transaction_id:eq:{trx}")
177
+ if worker_type:
178
+ request_filter.append(f"worker_type:eq:{worker_type}")
179
+ if project:
180
+ request_filter.append(f"project:eq:{project}")
181
+ return request_filter
182
+
183
+
184
+ def obtain_execution_list(
185
+ ctx: click.Context, server: TabsdataServer, request_filter, last: bool
186
+ ) -> tuple[list, bool]:
187
+ executions = server.list_executions_generator(
188
+ filter=request_filter,
189
+ order_by="triggered_on-",
190
+ )
191
+ # The listing is newest-first, so --last needs a single row, never a page.
192
+ if last:
193
+ return list(islice(executions, 1)), False
194
+ return take(ctx, executions)
195
+
196
+
197
+ def obtain_transaction_list(
198
+ ctx: click.Context, server: TabsdataServer, request_filter, last: bool
199
+ ) -> tuple[list, bool]:
200
+ transactions = server.list_transactions_generator(
201
+ filter=request_filter, order_by="triggered_on-"
202
+ )
203
+ if last:
204
+ return list(islice(transactions, 1)), False
205
+ return take(ctx, transactions)
206
+
207
+
208
+ def echo_plan_status(server: TabsdataServer, plan: str) -> None:
209
+ execution = server.get_execution(plan)
210
+ click.echo(f"The current status of the plan '{plan}' is '{execution.status}'")
211
+
212
+
213
+ def echo_transaction_status(server: TabsdataServer, trx: str) -> None:
214
+ transaction = server.get_transaction(trx)
215
+ click.echo(
216
+ f"The current status of the transaction '{trx}' is '{transaction.status}'"
217
+ )
218
+
219
+
220
+ def echo_function_run_status(server: TabsdataServer, fnrun: str) -> None:
221
+ function_run = server.get_function_run(fnrun)
222
+ click.echo(
223
+ f"The current status of the function run '{fnrun}' is '{function_run.status}'"
224
+ )
225
+
226
+
227
+ def patch_label(item) -> str:
228
+ """The patch a run/worker executed (a function-version id), or '-' for head
229
+ code. The patch id is surfaced onto the row as `function_version_patch_id`;
230
+ absent/None means unpatched (head)."""
231
+ return item.kwargs.get("function_version_patch_id") or "-"
232
+
233
+
234
+ def _truncate(text: str, width: int = 20) -> str:
235
+ text = text or ""
236
+ return text if len(text) <= width else text[:width] + "..."
237
+
238
+
239
+ def plans_table(executions, title: str = "Plans") -> Table:
240
+ table = Table(title=title)
241
+ table.add_column("ID", style="cyan", no_wrap=True)
242
+ table.add_column("Name")
243
+ table.add_column("Project")
244
+ table.add_column("Trigger coll / fn", no_wrap=True)
245
+ table.add_column("Triggered by")
246
+ table.add_column("Start time", no_wrap=True)
247
+ table.add_column("End time", no_wrap=True)
248
+ table.add_column("Status", no_wrap=True)
249
+ for execution in executions:
250
+ table.add_row(
251
+ execution.id,
252
+ _truncate(execution.name),
253
+ execution.project or "-",
254
+ f"{collection_label(execution)} / {execution.function.name}",
255
+ execution.triggered_by,
256
+ beautify_time(execution.started_on_str),
257
+ beautify_time(execution.ended_on_str),
258
+ execution.status,
259
+ )
260
+ return table
261
+
262
+
263
+ def transactions_table(transactions) -> Table:
264
+ table = Table(title="Transactions")
265
+ table.add_column("ID", style="cyan", no_wrap=True)
266
+ table.add_column("Execution plan ID", no_wrap=True)
267
+ table.add_column("Project")
268
+ table.add_column("Collection")
269
+ table.add_column("Triggered by")
270
+ table.add_column("Start time", no_wrap=True)
271
+ table.add_column("End time", no_wrap=True)
272
+ table.add_column("Status", no_wrap=True)
273
+ for transaction in transactions:
274
+ table.add_row(
275
+ transaction.id,
276
+ transaction.execution.id,
277
+ transaction.project or "-",
278
+ collection_label(transaction),
279
+ transaction.triggered_by,
280
+ beautify_time(transaction.started_on_str),
281
+ beautify_time(transaction.ended_on_str),
282
+ transaction.status,
283
+ )
284
+ return table
285
+
286
+
287
+ def function_runs_table(function_runs, title: str = "Function runs") -> Table:
288
+ table = Table(title=title)
289
+ table.add_column("Function run ID", style="cyan", no_wrap=True)
290
+ table.add_column("Function")
291
+ table.add_column("Execution plan ID", no_wrap=True)
292
+ table.add_column("Transaction ID", no_wrap=True)
293
+ table.add_column("Project")
294
+ table.add_column("Collection")
295
+ table.add_column("Start time", no_wrap=True)
296
+ table.add_column("End time", no_wrap=True)
297
+ table.add_column("Patch")
298
+ table.add_column("Status")
299
+ for fn_run in function_runs:
300
+ table.add_row(
301
+ fn_run.id,
302
+ fn_run.function.name,
303
+ fn_run.execution.id,
304
+ fn_run.transaction.id,
305
+ fn_run.project or "-",
306
+ collection_label(fn_run),
307
+ beautify_time(fn_run.started_on_str),
308
+ beautify_time(fn_run.ended_on_str),
309
+ patch_label(fn_run),
310
+ fn_run.status,
311
+ )
312
+ return table
313
+
314
+
315
+ def workers_table(workers, title: str = "Workers", indexed: bool = False) -> Table:
316
+ table = Table(title=title)
317
+ if indexed:
318
+ table.add_column("Index")
319
+ table.add_column("Worker ID", style="cyan", no_wrap=True)
320
+ table.add_column("Function run ID", no_wrap=True)
321
+ table.add_column("Function")
322
+ table.add_column("Execution plan ID", no_wrap=True)
323
+ table.add_column("Transaction ID", no_wrap=True)
324
+ table.add_column("Project")
325
+ table.add_column("Collection")
326
+ table.add_column("Start time", no_wrap=True)
327
+ table.add_column("End time", no_wrap=True)
328
+ table.add_column("Patch")
329
+ table.add_column("Status")
330
+ for position, worker in enumerate(workers, start=1):
331
+ row = [str(position)] if indexed else []
332
+ row.extend([
333
+ worker.id,
334
+ worker.kwargs.get("logical_function_run_id") or "-",
335
+ worker.function.name,
336
+ worker.kwargs.get("execution_id") or "-",
337
+ worker.kwargs.get("transaction_id") or "-",
338
+ worker.project or "-",
339
+ collection_label(worker),
340
+ beautify_time(worker.started_on_str),
341
+ beautify_time(worker.ended_on_str),
342
+ patch_label(worker),
343
+ worker.status,
344
+ ])
345
+ table.add_row(*row)
346
+ return table
347
+
348
+
349
+ def live_monitor(items, fetch: Callable, build_table: Callable, is_final: Callable):
350
+ """Refresh a listing until every item reaches a final status; returns the
351
+ final listing."""
352
+ refresh_rate = 1 # seconds
353
+ with Live(
354
+ build_table(items), refresh_per_second=refresh_rate, console=make_console()
355
+ ) as live:
356
+ while not all(is_final(item) for item in items):
357
+ items = fetch()
358
+ live.update(build_table(items))
359
+ sleep(1 / refresh_rate)
360
+ items = fetch()
361
+ live.update(build_table(items))
362
+ return items
363
+
364
+
365
+ def pick_worker(
366
+ ctx: click.Context, server: TabsdataServer, request_filter: List[str], subject: str
367
+ ) -> str:
368
+ """Resolve a worker to show logs for: the only one directly, or one the
369
+ user picks from the rendered listing when several ran."""
370
+ workers = server.list_workers(filter=request_filter)
371
+ if not workers:
372
+ raise click.ClickException(f"{subject} has no workers yet — nothing has run.")
373
+ if len(workers) == 1:
374
+ return workers[0].id
375
+ if output_format(ctx) == "table":
376
+ click.echo()
377
+ make_console().print(workers_table(workers, indexed=True))
378
+ if output_format(ctx) != "table" or ctx.obj["no_prompt"]:
379
+ raise click.ClickException(
380
+ f"{subject} has {len(workers)} workers. Pick one and run "
381
+ "'tdk worker logs --worker <WORKER-ID>'."
382
+ )
383
+ choice = logical_prompt(ctx, "Index or ID of the worker to show logs for")
384
+ choice = (choice or "").strip()
385
+ if is_valid_id(choice):
386
+ return choice
387
+ try:
388
+ index = int(choice)
389
+ except ValueError:
390
+ raise click.ClickException(
391
+ f"'{choice}' is neither a worker ID nor an index in the list."
392
+ )
393
+ if not 1 <= index <= len(workers):
394
+ raise click.ClickException(f"Index {index} is out of range (1-{len(workers)}).")
395
+ return workers[index - 1].id
396
+
397
+
398
+ def emit_worker_logs(
399
+ ctx: click.Context, server: TabsdataServer, worker_id: str, file: str
400
+ ) -> None:
401
+ generated_logs = (
402
+ f"Worker {worker_id}" + os.linesep + server.get_worker_log(worker_id)
403
+ )
404
+ if file == "-":
405
+ click.echo(generated_logs, nl=False)
406
+ return
407
+ if output_format(ctx) != "table":
408
+ if file:
409
+ with open(file, "w") as f:
410
+ f.write(generated_logs)
411
+ render(ctx, data={"logs": generated_logs}, table=lambda: None)
412
+ return
413
+ if file:
414
+ click.echo(f"Saving logs to file '{file}'")
415
+ with open(file, "w") as f:
416
+ f.write(generated_logs)
417
+ click.echo("Logs saved successfully")
418
+ else:
419
+ click.echo(generated_logs)
420
+
421
+
422
+ def confirm_cancel(ctx: click.Context, confirm: str, subject: str) -> None:
423
+ confirm = confirm or logical_prompt(
424
+ ctx, f"Please type 'cancel' to confirm canceling {subject}"
425
+ )
426
+ if confirm != "cancel":
427
+ raise click.ClickException(
428
+ "Cancellation not confirmed. The confirmation word is 'cancel'."
429
+ )