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,1109 @@
1
+ #
2
+ # Copyright 2026 Tabsdata Inc.
3
+ #
4
+
5
+ from __future__ import annotations
6
+
7
+ from time import sleep
8
+ from typing import TYPE_CHECKING
9
+
10
+ import rich_click as click
11
+ from rich.live import Live
12
+ from rich.table import Table
13
+
14
+ from tabsdatak.cli._registry import cli_command
15
+ from tabsdatak.cli.cli_utils import (
16
+ beautify_list,
17
+ beautify_time,
18
+ collection_label,
19
+ echo_more_results,
20
+ format_option,
21
+ get_currently_pinned_object,
22
+ hint_common_solutions,
23
+ load_info_and_project_root,
24
+ logical_prompt,
25
+ make_console,
26
+ max_result_option,
27
+ proj_option,
28
+ render,
29
+ show_hint,
30
+ take,
31
+ verify_login_or_prompt,
32
+ )
33
+ from tabsdatak.sdk.status_utils.execution import EXECUTION_FINAL_STATUSES
34
+ from tabsdatak.sdk.status_utils.function_run import (
35
+ FUNCTION_RUN_SUCCESSFUL_FINAL_STATUSES,
36
+ )
37
+ from tabsdatak.sdk.status_utils.transaction import TRANSACTION_FINAL_STATUSES
38
+
39
+ if TYPE_CHECKING:
40
+ from tabsdatak.sdk.tabsdata_server import Execution, TabsdataServer, Transaction
41
+
42
+
43
+ _FUNCTION_STATUS_LABELS = {"A": "Active", "F": "Frozen", "D": "Deleted"}
44
+
45
+
46
+ @cli_command
47
+ @click.group()
48
+ def fn():
49
+ """Function management commands"""
50
+
51
+
52
+ @fn.command()
53
+ @proj_option
54
+ @click.option(
55
+ "--name",
56
+ "-n",
57
+ help="Name of the function to be deleted.",
58
+ )
59
+ @click.option(
60
+ "--coll",
61
+ "-c",
62
+ help="Name of the collection to which the function belongs.",
63
+ )
64
+ @click.option(
65
+ "--confirm",
66
+ help="Write 'delete' to confirm deletion. Will be prompted for it if not provided.",
67
+ )
68
+ @click.pass_context
69
+ def delete(ctx: click.Context, name: str, coll: str, confirm: str):
70
+ """Delete a function"""
71
+ verify_login_or_prompt(ctx)
72
+ name = name or logical_prompt(ctx, "Name of the function to be deleted")
73
+ coll = (
74
+ coll
75
+ or get_currently_pinned_object(ctx, "collection")
76
+ or logical_prompt(ctx, "Name of the collection to which the function belongs")
77
+ )
78
+ click.echo(f"Deleting function '{name}' in collection '{coll}'")
79
+ click.echo("-" * 10)
80
+ confirm = confirm or logical_prompt(ctx, "Please type 'delete' to confirm deletion")
81
+ if confirm != "delete":
82
+ raise click.ClickException(
83
+ "Deletion not confirmed. The confirmation word is 'delete'."
84
+ )
85
+ try:
86
+ server: TabsdataServer = ctx.obj["tabsdataserver"]
87
+ server.delete_function(coll, name)
88
+ click.echo("Function deleted successfully")
89
+ show_hint(
90
+ ctx,
91
+ "You do not need to delete and re-register a function to change it. "
92
+ "Use 'tdk fn update' to modify the function instead.",
93
+ )
94
+ except Exception as e:
95
+ hint_common_solutions(ctx, e)
96
+ raise click.ClickException(f"Failed to delete function: {e}")
97
+
98
+
99
+ @fn.command()
100
+ @proj_option
101
+ @format_option
102
+ @click.option(
103
+ "--name",
104
+ "-n",
105
+ help="Name of the function to be displayed.",
106
+ )
107
+ @click.option(
108
+ "--coll",
109
+ "-c",
110
+ help="Name of the collection to which the function belongs.",
111
+ )
112
+ @click.option(
113
+ "--show-history",
114
+ is_flag=True,
115
+ help="Show the history of the function, newest first.",
116
+ )
117
+ @click.pass_context
118
+ def get(ctx: click.Context, name: str, coll: str, show_history: bool):
119
+ """Display a function"""
120
+ verify_login_or_prompt(ctx)
121
+ name = name or logical_prompt(ctx, "Name of the function to be displayed")
122
+ coll = (
123
+ coll
124
+ or get_currently_pinned_object(ctx, "collection")
125
+ or logical_prompt(ctx, "Name of the collection to which the function belongs")
126
+ )
127
+ try:
128
+ server: TabsdataServer = ctx.obj["tabsdataserver"]
129
+ if show_history:
130
+ function_list = server.list_function_history(coll, name)
131
+
132
+ def _table():
133
+ table = Table(title="History")
134
+ table.add_column("ID", style="cyan", no_wrap=True)
135
+ table.add_column("Patches", no_wrap=True)
136
+ table.add_column("Name")
137
+ table.add_column("Description")
138
+ table.add_column("Created by")
139
+ table.add_column("Created on")
140
+ table.add_column("Dependencies")
141
+ table.add_column("Triggers (functions)")
142
+ table.add_column("Tables")
143
+ for function in function_list:
144
+ table.add_row(
145
+ str(function.id),
146
+ str(function.patch_of or ""),
147
+ function.name,
148
+ function.description,
149
+ function.defined_by,
150
+ function.defined_on_str,
151
+ beautify_list(function.dependencies),
152
+ beautify_list(function.triggers),
153
+ beautify_list([t.name for t in function.tables]),
154
+ )
155
+ click.echo()
156
+ make_console().print(table)
157
+ click.echo(f"Number of versions: {len(function_list)}")
158
+ click.echo()
159
+
160
+ render(
161
+ ctx,
162
+ data=lambda: [f._data for f in function_list],
163
+ table=_table,
164
+ )
165
+ else:
166
+ # noinspection PyProtectedMember
167
+ from tabsdatak.sdk.tabsdata_server import _function_type_to_mapping
168
+
169
+ function = server.get_function(coll, name)
170
+
171
+ def _table():
172
+ data = function._data or {}
173
+ table = Table(
174
+ title="Function",
175
+ show_header=False,
176
+ show_lines=True,
177
+ )
178
+ table.add_column("Field", style="cyan", no_wrap=True)
179
+ table.add_column("Value")
180
+ table.add_row("Name:", function.name)
181
+ table.add_row("Description:", function.description or "")
182
+ table.add_row("Project:", server.connection.project or "-")
183
+ table.add_row("Collection:", coll)
184
+ table.add_row("Created by:", function.defined_by or "-")
185
+ table.add_row("Created on:", function.defined_on_str or "-")
186
+ table.add_row(
187
+ "Status:",
188
+ _FUNCTION_STATUS_LABELS.get(
189
+ data.get("status"), data.get("status") or "-"
190
+ ),
191
+ )
192
+ table.add_row("Enabled:", "Yes" if function.enabled else "No")
193
+ table.add_row("Type:", _function_type_to_mapping(function.type))
194
+ table.add_row(
195
+ "Tables:", beautify_list([t.name for t in function.tables])
196
+ )
197
+ table.add_row("Triggers:", beautify_list(function.triggers))
198
+ table.add_row("Dependencies:", beautify_list(function.dependencies))
199
+ table.add_row("Snippet:", data.get("snippet") or "")
200
+ click.echo()
201
+ # emoji/markup off: the snippet is Python source with ':'/'[]'
202
+ # that rich would otherwise treat as an emoji shortcode / markup.
203
+ make_console(emoji=False, markup=False).print(table)
204
+ click.echo()
205
+
206
+ render(ctx, data=lambda: function._data, table=_table)
207
+
208
+ except Exception as e:
209
+ hint_common_solutions(ctx, e)
210
+ raise click.ClickException(f"Failed to display function: {e}")
211
+
212
+
213
+ @fn.command()
214
+ @proj_option
215
+ @click.option(
216
+ "--fn-version",
217
+ "fn_version",
218
+ help=(
219
+ "Function version id (fvid) whose bundle will be downloaded. Use "
220
+ "'tdk fn get --show-history' to find available fvids."
221
+ ),
222
+ )
223
+ @click.option(
224
+ "--file",
225
+ "-f",
226
+ "file",
227
+ help=(
228
+ "Path to write the downloaded bundle to. The bundle is a "
229
+ "zstandard-compressed tar archive (.tar.zst)."
230
+ ),
231
+ )
232
+ @click.pass_context
233
+ def bundle(ctx: click.Context, fn_version: str, file: str):
234
+ """Download the bundle of a function version as a tar.zst archive.
235
+
236
+ The bundle is addressed by its function version id (fvid); only Active or
237
+ Frozen versions can be downloaded (a version whose function has been
238
+ deleted is not found).
239
+ """
240
+ verify_login_or_prompt(ctx)
241
+ fn_version = fn_version or logical_prompt(
242
+ ctx, "Function version id (fvid) whose bundle will be downloaded"
243
+ )
244
+ file = file or logical_prompt(
245
+ ctx, "Path to write the downloaded bundle to (a .tar.zst archive)"
246
+ )
247
+ click.echo(f"Downloading bundle of function version '{fn_version}' to '{file}'")
248
+ click.echo("-" * 10)
249
+ try:
250
+ server: TabsdataServer = ctx.obj["tabsdataserver"]
251
+ server.download_function_bundle(fn_version, file)
252
+ click.echo("Bundle downloaded successfully")
253
+ except Exception as e:
254
+ hint_common_solutions(ctx, e)
255
+ raise click.ClickException(f"Failed to download function bundle: {e}")
256
+
257
+
258
+ # Registered as "list" but the Python function is NOT named ``list`` to avoid
259
+ # shadowing the ``list`` builtin at module scope.
260
+ @fn.command("list")
261
+ @proj_option
262
+ @format_option
263
+ @click.option(
264
+ "--coll",
265
+ "-c",
266
+ help="Name of the collection to which the functions belong.",
267
+ )
268
+ @max_result_option
269
+ @click.pass_context
270
+ def list_functions(ctx: click.Context, coll: str):
271
+ """List all functions in a collection"""
272
+ # noinspection PyProtectedMember
273
+ from tabsdatak.sdk.tabsdata_server import _function_type_to_mapping
274
+
275
+ verify_login_or_prompt(ctx)
276
+ coll = (
277
+ coll
278
+ or get_currently_pinned_object(ctx, "collection")
279
+ or logical_prompt(ctx, "Name of the collection to which the functions belong")
280
+ )
281
+ try:
282
+ server: TabsdataServer = ctx.obj["tabsdataserver"]
283
+ list_of_functions, truncated = take(ctx, server.list_functions_generator(coll))
284
+
285
+ def _table():
286
+ table = Table(title="Functions")
287
+ table.add_column("Name", style="cyan", no_wrap=True)
288
+ table.add_column("Type")
289
+ table.add_column("Description")
290
+ table.add_column("Created by")
291
+ table.add_column("Created on")
292
+ for function in list_of_functions:
293
+ table.add_row(
294
+ function.name,
295
+ _function_type_to_mapping(function.type),
296
+ function.description,
297
+ function.defined_by,
298
+ function.defined_on_str,
299
+ )
300
+ click.echo()
301
+ make_console().print(table)
302
+ click.echo(
303
+ f"Number of functions in collection '{coll}': {len(list_of_functions)}"
304
+ )
305
+ click.echo()
306
+
307
+ render(
308
+ ctx,
309
+ data=lambda: [f._data for f in list_of_functions],
310
+ table=_table,
311
+ )
312
+ echo_more_results(ctx, truncated)
313
+
314
+ except Exception as e:
315
+ hint_common_solutions(ctx, e)
316
+ raise click.ClickException(f"Failed to list functions: {e}")
317
+
318
+
319
+ @fn.command()
320
+ @proj_option
321
+ @click.option(
322
+ "--coll",
323
+ "-c",
324
+ help="Name of the collection to which the function belongs.",
325
+ )
326
+ @click.option("--description", help="Description of the function.")
327
+ @click.option(
328
+ "--path",
329
+ help=(
330
+ "Path to the function file. Must be of the form "
331
+ "'/path/to/file.py::function_name'. Will be prompted "
332
+ "for it if not provided."
333
+ ),
334
+ )
335
+ @click.option(
336
+ "--dir-to-bundle",
337
+ help=(
338
+ "Path to the directory that should be stored in the bundle for "
339
+ "execution in the backend. If not provided, the folder where the "
340
+ "function file is will be used."
341
+ ),
342
+ )
343
+ # --requirements and --local-pkg are temporarily disabled.
344
+ @click.option(
345
+ "--reuse-tables",
346
+ is_flag=True,
347
+ help=(
348
+ "Reuse previously created tables. "
349
+ "If provided, any tables that the "
350
+ "function produces that already "
351
+ "exist in the system (but no "
352
+ "longer have a function that "
353
+ "generates them) will be "
354
+ "reused, adding to their data "
355
+ "history. If not provided, "
356
+ "those tables will be recreated "
357
+ "and their data history re-initialized."
358
+ ),
359
+ )
360
+ @click.option(
361
+ "--enabled/--disabled",
362
+ default=True,
363
+ help="Enable or disable the feature",
364
+ )
365
+ @click.option(
366
+ "--update",
367
+ is_flag=True,
368
+ help=(
369
+ "If provided, update the function if it already exists in the "
370
+ "collection. If not present, the command falls through to the "
371
+ "normal register flow."
372
+ ),
373
+ )
374
+ @click.option(
375
+ "--storage-path",
376
+ "base_path",
377
+ help=(
378
+ "Optional storage-prefix override for this function's output table data. "
379
+ "When omitted, the collection's base_path is used."
380
+ ),
381
+ )
382
+ @click.pass_context
383
+ def register(
384
+ ctx: click.Context,
385
+ coll: str,
386
+ description: str,
387
+ path: str,
388
+ dir_to_bundle: str,
389
+ reuse_tables: bool,
390
+ enabled: bool,
391
+ base_path: str = None,
392
+ update: bool = False,
393
+ ):
394
+ """Register a function.
395
+
396
+ Dynamically imports the symbol named by ``--path`` (which must be
397
+ a tabsdatak ``PublisherInfo`` / ``TransformerInfo`` /
398
+ ``SubscriberInfo`` produced by a ``@publisher`` / ``@transformer``
399
+ / ``@subscriber`` decorator) and registers it via the v2 bundle
400
+ producer (``tabsdatak.common._archive.bundle.build_bundle``).
401
+
402
+ With ``--update``, behaves idempotently: if the function already
403
+ exists in the collection it is updated; otherwise it is registered.
404
+ """
405
+ # noinspection PyProtectedMember
406
+ from tabsdatak.sdk._apiserver import APIServerError
407
+
408
+ verify_login_or_prompt(ctx)
409
+ description = description or ""
410
+ click.echo("Registering a function")
411
+ click.echo("-" * 10)
412
+ coll = (
413
+ coll
414
+ or get_currently_pinned_object(ctx, "collection")
415
+ or logical_prompt(ctx, "Name of the collection to which the function belongs")
416
+ )
417
+ path = path or logical_prompt(
418
+ ctx,
419
+ "Path to the function file. "
420
+ "Must be of the form "
421
+ "'/path/to/file.py::function_name'",
422
+ )
423
+ try:
424
+ info, project_root = load_info_and_project_root(path, dir_to_bundle)
425
+ server: TabsdataServer = ctx.obj["tabsdataserver"]
426
+ if update:
427
+ show_hint(
428
+ ctx,
429
+ "To update functions you can also use the dedicated command 'tdk fn "
430
+ "update'",
431
+ final_empty_line=True,
432
+ )
433
+ function_name = (
434
+ getattr(info.func, "__name__", None) or info.__class__.__name__
435
+ )
436
+ try:
437
+ server.get_function(coll, function_name)
438
+ click.echo(
439
+ f"The function '{function_name}' already exists in collection "
440
+ f"'{coll}'. Proceeding to update it."
441
+ )
442
+ server.update_function(
443
+ coll,
444
+ function_name,
445
+ info,
446
+ project_root,
447
+ description=description,
448
+ local_packages=None,
449
+ reuse_tables=reuse_tables,
450
+ enabled=enabled,
451
+ requirements=None,
452
+ base_path=base_path,
453
+ )
454
+ click.echo("Function updated successfully")
455
+ return
456
+ except APIServerError:
457
+ click.echo(
458
+ f"The function '{function_name}' does not exist in collection "
459
+ f"'{coll}'. Proceeding to register it."
460
+ )
461
+ server.register_function(
462
+ coll,
463
+ info,
464
+ project_root,
465
+ description=description,
466
+ local_packages=None,
467
+ reuse_tables=reuse_tables,
468
+ enabled=enabled,
469
+ requirements=None,
470
+ base_path=base_path,
471
+ )
472
+ click.echo("Function registered successfully")
473
+ except Exception as e:
474
+ hint_common_solutions(ctx, e)
475
+ raise click.ClickException(f"Failed to register function: {e}")
476
+
477
+
478
+ # @fn.command()
479
+ # @click.option(
480
+ # "--name",
481
+ # "-n",
482
+ # help="Name of the function to be triggered.",
483
+ # )
484
+ # @click.option(
485
+ # "--coll",
486
+ # "-c",
487
+ # help="Name of the collection to which the function belongs.",
488
+ # )
489
+ # @click.option(
490
+ # "--plan",
491
+ # help="ID of the plan to read.",
492
+ # )
493
+ # @click.pass_context
494
+ # def read_run(ctx: click.Context, name: str, coll: str, plan: str):
495
+ # """Read the information of a function run"""
496
+ # verify_login_or_prompt(ctx)
497
+ # name = name or logical_prompt(ctx, "Name of the function the run belongs to")
498
+ # coll = (
499
+ # coll
500
+ # or get_currently_pinned_object(ctx, "collection")
501
+ # or logical_prompt(ctx, "Name of the collection to which the function belongs")
502
+ # )
503
+ # plan = plan or logical_prompt(ctx, "ID of the plan to read")
504
+ # click.echo(
505
+ # f"Reading information of plan '{plan}' for function '{name}' in "
506
+ # f"collection '{coll}'"
507
+ # )
508
+ # click.echo("-" * 10)
509
+ # try:
510
+ # server: TabsdataServer = ctx.obj["tabsdataserver"]
511
+ # response = server.read_function_run(coll, name, plan)
512
+ # data = response.json().get("data")
513
+ #
514
+ # table = Table(title="Run information")
515
+ # table.add_column("Status", style="cyan", no_wrap=True)
516
+ # table.add_column("Triggered on")
517
+ # table.add_column("Started on")
518
+ # table.add_column("Ended on")
519
+ # table.add_column("Triggered by")
520
+ #
521
+ # status = status_to_mapping(data.get("status"))
522
+ # triggered_on = (
523
+ # _convert_timestamp_to_string(data.get("triggered_on"))
524
+ # if data.get("triggered_on")
525
+ # else "-"
526
+ # )
527
+ # started_on = (
528
+ # _convert_timestamp_to_string(data.get("started_on"))
529
+ # if data.get("started_on")
530
+ # else "-"
531
+ # )
532
+ # ended_on = (
533
+ # _convert_timestamp_to_string(data.get("ended_on"))
534
+ # if data.get("ended_on")
535
+ # else "-"
536
+ # )
537
+ # triggered_by = data.get("triggered_by")
538
+ # table.add_row(
539
+ # status,
540
+ # triggered_on,
541
+ # started_on,
542
+ # ended_on,
543
+ # triggered_by,
544
+ # )
545
+ #
546
+ # click.echo()
547
+ # console = Console()
548
+ # console.print(table)
549
+ #
550
+ # except Exception as e:
551
+ # hint_common_solutions(ctx, e)
552
+ # raise click.ClickException(f"Failed to read run information: {e}")
553
+
554
+
555
+ @fn.command()
556
+ @proj_option
557
+ @click.option(
558
+ "--name",
559
+ "-n",
560
+ help="Name of the function to be triggered.",
561
+ )
562
+ @click.option(
563
+ "--coll",
564
+ "-c",
565
+ help="Name of the collection to which the function belongs.",
566
+ )
567
+ @click.option(
568
+ "--plan-name",
569
+ help="Name to be given to the plan that will be generated by the trigger.",
570
+ )
571
+ @click.option(
572
+ "--detach",
573
+ "-d",
574
+ is_flag=True,
575
+ default=False,
576
+ help="Do not monitor the status of the plan.",
577
+ )
578
+ @click.pass_context
579
+ def trigger(
580
+ ctx: click.Context,
581
+ name: str,
582
+ coll: str,
583
+ plan_name: str,
584
+ detach: bool,
585
+ ):
586
+ """Trigger a function"""
587
+ verify_login_or_prompt(ctx)
588
+ name = name or logical_prompt(ctx, "Name of the function to be triggered")
589
+ coll = (
590
+ coll
591
+ or get_currently_pinned_object(ctx, "collection")
592
+ or logical_prompt(ctx, "Name of the collection to which the function belongs")
593
+ )
594
+ click.echo(f"Triggering function '{name}' in collection '{coll}'")
595
+ click.echo("-" * 10)
596
+ try:
597
+ server: TabsdataServer = ctx.obj["tabsdataserver"]
598
+ execution = server.trigger_function(coll, name, execution_name=plan_name)
599
+ click.echo("Function triggered. Execution has started.")
600
+ click.echo(f"Plan id: {execution.id}")
601
+ click.echo("")
602
+ if not detach:
603
+ show_hint(
604
+ ctx,
605
+ "To trigger the function without monitoring the plan, "
606
+ "use the '--debug' or '-d' option.",
607
+ final_empty_line=True,
608
+ )
609
+ _monitor_execution_or_transaction(ctx, execution=execution)
610
+ else:
611
+ show_hint(
612
+ ctx,
613
+ "You can check the status of the plan with 'tdk plan get "
614
+ f"--plan {execution.id}', or monitor the execution of the function "
615
+ f"with 'tdk plan monitor --plan {execution.id}'",
616
+ )
617
+
618
+ except Exception as e:
619
+ hint_common_solutions(ctx, e)
620
+ raise click.ClickException(f"Failed to trigger function: {e}")
621
+
622
+
623
+ def _monitor_execution_or_transaction(
624
+ ctx: click.Context, execution: Execution = None, transaction: Transaction = None
625
+ ):
626
+ """Monitor the execution of a plan or a transaction"""
627
+ if execution and transaction:
628
+ raise click.ClickException(
629
+ "Internal error: _monitor_execution_or_transaction takes either an "
630
+ "execution or a transaction, but not both."
631
+ )
632
+ if not execution and not transaction:
633
+ raise click.ClickException(
634
+ "Internal error: _monitor_execution_or_transaction takes either an "
635
+ "execution or a transaction, but not neither."
636
+ )
637
+ if execution:
638
+ entity_final_statuses = EXECUTION_FINAL_STATUSES
639
+ else:
640
+ entity_final_statuses = TRANSACTION_FINAL_STATUSES
641
+ keyword = "plan" if execution else "transaction"
642
+ supervised_entity = execution or transaction
643
+
644
+ list_of_runs = supervised_entity.function_runs
645
+
646
+ def build_table():
647
+ table = Table(title="Function runs")
648
+ table.add_column("Function run ID", style="cyan", no_wrap=True)
649
+ table.add_column("Collection")
650
+ table.add_column("Function")
651
+ (
652
+ table.add_column("Transaction ID", no_wrap=True)
653
+ if execution
654
+ else table.add_column("Plan ID", no_wrap=True)
655
+ )
656
+ table.add_column("Started on", no_wrap=True)
657
+ table.add_column("Ended on", no_wrap=True)
658
+ table.add_column("Status")
659
+ for function_run in list_of_runs:
660
+ table.add_row(
661
+ function_run.id,
662
+ collection_label(function_run),
663
+ function_run.function.name,
664
+ function_run.transaction.id if execution else function_run.execution.id,
665
+ beautify_time(function_run.started_on_str),
666
+ beautify_time(function_run.ended_on_str),
667
+ function_run.status,
668
+ )
669
+ return table
670
+
671
+ click.echo(f"Waiting for the {keyword} to finish...")
672
+
673
+ refresh_rate = 1 # seconds
674
+ with Live(
675
+ build_table(), refresh_per_second=refresh_rate, console=make_console()
676
+ ) as live:
677
+ while True:
678
+ # Note: while it would initially make more sense to write this as
679
+ # 'if transaction.status in FAILED_FINAL_STATUSES', this approach avoids
680
+ # the risk of ignoring failed transactions that are not in a recognized
681
+ # status due to a mismatch between the transaction status and the
682
+ # FINAL_STATUSES set (which ideally should not happen).
683
+ if supervised_entity.status in entity_final_statuses:
684
+ break
685
+ list_of_runs = supervised_entity.function_runs
686
+ live.update(build_table())
687
+ supervised_entity.refresh()
688
+ sleep(1 / refresh_rate)
689
+ list_of_runs = supervised_entity.function_runs
690
+ live.update(build_table())
691
+ supervised_entity.refresh()
692
+
693
+ click.echo(f"{keyword.capitalize()} finished.")
694
+
695
+ failed_runs = [
696
+ fn_run
697
+ for fn_run in list_of_runs
698
+ if fn_run.status not in FUNCTION_RUN_SUCCESSFUL_FINAL_STATUSES
699
+ ]
700
+ if failed_runs:
701
+ click.echo("Some function runs failed:")
702
+ for fn_run in failed_runs:
703
+ click.echo(f"- {fn_run.id}")
704
+ complete_command = (
705
+ f"'tdk plan logs --plan {supervised_entity.id}'"
706
+ if execution
707
+ else f"'tdk trx logs --trx {supervised_entity.id}'"
708
+ )
709
+ show_hint(
710
+ ctx,
711
+ "You can check their logs with 'tdk fnrun logs --fnrun <RUN-ID>' or with "
712
+ + complete_command,
713
+ )
714
+ else:
715
+ click.echo("All function runs were successful.")
716
+
717
+
718
+ @fn.command()
719
+ @proj_option
720
+ @click.option(
721
+ "--name",
722
+ "-n",
723
+ help="Name of the function to be updated.",
724
+ )
725
+ @click.option(
726
+ "--coll",
727
+ "-c",
728
+ help="Name of the collection to which the function belongs.",
729
+ )
730
+ @click.option("--description", help="Description of the function.")
731
+ @click.option(
732
+ "--path",
733
+ help=(
734
+ "Path to the function file. Must be of the form "
735
+ "'/path/to/file.py::function_name'. Will be prompted "
736
+ "for it if not provided."
737
+ ),
738
+ )
739
+ @click.option(
740
+ "--dir-to-bundle",
741
+ help=(
742
+ "Path to the directory that should be stored in the bundle for "
743
+ "execution in the backend. If not provided, the folder where the "
744
+ "function file is will be used."
745
+ ),
746
+ )
747
+ # --requirements and --local-pkg are temporarily disabled.
748
+ @click.option(
749
+ "--reuse-tables",
750
+ is_flag=True,
751
+ help=(
752
+ "Reuse previously created tables. "
753
+ "If provided, any tables that the "
754
+ "function produces that already "
755
+ "exist in the system (but no "
756
+ "longer have a function that "
757
+ "generates them) will be "
758
+ "reused, adding to their data "
759
+ "history. If not provided, "
760
+ "those tables will be recreated "
761
+ "and their data history re-initialized."
762
+ ),
763
+ )
764
+ @click.option(
765
+ "--enabled/--disabled",
766
+ default=True,
767
+ help="Enable or disable the feature",
768
+ )
769
+ @click.option(
770
+ "--storage-path",
771
+ "base_path",
772
+ help=(
773
+ "Optional storage-prefix override for this function's output table data. "
774
+ "When omitted, the previous version's value is carried forward."
775
+ ),
776
+ )
777
+ @click.option(
778
+ "--reset-state",
779
+ is_flag=True,
780
+ help=(
781
+ "Regenerate the function's state table id instead of carrying it forward, "
782
+ "so publishers re-read their initial configuration values from scratch."
783
+ ),
784
+ )
785
+ @click.option(
786
+ "--reset-stage",
787
+ is_flag=True,
788
+ help=(
789
+ "Regenerate the function's stage id (a fresh stage/checkpoint dir), discarding "
790
+ "accumulated stage state. The stage's storage prefix is unchanged."
791
+ ),
792
+ )
793
+ @click.pass_context
794
+ def update(
795
+ ctx: click.Context,
796
+ name: str,
797
+ coll: str,
798
+ description: str,
799
+ path: str,
800
+ dir_to_bundle: str,
801
+ reuse_tables: bool,
802
+ enabled: bool,
803
+ base_path: str = None,
804
+ reset_state: bool = False,
805
+ reset_stage: bool = False,
806
+ ):
807
+ """Update a function.
808
+
809
+ Dynamically imports the symbol named by ``--path`` (a tabsdatak
810
+ ``*Info`` descriptor) and updates the existing function via the
811
+ v2 bundle producer (``tabsdatak.common._archive.bundle.build_bundle``).
812
+ """
813
+ verify_login_or_prompt(ctx)
814
+ name = name or logical_prompt(ctx, "Name of the function to be updated")
815
+ coll = (
816
+ coll
817
+ or get_currently_pinned_object(ctx, "collection")
818
+ or logical_prompt(ctx, "Name of the collection to which the function belongs")
819
+ )
820
+ path = path or logical_prompt(
821
+ ctx,
822
+ "Path to the function file. "
823
+ "Must be of the form "
824
+ "'/path/to/file.py::function_name'",
825
+ )
826
+ click.echo(f"Updating function '{name}' in collection '{coll}'")
827
+ click.echo("-" * 10)
828
+ try:
829
+ info, project_root = load_info_and_project_root(path, dir_to_bundle)
830
+ server: TabsdataServer = ctx.obj["tabsdataserver"]
831
+ server.update_function(
832
+ coll,
833
+ name,
834
+ info,
835
+ project_root,
836
+ description=description,
837
+ local_packages=None,
838
+ reuse_tables=reuse_tables,
839
+ enabled=enabled,
840
+ requirements=None,
841
+ base_path=base_path,
842
+ reset_state=reset_state,
843
+ reset_stage=reset_stage,
844
+ )
845
+ click.echo("Function updated successfully")
846
+ except Exception as e:
847
+ hint_common_solutions(ctx, e)
848
+ raise click.ClickException(f"Failed to update function: {e}")
849
+
850
+
851
+ @fn.command()
852
+ @proj_option
853
+ @click.option(
854
+ "--name",
855
+ "-n",
856
+ help="Name of the function to enable.",
857
+ )
858
+ @click.option(
859
+ "--coll",
860
+ "-c",
861
+ help="Name of the collection to which the function belongs.",
862
+ )
863
+ @click.pass_context
864
+ def enable(
865
+ ctx: click.Context,
866
+ name: str,
867
+ coll: str,
868
+ ):
869
+ """Enables a function
870
+
871
+ Enabling an already-enabled function does nothing.
872
+ """
873
+ verify_login_or_prompt(ctx)
874
+ name = name or logical_prompt(ctx, "Name of the function to be updated")
875
+ coll = (
876
+ coll
877
+ or get_currently_pinned_object(ctx, "collection")
878
+ or logical_prompt(ctx, "Name of the collection to which the function belongs")
879
+ )
880
+ set_function_enabled(ctx, name, coll, True)
881
+
882
+
883
+ @fn.command()
884
+ @proj_option
885
+ @click.option(
886
+ "--name",
887
+ "-n",
888
+ help="Name of the function to disable.",
889
+ )
890
+ @click.option(
891
+ "--coll",
892
+ "-c",
893
+ help="Name of the collection to which the function belongs.",
894
+ )
895
+ @click.pass_context
896
+ def disable(
897
+ ctx: click.Context,
898
+ name: str,
899
+ coll: str,
900
+ ):
901
+ """Disables a function
902
+
903
+ Disabling an already-disabled function does nothing.
904
+ """
905
+ verify_login_or_prompt(ctx)
906
+ name = name or logical_prompt(ctx, "Name of the function to be updated")
907
+ coll = (
908
+ coll
909
+ or get_currently_pinned_object(ctx, "collection")
910
+ or logical_prompt(ctx, "Name of the collection to which the function belongs")
911
+ )
912
+ set_function_enabled(ctx, name, coll, False)
913
+
914
+
915
+ def set_function_enabled(ctx: click.Context, name: str, coll: str, enabled: bool):
916
+ """Enable or disable a function, doing nothing if it is already in that
917
+ state. Shared with the ``tdk cron`` / ``tdk stream`` enable/disable, which
918
+ are this command restricted to a trigger kind."""
919
+ action = "enable" if enabled else "disable"
920
+ state = "enabled" if enabled else "disabled"
921
+ try:
922
+ server: TabsdataServer = ctx.obj["tabsdataserver"]
923
+ if server.get_function(coll, name).enabled == enabled:
924
+ click.echo(f"Function '{name}' in collection '{coll}' is already {state}")
925
+ return
926
+ click.echo(
927
+ f"{'Enabling' if enabled else 'Disabling'} function '{name}' in "
928
+ f"collection '{coll}'"
929
+ )
930
+ click.echo("-" * 10)
931
+ server.enable_function(
932
+ collection_name=coll,
933
+ function_name=name,
934
+ enabled=enabled,
935
+ )
936
+ click.echo("Function updated successfully")
937
+ except Exception as e:
938
+ hint_common_solutions(ctx, e)
939
+ raise click.ClickException(f"Failed to {action} function: {e}")
940
+
941
+
942
+ @fn.command()
943
+ @proj_option
944
+ @click.option(
945
+ "--name",
946
+ "-n",
947
+ help="Name of the function to patch.",
948
+ )
949
+ @click.option(
950
+ "--coll",
951
+ "-c",
952
+ help="Name of the collection to which the function belongs.",
953
+ )
954
+ @click.option(
955
+ "--fn-version",
956
+ "fn_version",
957
+ help=(
958
+ "Function-version id (fvid) this patch overrides. Use 'tdk fn get "
959
+ "--show-history' to find available fvids."
960
+ ),
961
+ )
962
+ @click.option(
963
+ "--path",
964
+ help=(
965
+ "Path to the function file. Must be of the form "
966
+ "'/path/to/file.py::function_name'. Will be prompted "
967
+ "for it if not provided."
968
+ ),
969
+ )
970
+ @click.option("--description", help="Description of the patch.")
971
+ @click.option(
972
+ "--dir-to-bundle",
973
+ help=(
974
+ "Path to the directory that should be stored in the bundle for "
975
+ "execution in the backend. If not provided, the folder where the "
976
+ "function file is will be used."
977
+ ),
978
+ )
979
+ # --requirements and --local-pkg are temporarily disabled.
980
+ @click.pass_context
981
+ def patch(
982
+ ctx: click.Context,
983
+ name: str,
984
+ coll: str,
985
+ fn_version: str,
986
+ path: str,
987
+ description: str,
988
+ dir_to_bundle: str,
989
+ ):
990
+ """Register a code-only patch of a function version.
991
+
992
+ A patch shares the parent's topology (tables, dependencies, triggers,
993
+ workers) — only bundle / snippet / runtime values / description can be
994
+ overridden. Patches are reachable only by explicit fvid via replay.
995
+ """
996
+ verify_login_or_prompt(ctx)
997
+ coll = (
998
+ coll
999
+ or get_currently_pinned_object(ctx, "collection")
1000
+ or logical_prompt(ctx, "Name of the collection to which the function belongs")
1001
+ )
1002
+ name = name or logical_prompt(ctx, "Name of the function to patch")
1003
+ fn_version = fn_version or logical_prompt(
1004
+ ctx, "Function-version id (fvid) this patch overrides"
1005
+ )
1006
+ path = path or logical_prompt(
1007
+ ctx,
1008
+ "Path to the function file. Must be of the form "
1009
+ "'/path/to/file.py::function_name'",
1010
+ )
1011
+ click.echo(
1012
+ f"Patching function '{name}' in collection '{coll}' (fn_version={fn_version})"
1013
+ )
1014
+ click.echo("-" * 10)
1015
+ try:
1016
+ info, project_root = load_info_and_project_root(path, dir_to_bundle)
1017
+ server: TabsdataServer = ctx.obj["tabsdataserver"]
1018
+ record = server.patch_function(
1019
+ coll,
1020
+ name,
1021
+ info,
1022
+ project_root,
1023
+ patch_of=fn_version,
1024
+ description=description,
1025
+ local_packages=None,
1026
+ requirements=None,
1027
+ )
1028
+ click.echo("Patch registered successfully")
1029
+ click.echo(f"Patch id: {record.get('id')}")
1030
+ click.echo(f"Patches: {record.get('parent_function_version_id')}")
1031
+ show_hint(
1032
+ ctx,
1033
+ "Replay a finished function run against the latest patch "
1034
+ "with 'tdk fnrun replay --fnrun <RUN-ID>'.",
1035
+ final_empty_line=True,
1036
+ )
1037
+ except Exception as e:
1038
+ hint_common_solutions(ctx, e)
1039
+ raise click.ClickException(f"Failed to patch function: {e}")
1040
+
1041
+
1042
+ @fn.command("patches")
1043
+ @proj_option
1044
+ @format_option
1045
+ @click.option(
1046
+ "--name",
1047
+ "-n",
1048
+ help="Name of the function whose patches will be listed.",
1049
+ )
1050
+ @click.option(
1051
+ "--coll",
1052
+ "-c",
1053
+ help="Name of the collection to which the function belongs.",
1054
+ )
1055
+ @max_result_option
1056
+ @click.pass_context
1057
+ def list_patches(ctx: click.Context, name: str, coll: str):
1058
+ """List patch versions of a function (latest first)."""
1059
+ verify_login_or_prompt(ctx)
1060
+ coll = (
1061
+ coll
1062
+ or get_currently_pinned_object(ctx, "collection")
1063
+ or logical_prompt(ctx, "Name of the collection to which the function belongs")
1064
+ )
1065
+ name = name or logical_prompt(
1066
+ ctx, "Name of the function whose patches will be listed"
1067
+ )
1068
+ try:
1069
+ server: TabsdataServer = ctx.obj["tabsdataserver"]
1070
+ patches, truncated = take(
1071
+ ctx, server.list_function_patches_generator(coll, name)
1072
+ )
1073
+
1074
+ def _table():
1075
+ table = Table(title="Patches")
1076
+ table.add_column("Patch id", style="cyan", no_wrap=True)
1077
+ table.add_column("Patches")
1078
+ table.add_column("Description")
1079
+ table.add_column("Defined on")
1080
+ table.add_column("Defined by")
1081
+ table.add_column("Status")
1082
+ for p in patches:
1083
+ defined_on = p.get("defined_on")
1084
+ if isinstance(defined_on, (int, float)):
1085
+ # epoch milliseconds -> ISO-ish UTC string
1086
+ from datetime import datetime, timezone
1087
+
1088
+ defined_on = datetime.fromtimestamp(
1089
+ defined_on / 1000.0, tz=timezone.utc
1090
+ ).strftime("%Y-%m-%dT%H:%M:%SZ")
1091
+ table.add_row(
1092
+ p.get("id") or "",
1093
+ p.get("parent_function_version_id") or "",
1094
+ p.get("description") or "",
1095
+ beautify_time(defined_on) if defined_on else "",
1096
+ p.get("defined_by") or p.get("defined_by_id") or "",
1097
+ p.get("status") or "",
1098
+ )
1099
+ click.echo()
1100
+ make_console().print(table)
1101
+ click.echo(f"Number of patches: {len(patches)}")
1102
+ click.echo()
1103
+
1104
+ # `patches` is already the raw list of patch dicts -> full info as-is.
1105
+ render(ctx, data=lambda: patches, table=_table)
1106
+ echo_more_results(ctx, truncated)
1107
+ except Exception as e:
1108
+ hint_common_solutions(ctx, e)
1109
+ raise click.ClickException(f"Failed to list patches: {e}")