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,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}")
|