tinybird 0.0.1.dev42__py3-none-any.whl → 0.0.1.dev44__py3-none-any.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.
Potentially problematic release.
This version of tinybird might be problematic. Click here for more details.
- tinybird/client.py +1 -1
- tinybird/connectors.py +3 -3
- tinybird/feedback_manager.py +1 -1
- tinybird/prompts.py +31 -3
- tinybird/sql.py +1 -1
- tinybird/sql_template_fmt.py +1 -1
- tinybird/tb/__cli__.py +2 -2
- tinybird/tb/cli.py +1 -1
- tinybird/tb/modules/build.py +39 -21
- tinybird/tb/modules/cicd.py +2 -2
- tinybird/tb/modules/cli.py +8 -61
- tinybird/tb/modules/common.py +2 -1
- tinybird/tb/modules/copy.py +96 -5
- tinybird/tb/modules/create.py +105 -46
- tinybird/tb/modules/datafile/build.py +64 -247
- tinybird/tb/modules/datasource.py +1 -1
- tinybird/tb/modules/deployment.py +86 -61
- tinybird/tb/modules/endpoint.py +90 -3
- tinybird/tb/modules/llm_utils.py +2 -2
- tinybird/tb/modules/materialization.py +146 -0
- tinybird/tb/modules/mock.py +56 -16
- tinybird/tb/modules/pipe.py +2 -411
- tinybird/tb/modules/project.py +31 -1
- tinybird/tb/modules/test.py +72 -37
- tinybird/tb/modules/update.py +1 -1
- tinybird/tb/modules/watch.py +54 -5
- tinybird/tb_cli_modules/common.py +1 -1
- tinybird/tornado_template.py +2 -2
- {tinybird-0.0.1.dev42.dist-info → tinybird-0.0.1.dev44.dist-info}/METADATA +1 -1
- {tinybird-0.0.1.dev42.dist-info → tinybird-0.0.1.dev44.dist-info}/RECORD +33 -33
- tinybird/tb/modules/build_client.py +0 -199
- {tinybird-0.0.1.dev42.dist-info → tinybird-0.0.1.dev44.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev42.dist-info → tinybird-0.0.1.dev44.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev42.dist-info → tinybird-0.0.1.dev44.dist-info}/top_level.txt +0 -0
tinybird/tb/modules/pipe.py
CHANGED
|
@@ -5,21 +5,17 @@
|
|
|
5
5
|
|
|
6
6
|
import json
|
|
7
7
|
import re
|
|
8
|
-
from typing import Dict, List, Optional, Tuple
|
|
9
8
|
|
|
10
9
|
import click
|
|
11
|
-
import humanfriendly
|
|
12
10
|
from click import Context
|
|
13
11
|
|
|
14
|
-
from tinybird.client import
|
|
12
|
+
from tinybird.client import TinyB
|
|
15
13
|
from tinybird.tb.modules.cli import cli
|
|
16
14
|
from tinybird.tb.modules.common import (
|
|
17
15
|
coro,
|
|
18
|
-
create_tb_client,
|
|
19
16
|
echo_safe_humanfriendly_tables_format_smart_table,
|
|
20
|
-
wait_job,
|
|
21
17
|
)
|
|
22
|
-
from tinybird.tb.modules.datafile.common import
|
|
18
|
+
from tinybird.tb.modules.datafile.common import get_name_version
|
|
23
19
|
from tinybird.tb.modules.exceptions import CLIPipeException
|
|
24
20
|
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
25
21
|
|
|
@@ -30,105 +26,6 @@ def pipe(ctx):
|
|
|
30
26
|
"""Pipes commands"""
|
|
31
27
|
|
|
32
28
|
|
|
33
|
-
@pipe.group(name="copy")
|
|
34
|
-
@click.pass_context
|
|
35
|
-
def pipe_copy(ctx: Context) -> None:
|
|
36
|
-
"""Copy Pipe commands"""
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@pipe.group(name="sink")
|
|
40
|
-
@click.pass_context
|
|
41
|
-
def pipe_sink(ctx: Context) -> None:
|
|
42
|
-
"""Sink Pipe commands"""
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
@pipe.command(name="stats")
|
|
46
|
-
@click.argument("pipes", nargs=-1)
|
|
47
|
-
@click.option(
|
|
48
|
-
"--format",
|
|
49
|
-
"format_",
|
|
50
|
-
type=click.Choice(["json"], case_sensitive=False),
|
|
51
|
-
default=None,
|
|
52
|
-
help="Force a type of the output. To parse the output, keep in mind to use `tb --no-version-warning pipe stats` option.",
|
|
53
|
-
)
|
|
54
|
-
@click.pass_context
|
|
55
|
-
@coro
|
|
56
|
-
async def pipe_stats(ctx: click.Context, pipes: Tuple[str, ...], format_: str):
|
|
57
|
-
"""
|
|
58
|
-
Print pipe stats for the last 7 days
|
|
59
|
-
"""
|
|
60
|
-
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
61
|
-
all_pipes = await client.pipes()
|
|
62
|
-
pipes_to_get_stats = []
|
|
63
|
-
pipes_ids: Dict = {}
|
|
64
|
-
|
|
65
|
-
if pipes:
|
|
66
|
-
# We filter by the pipes we want to look for
|
|
67
|
-
all_pipes = [pipe for pipe in all_pipes if pipe["name"] in pipes]
|
|
68
|
-
|
|
69
|
-
for pipe in all_pipes:
|
|
70
|
-
name_version = get_name_version(pipe["name"])
|
|
71
|
-
if name_version["name"] in pipe["name"]:
|
|
72
|
-
pipes_to_get_stats.append(f"'{pipe['id']}'")
|
|
73
|
-
pipes_ids[pipe["id"]] = name_version
|
|
74
|
-
|
|
75
|
-
if not pipes_to_get_stats:
|
|
76
|
-
if format_ == "json":
|
|
77
|
-
click.echo(json.dumps({"pipes": []}, indent=2))
|
|
78
|
-
else:
|
|
79
|
-
click.echo(FeedbackManager.info_no_pipes_stats())
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
sql = f"""
|
|
83
|
-
SELECT
|
|
84
|
-
pipe_id id,
|
|
85
|
-
sumIf(view_count, date > now() - interval 7 day) requests,
|
|
86
|
-
sumIf(error_count, date > now() - interval 7 day) errors,
|
|
87
|
-
avgMergeIf(avg_duration_state, date > now() - interval 7 day) latency
|
|
88
|
-
FROM tinybird.pipe_stats
|
|
89
|
-
WHERE pipe_id in ({','.join(pipes_to_get_stats)})
|
|
90
|
-
GROUP BY pipe_id
|
|
91
|
-
ORDER BY requests DESC
|
|
92
|
-
FORMAT JSON
|
|
93
|
-
"""
|
|
94
|
-
|
|
95
|
-
res = await client.query(sql)
|
|
96
|
-
|
|
97
|
-
if res and "error" in res:
|
|
98
|
-
raise CLIPipeException(FeedbackManager.error_exception(error=str(res["error"])))
|
|
99
|
-
|
|
100
|
-
columns = ["name", "request count", "error count", "avg latency"]
|
|
101
|
-
table_human_readable: List[Tuple] = []
|
|
102
|
-
table_machine_readable: List[Dict] = []
|
|
103
|
-
if res and "data" in res:
|
|
104
|
-
for x in res["data"]:
|
|
105
|
-
tk = pipes_ids[x["id"]]
|
|
106
|
-
table_human_readable.append(
|
|
107
|
-
(
|
|
108
|
-
tk["name"],
|
|
109
|
-
x["requests"],
|
|
110
|
-
x["errors"],
|
|
111
|
-
x["latency"],
|
|
112
|
-
)
|
|
113
|
-
)
|
|
114
|
-
table_machine_readable.append(
|
|
115
|
-
{
|
|
116
|
-
"name": tk["name"],
|
|
117
|
-
"requests": x["requests"],
|
|
118
|
-
"errors": x["errors"],
|
|
119
|
-
"latency": x["latency"],
|
|
120
|
-
}
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
table_human_readable.sort(key=lambda x: (x[1], x[0]))
|
|
124
|
-
table_machine_readable.sort(key=lambda x: x["name"])
|
|
125
|
-
|
|
126
|
-
if format_ == "json":
|
|
127
|
-
click.echo(json.dumps({"pipes": table_machine_readable}, indent=2))
|
|
128
|
-
else:
|
|
129
|
-
echo_safe_humanfriendly_tables_format_smart_table(table_human_readable, column_names=columns)
|
|
130
|
-
|
|
131
|
-
|
|
132
29
|
@pipe.command(name="ls")
|
|
133
30
|
@click.option("--match", default=None, help="Retrieve any resourcing matching the pattern. eg --match _test")
|
|
134
31
|
@click.option(
|
|
@@ -172,309 +69,3 @@ async def pipe_ls(ctx: Context, match: str, format_: str):
|
|
|
172
69
|
click.echo(json.dumps({"pipes": table_machine_readable}, indent=2))
|
|
173
70
|
else:
|
|
174
71
|
raise CLIPipeException(FeedbackManager.error_pipe_ls_type())
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
@pipe.command(name="populate")
|
|
178
|
-
@click.argument("pipe_name")
|
|
179
|
-
@click.option("--node", type=str, help="Name of the materialized node.", default=None, required=False)
|
|
180
|
-
@click.option(
|
|
181
|
-
"--sql-condition",
|
|
182
|
-
type=str,
|
|
183
|
-
default=None,
|
|
184
|
-
help="Populate with a SQL condition to be applied to the trigger Data Source of the Materialized View. For instance, `--sql-condition='date == toYYYYMM(now())'` it'll populate taking all the rows from the trigger Data Source which `date` is the current month. Use it together with --populate. --sql-condition is not taken into account if the --subset param is present. Including in the ``sql_condition`` any column present in the Data Source ``engine_sorting_key`` will make the populate job process less data.",
|
|
185
|
-
)
|
|
186
|
-
@click.option(
|
|
187
|
-
"--truncate", is_flag=True, default=False, help="Truncates the materialized Data Source before populating it."
|
|
188
|
-
)
|
|
189
|
-
@click.option(
|
|
190
|
-
"--unlink-on-populate-error",
|
|
191
|
-
is_flag=True,
|
|
192
|
-
default=False,
|
|
193
|
-
help="If the populate job fails the Materialized View is unlinked and new data won't be ingested in the Materialized View. First time a populate job fails, the Materialized View is always unlinked.",
|
|
194
|
-
)
|
|
195
|
-
@click.option(
|
|
196
|
-
"--wait",
|
|
197
|
-
is_flag=True,
|
|
198
|
-
default=False,
|
|
199
|
-
help="Waits for populate jobs to finish, showing a progress bar. Disabled by default.",
|
|
200
|
-
)
|
|
201
|
-
@click.pass_context
|
|
202
|
-
@coro
|
|
203
|
-
async def pipe_populate(
|
|
204
|
-
ctx: click.Context,
|
|
205
|
-
pipe_name: str,
|
|
206
|
-
node: str,
|
|
207
|
-
sql_condition: str,
|
|
208
|
-
truncate: bool,
|
|
209
|
-
unlink_on_populate_error: bool,
|
|
210
|
-
wait: bool,
|
|
211
|
-
):
|
|
212
|
-
"""Populate the result of a Materialized Node into the target Materialized View"""
|
|
213
|
-
cl = create_tb_client(ctx)
|
|
214
|
-
|
|
215
|
-
pipe = await cl.pipe(pipe_name)
|
|
216
|
-
|
|
217
|
-
if pipe["type"] != PipeTypes.MATERIALIZED:
|
|
218
|
-
raise CLIPipeException(FeedbackManager.error_pipe_not_materialized(pipe=pipe_name))
|
|
219
|
-
|
|
220
|
-
if not node:
|
|
221
|
-
materialized_ids = [pipe_node["id"] for pipe_node in pipe["nodes"] if pipe_node.get("materialized") is not None]
|
|
222
|
-
|
|
223
|
-
if not materialized_ids:
|
|
224
|
-
raise CLIPipeException(FeedbackManager.error_populate_no_materialized_in_pipe(pipe=pipe_name))
|
|
225
|
-
|
|
226
|
-
elif len(materialized_ids) > 1:
|
|
227
|
-
raise CLIPipeException(FeedbackManager.error_populate_several_materialized_in_pipe(pipe=pipe_name))
|
|
228
|
-
|
|
229
|
-
node = materialized_ids[0]
|
|
230
|
-
|
|
231
|
-
response = await cl.populate_node(
|
|
232
|
-
pipe_name,
|
|
233
|
-
node,
|
|
234
|
-
populate_condition=sql_condition,
|
|
235
|
-
truncate=truncate,
|
|
236
|
-
unlink_on_populate_error=unlink_on_populate_error,
|
|
237
|
-
)
|
|
238
|
-
if "job" not in response:
|
|
239
|
-
raise CLIPipeException(response)
|
|
240
|
-
|
|
241
|
-
job_id = response["job"]["id"]
|
|
242
|
-
job_url = response["job"]["job_url"]
|
|
243
|
-
if sql_condition:
|
|
244
|
-
click.echo(FeedbackManager.info_populate_condition_job_url(url=job_url, populate_condition=sql_condition))
|
|
245
|
-
else:
|
|
246
|
-
click.echo(FeedbackManager.info_populate_job_url(url=job_url))
|
|
247
|
-
if wait:
|
|
248
|
-
await wait_job(cl, job_id, job_url, "Populating")
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
@pipe.command(name="token_read")
|
|
252
|
-
@click.argument("pipe_name")
|
|
253
|
-
@click.pass_context
|
|
254
|
-
@coro
|
|
255
|
-
async def pipe_token_read(ctx: click.Context, pipe_name: str):
|
|
256
|
-
"""Retrieve a token to read a pipe"""
|
|
257
|
-
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
258
|
-
|
|
259
|
-
try:
|
|
260
|
-
await client.pipe_file(pipe_name)
|
|
261
|
-
except DoesNotExistException:
|
|
262
|
-
raise CLIPipeException(FeedbackManager.error_pipe_does_not_exist(pipe=pipe_name))
|
|
263
|
-
|
|
264
|
-
tokens = await client.tokens()
|
|
265
|
-
token = None
|
|
266
|
-
|
|
267
|
-
for t in tokens:
|
|
268
|
-
for scope in t["scopes"]:
|
|
269
|
-
if scope["type"] == "PIPES:READ" and scope["resource"] == pipe_name:
|
|
270
|
-
token = t["token"]
|
|
271
|
-
if token:
|
|
272
|
-
click.echo(token)
|
|
273
|
-
else:
|
|
274
|
-
click.echo(FeedbackManager.warning_token_pipe(pipe=pipe_name))
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
@pipe.command(
|
|
278
|
-
name="data",
|
|
279
|
-
context_settings=dict(
|
|
280
|
-
allow_extra_args=True,
|
|
281
|
-
ignore_unknown_options=True,
|
|
282
|
-
),
|
|
283
|
-
)
|
|
284
|
-
@click.argument("pipe")
|
|
285
|
-
@click.option("--query", default=None, help="Run SQL over pipe results")
|
|
286
|
-
@click.option(
|
|
287
|
-
"--format", "format_", type=click.Choice(["json", "csv"], case_sensitive=False), help="Return format (CSV, JSON)"
|
|
288
|
-
)
|
|
289
|
-
@click.pass_context
|
|
290
|
-
@coro
|
|
291
|
-
async def print_pipe(ctx: Context, pipe: str, query: str, format_: str):
|
|
292
|
-
"""Print data returned by a pipe
|
|
293
|
-
|
|
294
|
-
Syntax: tb pipe data <pipe_name> --param_name value --param2_name value2 ...
|
|
295
|
-
"""
|
|
296
|
-
|
|
297
|
-
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
298
|
-
params = {ctx.args[i][2:]: ctx.args[i + 1] for i in range(0, len(ctx.args), 2)}
|
|
299
|
-
req_format = "json" if not format_ else format_.lower()
|
|
300
|
-
try:
|
|
301
|
-
res = await client.pipe_data(pipe, format=req_format, sql=query, params=params)
|
|
302
|
-
except AuthNoTokenException:
|
|
303
|
-
raise
|
|
304
|
-
except Exception as e:
|
|
305
|
-
raise CLIPipeException(FeedbackManager.error_exception(error=str(e)))
|
|
306
|
-
|
|
307
|
-
if not format_:
|
|
308
|
-
stats = res["statistics"]
|
|
309
|
-
seconds = stats["elapsed"]
|
|
310
|
-
rows_read = humanfriendly.format_number(stats["rows_read"])
|
|
311
|
-
bytes_read = humanfriendly.format_size(stats["bytes_read"])
|
|
312
|
-
|
|
313
|
-
click.echo(FeedbackManager.success_print_pipe(pipe=pipe))
|
|
314
|
-
click.echo(FeedbackManager.info_query_stats(seconds=seconds, rows=rows_read, bytes=bytes_read))
|
|
315
|
-
|
|
316
|
-
if not res["data"]:
|
|
317
|
-
click.echo(FeedbackManager.info_no_rows())
|
|
318
|
-
else:
|
|
319
|
-
echo_safe_humanfriendly_tables_format_smart_table(
|
|
320
|
-
data=[d.values() for d in res["data"]], column_names=res["data"][0].keys()
|
|
321
|
-
)
|
|
322
|
-
click.echo("\n")
|
|
323
|
-
elif req_format == "json":
|
|
324
|
-
click.echo(json.dumps(res))
|
|
325
|
-
else:
|
|
326
|
-
click.echo(res)
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
@pipe_copy.command(name="run", short_help="Run an on-demand copy job")
|
|
330
|
-
@click.argument("pipe_name_or_id")
|
|
331
|
-
@click.option("--wait", is_flag=True, default=False, help="Wait for the copy job to finish")
|
|
332
|
-
@click.option(
|
|
333
|
-
"--mode", type=click.Choice(["append", "replace"], case_sensitive=True), default=None, help="Copy strategy"
|
|
334
|
-
)
|
|
335
|
-
@click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
|
|
336
|
-
@click.option(
|
|
337
|
-
"--param",
|
|
338
|
-
nargs=1,
|
|
339
|
-
type=str,
|
|
340
|
-
multiple=True,
|
|
341
|
-
default=None,
|
|
342
|
-
help="Key and value of the params you want the Copy pipe to be called with. For example: tb pipe copy run <my_copy_pipe> --param foo=bar",
|
|
343
|
-
)
|
|
344
|
-
@click.pass_context
|
|
345
|
-
@coro
|
|
346
|
-
async def pipe_copy_run(
|
|
347
|
-
ctx: click.Context, pipe_name_or_id: str, wait: bool, mode: str, yes: bool, param: Optional[Tuple[str]]
|
|
348
|
-
):
|
|
349
|
-
"""Run an on-demand copy job"""
|
|
350
|
-
|
|
351
|
-
params = dict(key_value.split("=") for key_value in param) if param else {}
|
|
352
|
-
|
|
353
|
-
if yes or click.confirm(FeedbackManager.warning_confirm_copy_pipe(pipe=pipe_name_or_id)):
|
|
354
|
-
click.echo(FeedbackManager.info_copy_job_running(pipe=pipe_name_or_id))
|
|
355
|
-
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
356
|
-
|
|
357
|
-
try:
|
|
358
|
-
response = await client.pipe_run_copy(pipe_name_or_id, params, mode)
|
|
359
|
-
|
|
360
|
-
job_id = response["job"]["job_id"]
|
|
361
|
-
job_url = response["job"]["job_url"]
|
|
362
|
-
target_datasource_id = response["tags"]["copy_target_datasource"]
|
|
363
|
-
target_datasource = await client.get_datasource(target_datasource_id)
|
|
364
|
-
target_datasource_name = target_datasource["name"]
|
|
365
|
-
click.echo(
|
|
366
|
-
FeedbackManager.success_copy_job_created(target_datasource=target_datasource_name, job_url=job_url)
|
|
367
|
-
)
|
|
368
|
-
|
|
369
|
-
if wait:
|
|
370
|
-
await wait_job(client, job_id, job_url, "** Copying data")
|
|
371
|
-
click.echo(FeedbackManager.success_data_copied_to_ds(target_datasource=target_datasource_name))
|
|
372
|
-
|
|
373
|
-
except AuthNoTokenException:
|
|
374
|
-
raise
|
|
375
|
-
except Exception as e:
|
|
376
|
-
raise CLIPipeException(FeedbackManager.error_creating_copy_job(error=e))
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
@pipe_copy.command(name="resume", short_help="Resume a paused copy pipe")
|
|
380
|
-
@click.argument("pipe_name_or_id")
|
|
381
|
-
@click.pass_context
|
|
382
|
-
@coro
|
|
383
|
-
async def pipe_copy_resume(ctx: click.Context, pipe_name_or_id: str):
|
|
384
|
-
"""Resume a paused copy pipe"""
|
|
385
|
-
|
|
386
|
-
click.echo(FeedbackManager.info_copy_pipe_resuming(pipe=pipe_name_or_id))
|
|
387
|
-
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
388
|
-
|
|
389
|
-
try:
|
|
390
|
-
await client.pipe_resume_copy(pipe_name_or_id)
|
|
391
|
-
click.echo(FeedbackManager.success_copy_pipe_resumed(pipe=pipe_name_or_id))
|
|
392
|
-
|
|
393
|
-
except AuthNoTokenException:
|
|
394
|
-
raise
|
|
395
|
-
except Exception as e:
|
|
396
|
-
raise CLIPipeException(FeedbackManager.error_resuming_copy_pipe(error=e))
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
@pipe_copy.command(name="pause", short_help="Pause a running copy pipe")
|
|
400
|
-
@click.argument("pipe_name_or_id")
|
|
401
|
-
@click.pass_context
|
|
402
|
-
@coro
|
|
403
|
-
async def pipe_copy_pause(ctx: click.Context, pipe_name_or_id: str):
|
|
404
|
-
"""Pause a running copy pipe"""
|
|
405
|
-
|
|
406
|
-
click.echo(FeedbackManager.info_copy_pipe_pausing(pipe=pipe_name_or_id))
|
|
407
|
-
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
408
|
-
|
|
409
|
-
try:
|
|
410
|
-
await client.pipe_pause_copy(pipe_name_or_id)
|
|
411
|
-
click.echo(FeedbackManager.success_copy_pipe_paused(pipe=pipe_name_or_id))
|
|
412
|
-
|
|
413
|
-
except AuthNoTokenException:
|
|
414
|
-
raise
|
|
415
|
-
except Exception as e:
|
|
416
|
-
raise CLIPipeException(FeedbackManager.error_pausing_copy_pipe(error=e))
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
@pipe_sink.command(name="run", short_help="Run an on-demand sink job")
|
|
420
|
-
@click.argument("pipe_name_or_id")
|
|
421
|
-
@click.option("--wait", is_flag=True, default=False, help="Wait for the sink job to finish")
|
|
422
|
-
@click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
|
|
423
|
-
@click.option("--dry-run", is_flag=True, default=False, help="Run the command without executing the sink job")
|
|
424
|
-
@click.option(
|
|
425
|
-
"--param",
|
|
426
|
-
nargs=1,
|
|
427
|
-
type=str,
|
|
428
|
-
multiple=True,
|
|
429
|
-
default=None,
|
|
430
|
-
help="Key and value of the params you want the Sink pipe to be called with. For example: tb pipe sink run <my_sink_pipe> --param foo=bar",
|
|
431
|
-
)
|
|
432
|
-
@click.pass_context
|
|
433
|
-
@coro
|
|
434
|
-
async def pipe_sink_run(
|
|
435
|
-
ctx: click.Context, pipe_name_or_id: str, wait: bool, yes: bool, dry_run: bool, param: Optional[Tuple[str]]
|
|
436
|
-
):
|
|
437
|
-
"""Run an on-demand sink job"""
|
|
438
|
-
|
|
439
|
-
params = dict(key_value.split("=") for key_value in param) if param else {}
|
|
440
|
-
|
|
441
|
-
if dry_run or yes or click.confirm(FeedbackManager.warning_confirm_sink_job(pipe=pipe_name_or_id)):
|
|
442
|
-
click.echo(FeedbackManager.info_sink_job_running(pipe=pipe_name_or_id))
|
|
443
|
-
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
444
|
-
|
|
445
|
-
try:
|
|
446
|
-
pipe = await client.pipe(pipe_name_or_id)
|
|
447
|
-
connections = await client.get_connections()
|
|
448
|
-
|
|
449
|
-
if (pipe.get("type", None) != "sink") or (not pipe.get("sink_node", None)):
|
|
450
|
-
error_message = f"Pipe {pipe_name_or_id} is not published as a Sink pipe"
|
|
451
|
-
raise Exception(FeedbackManager.error_running_on_demand_sink_job(error=error_message))
|
|
452
|
-
|
|
453
|
-
current_sink = None
|
|
454
|
-
for connection in connections:
|
|
455
|
-
for sink in connection.get("sinks", []):
|
|
456
|
-
if sink.get("resource_id") == pipe["id"]:
|
|
457
|
-
current_sink = sink
|
|
458
|
-
break
|
|
459
|
-
|
|
460
|
-
if not current_sink:
|
|
461
|
-
click.echo(FeedbackManager.warning_sink_no_connection(pipe_name=pipe.get("name", "")))
|
|
462
|
-
|
|
463
|
-
if dry_run:
|
|
464
|
-
click.echo(FeedbackManager.info_dry_sink_run())
|
|
465
|
-
return
|
|
466
|
-
|
|
467
|
-
bucket_path = (current_sink or {}).get("settings", {}).get("bucket_path", "")
|
|
468
|
-
response = await client.pipe_run_sink(pipe_name_or_id, params)
|
|
469
|
-
job_id = response["job"]["id"]
|
|
470
|
-
job_url = response["job"]["job_url"]
|
|
471
|
-
click.echo(FeedbackManager.success_sink_job_created(bucket_path=bucket_path, job_url=job_url))
|
|
472
|
-
|
|
473
|
-
if wait:
|
|
474
|
-
await wait_job(client, job_id, job_url, "** Sinking data")
|
|
475
|
-
click.echo(FeedbackManager.success_sink_job_finished(bucket_path=bucket_path))
|
|
476
|
-
|
|
477
|
-
except AuthNoTokenException:
|
|
478
|
-
raise
|
|
479
|
-
except Exception as e:
|
|
480
|
-
raise CLIPipeException(FeedbackManager.error_creating_sink_job(error=str(e)))
|
tinybird/tb/modules/project.py
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import glob
|
|
2
2
|
import os
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import List, Optional
|
|
4
|
+
from typing import Dict, List, Optional
|
|
5
5
|
|
|
6
6
|
from tinybird.tb.modules.config import CLIConfig
|
|
7
|
+
from tinybird.tb.modules.datafile.common import Datafile
|
|
8
|
+
from tinybird.tb.modules.datafile.parse_datasource import parse_datasource
|
|
9
|
+
from tinybird.tb.modules.datafile.parse_pipe import parse_pipe
|
|
7
10
|
|
|
8
11
|
|
|
9
12
|
class Project:
|
|
@@ -42,3 +45,30 @@ class Project:
|
|
|
42
45
|
@property
|
|
43
46
|
def pipes(self) -> List[str]:
|
|
44
47
|
return [Path(f).stem for f in glob.glob(f"{self.path}/**/*.pipe", recursive=True)]
|
|
48
|
+
|
|
49
|
+
def get_pipe_datafile(self, filename: str) -> Optional[Datafile]:
|
|
50
|
+
try:
|
|
51
|
+
return parse_pipe(filename)
|
|
52
|
+
except Exception:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
def get_datasource_datafile(self, filename: str) -> Optional[Datafile]:
|
|
56
|
+
try:
|
|
57
|
+
return parse_datasource(filename)
|
|
58
|
+
except Exception:
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
def get_datafile(self, filename: str) -> Optional[Datafile]:
|
|
62
|
+
if filename.endswith(".pipe"):
|
|
63
|
+
return self.get_pipe_datafile(filename)
|
|
64
|
+
elif filename.endswith(".datasource"):
|
|
65
|
+
return self.get_datasource_datafile(filename)
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
def get_project_datafiles(self) -> Dict[str, Datafile]:
|
|
69
|
+
project_filenames = self.get_project_files()
|
|
70
|
+
datafiles: Dict[str, Datafile] = {}
|
|
71
|
+
for filename in project_filenames:
|
|
72
|
+
if datafile := self.get_datafile(filename):
|
|
73
|
+
datafiles[filename] = datafile
|
|
74
|
+
return datafiles
|
tinybird/tb/modules/test.py
CHANGED
|
@@ -37,7 +37,7 @@ def repr_str(dumper, data):
|
|
|
37
37
|
yaml.add_representer(str, repr_str, Dumper=yaml.SafeDumper)
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
def generate_test_file(pipe_name: str, tests: List[Dict[str, Any]], folder: Optional[str], mode: str = "w"):
|
|
40
|
+
def generate_test_file(pipe_name: str, tests: List[Dict[str, Any]], folder: Optional[str], mode: str = "w") -> Path:
|
|
41
41
|
base = Path("tests")
|
|
42
42
|
if folder:
|
|
43
43
|
base = Path(folder) / base
|
|
@@ -50,6 +50,7 @@ def generate_test_file(pipe_name: str, tests: List[Dict[str, Any]], folder: Opti
|
|
|
50
50
|
path = base / f"{pipe_name}.yaml"
|
|
51
51
|
with open(path, mode) as f:
|
|
52
52
|
f.write(formatted_yaml)
|
|
53
|
+
return path
|
|
53
54
|
|
|
54
55
|
|
|
55
56
|
@cli.group()
|
|
@@ -66,9 +67,14 @@ def test(ctx: click.Context) -> None:
|
|
|
66
67
|
@click.option(
|
|
67
68
|
"--prompt", type=str, default="Create a test for the selected pipe", help="Prompt to be used to create the test"
|
|
68
69
|
)
|
|
70
|
+
@click.option(
|
|
71
|
+
"--skip",
|
|
72
|
+
is_flag=True,
|
|
73
|
+
help="Skip the test creation process and only generate the test file",
|
|
74
|
+
)
|
|
69
75
|
@click.pass_context
|
|
70
76
|
@coro
|
|
71
|
-
async def test_create(ctx: click.Context, name_or_filename: str, prompt: str) -> None:
|
|
77
|
+
async def test_create(ctx: click.Context, name_or_filename: str, prompt: str, skip: bool) -> None:
|
|
72
78
|
"""
|
|
73
79
|
Create a test for an existing pipe
|
|
74
80
|
"""
|
|
@@ -111,43 +117,72 @@ async def test_create(ctx: click.Context, name_or_filename: str, prompt: str) ->
|
|
|
111
117
|
raise CLIException(FeedbackManager.error(message="No user token found"))
|
|
112
118
|
llm = LLM(user_token=user_token, host=config.get_client().host)
|
|
113
119
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
tests: List[Dict[str, Any]] = []
|
|
119
|
-
for test_content in tests_content:
|
|
120
|
-
test: Dict[str, Any] = {}
|
|
121
|
-
test["name"] = extract_xml(test_content, "name")
|
|
122
|
-
test["description"] = extract_xml(test_content, "description")
|
|
123
|
-
parameters_api = extract_xml(test_content, "parameters")
|
|
124
|
-
test["parameters"] = parameters_api.split("?")[1] if "?" in parameters_api else parameters_api
|
|
125
|
-
test["expected_result"] = ""
|
|
126
|
-
|
|
127
|
-
response = None
|
|
128
|
-
try:
|
|
129
|
-
response = await get_pipe_data(client, pipe_name=pipe_name, test_params=test["parameters"])
|
|
130
|
-
except Exception:
|
|
131
|
-
pass
|
|
120
|
+
iterations = 0
|
|
121
|
+
history = ""
|
|
122
|
+
test_path: Optional[Path] = None
|
|
132
123
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
124
|
+
while iterations < 10:
|
|
125
|
+
feedback = ""
|
|
126
|
+
if iterations > 0:
|
|
127
|
+
feedback = click.prompt("\nFollow-up instructions or continue", default="continue")
|
|
128
|
+
if iterations > 0 and (not feedback or feedback in ("continue", "ok", "exit", "quit", "q")):
|
|
129
|
+
break
|
|
130
|
+
else:
|
|
131
|
+
if iterations > 0 and test_path:
|
|
132
|
+
test_path.unlink()
|
|
133
|
+
test_path = None
|
|
134
|
+
click.echo(FeedbackManager.highlight(message=f"\n» Creating test for {pipe_name} endpoint..."))
|
|
135
|
+
|
|
136
|
+
response_llm = llm.ask(system_prompt=system_prompt, prompt=prompt)
|
|
137
|
+
response_xml = extract_xml(response_llm, "response")
|
|
138
|
+
tests_content = parse_xml(response_xml, "test")
|
|
139
|
+
|
|
140
|
+
tests: List[Dict[str, Any]] = []
|
|
141
|
+
|
|
142
|
+
for test_content in tests_content:
|
|
143
|
+
test: Dict[str, Any] = {}
|
|
144
|
+
test["name"] = extract_xml(test_content, "name")
|
|
145
|
+
test["description"] = extract_xml(test_content, "description")
|
|
146
|
+
parameters_api = extract_xml(test_content, "parameters")
|
|
147
|
+
test["parameters"] = parameters_api.split("?")[1] if "?" in parameters_api else parameters_api
|
|
148
|
+
test["expected_result"] = ""
|
|
149
|
+
|
|
150
|
+
response = None
|
|
151
|
+
try:
|
|
152
|
+
response = await get_pipe_data(client, pipe_name=pipe_name, test_params=test["parameters"])
|
|
153
|
+
except Exception:
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
if response:
|
|
157
|
+
if response.status_code >= 400:
|
|
158
|
+
test["expected_http_status"] = response.status_code
|
|
159
|
+
test["expected_result"] = response.json()["error"]
|
|
160
|
+
else:
|
|
161
|
+
if "expected_http_status" in test:
|
|
162
|
+
del test["expected_http_status"]
|
|
163
|
+
test["expected_result"] = response.text or ""
|
|
164
|
+
|
|
165
|
+
tests.append(test)
|
|
166
|
+
|
|
167
|
+
if len(tests) > 0:
|
|
168
|
+
test_path = generate_test_file(pipe_name, tests, folder, mode="a")
|
|
169
|
+
for test in tests:
|
|
170
|
+
test_name = test["name"]
|
|
171
|
+
click.echo(FeedbackManager.info(message=f"✓ {test_name} created"))
|
|
172
|
+
|
|
173
|
+
history = (
|
|
174
|
+
history
|
|
175
|
+
+ f"""
|
|
176
|
+
<result_iteration_{iterations}>
|
|
177
|
+
{response_xml}
|
|
178
|
+
</result_iteration_{iterations}>
|
|
179
|
+
"""
|
|
180
|
+
)
|
|
181
|
+
if skip:
|
|
182
|
+
break
|
|
183
|
+
iterations += 1
|
|
137
184
|
else:
|
|
138
|
-
|
|
139
|
-
del test["expected_http_status"]
|
|
140
|
-
test["expected_result"] = response.text or ""
|
|
141
|
-
|
|
142
|
-
tests.append(test)
|
|
143
|
-
|
|
144
|
-
if len(tests) > 0:
|
|
145
|
-
generate_test_file(pipe_name, tests, folder, mode="a")
|
|
146
|
-
for test in tests:
|
|
147
|
-
test_name = test["name"]
|
|
148
|
-
click.echo(FeedbackManager.info(message=f"✓ {test_name} created"))
|
|
149
|
-
else:
|
|
150
|
-
click.echo(FeedbackManager.info(message="* No tests created"))
|
|
185
|
+
click.echo(FeedbackManager.info(message="* No tests created"))
|
|
151
186
|
|
|
152
187
|
click.echo(FeedbackManager.success(message="✓ Done!\n"))
|
|
153
188
|
except Exception as e:
|
tinybird/tb/modules/update.py
CHANGED