tinybird 0.0.1.dev43__py3-none-any.whl → 0.0.1.dev46__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.
- tinybird/client.py +17 -1
- tinybird/prompts.py +135 -15
- tinybird/tb/__cli__.py +2 -2
- tinybird/tb/cli.py +1 -1
- tinybird/tb/modules/build.py +28 -20
- tinybird/tb/modules/cli.py +18 -62
- tinybird/tb/modules/common.py +3 -2
- tinybird/tb/modules/copy.py +1 -1
- tinybird/tb/modules/create.py +134 -59
- tinybird/tb/modules/datafile/build.py +12 -221
- tinybird/tb/modules/datafile/common.py +1 -1
- tinybird/tb/modules/datafile/format_datasource.py +1 -1
- tinybird/tb/modules/datafile/format_pipe.py +4 -4
- tinybird/tb/modules/datafile/pipe_checker.py +3 -3
- tinybird/tb/modules/datasource.py +1 -1
- tinybird/tb/modules/deployment.py +1 -1
- tinybird/tb/modules/endpoint.py +89 -2
- tinybird/tb/modules/feedback_manager.py +5 -1
- tinybird/tb/modules/local_common.py +10 -7
- tinybird/tb/modules/materialization.py +146 -0
- tinybird/tb/modules/mock.py +56 -16
- tinybird/tb/modules/pipe.py +8 -326
- tinybird/tb/modules/project.py +10 -4
- tinybird/tb/modules/shell.py +3 -3
- tinybird/tb/modules/test.py +73 -38
- tinybird/tb/modules/tinyunit/tinyunit.py +1 -1
- tinybird/tb/modules/update.py +1 -1
- tinybird/tb/modules/workspace.py +2 -1
- {tinybird-0.0.1.dev43.dist-info → tinybird-0.0.1.dev46.dist-info}/METADATA +1 -1
- {tinybird-0.0.1.dev43.dist-info → tinybird-0.0.1.dev46.dist-info}/RECORD +33 -33
- tinybird/tb/modules/build_client.py +0 -199
- {tinybird-0.0.1.dev43.dist-info → tinybird-0.0.1.dev46.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev43.dist-info → tinybird-0.0.1.dev46.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev43.dist-info → tinybird-0.0.1.dev46.dist-info}/top_level.txt +0 -0
tinybird/tb/modules/pipe.py
CHANGED
|
@@ -5,128 +5,25 @@
|
|
|
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
|
|
|
26
22
|
|
|
27
|
-
@cli.group(hidden=
|
|
23
|
+
@cli.group(hidden=False)
|
|
28
24
|
@click.pass_context
|
|
29
25
|
def pipe(ctx):
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
|
|
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)
|
|
26
|
+
"""Pipe commands"""
|
|
130
27
|
|
|
131
28
|
|
|
132
29
|
@pipe.command(name="ls")
|
|
@@ -144,10 +41,10 @@ async def pipe_ls(ctx: Context, match: str, format_: str):
|
|
|
144
41
|
"""List pipes"""
|
|
145
42
|
|
|
146
43
|
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
147
|
-
pipes = await client.pipes(dependencies=False, node_attrs="name", attrs="name,updated_at")
|
|
44
|
+
pipes = await client.pipes(dependencies=False, node_attrs="name", attrs="name,updated_at,type")
|
|
148
45
|
pipes = sorted(pipes, key=lambda p: p["updated_at"])
|
|
149
46
|
|
|
150
|
-
columns = ["name", "published date", "nodes"]
|
|
47
|
+
columns = ["name", "published date", "nodes", "type"]
|
|
151
48
|
table_human_readable = []
|
|
152
49
|
table_machine_readable = []
|
|
153
50
|
pattern = re.compile(match) if match else None
|
|
@@ -155,12 +52,13 @@ async def pipe_ls(ctx: Context, match: str, format_: str):
|
|
|
155
52
|
tk = get_name_version(t["name"])
|
|
156
53
|
if pattern and not pattern.search(tk["name"]):
|
|
157
54
|
continue
|
|
158
|
-
table_human_readable.append((tk["name"], t["updated_at"][:-7], len(t["nodes"])))
|
|
55
|
+
table_human_readable.append((tk["name"], t["updated_at"][:-7], len(t["nodes"]), t["type"]))
|
|
159
56
|
table_machine_readable.append(
|
|
160
57
|
{
|
|
161
58
|
"name": tk["name"],
|
|
162
59
|
"published date": t["updated_at"][:-7],
|
|
163
60
|
"nodes": len(t["nodes"]),
|
|
61
|
+
"type": t["type"],
|
|
164
62
|
}
|
|
165
63
|
)
|
|
166
64
|
|
|
@@ -172,219 +70,3 @@ async def pipe_ls(ctx: Context, match: str, format_: str):
|
|
|
172
70
|
click.echo(json.dumps({"pipes": table_machine_readable}, indent=2))
|
|
173
71
|
else:
|
|
174
72
|
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_sink.command(name="run", short_help="Run an on-demand sink job")
|
|
330
|
-
@click.argument("pipe_name_or_id")
|
|
331
|
-
@click.option("--wait", is_flag=True, default=False, help="Wait for the sink job to finish")
|
|
332
|
-
@click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
|
|
333
|
-
@click.option("--dry-run", is_flag=True, default=False, help="Run the command without executing the sink job")
|
|
334
|
-
@click.option(
|
|
335
|
-
"--param",
|
|
336
|
-
nargs=1,
|
|
337
|
-
type=str,
|
|
338
|
-
multiple=True,
|
|
339
|
-
default=None,
|
|
340
|
-
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",
|
|
341
|
-
)
|
|
342
|
-
@click.pass_context
|
|
343
|
-
@coro
|
|
344
|
-
async def pipe_sink_run(
|
|
345
|
-
ctx: click.Context, pipe_name_or_id: str, wait: bool, yes: bool, dry_run: bool, param: Optional[Tuple[str]]
|
|
346
|
-
):
|
|
347
|
-
"""Run an on-demand sink job"""
|
|
348
|
-
|
|
349
|
-
params = dict(key_value.split("=") for key_value in param) if param else {}
|
|
350
|
-
|
|
351
|
-
if dry_run or yes or click.confirm(FeedbackManager.warning_confirm_sink_job(pipe=pipe_name_or_id)):
|
|
352
|
-
click.echo(FeedbackManager.info_sink_job_running(pipe=pipe_name_or_id))
|
|
353
|
-
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
354
|
-
|
|
355
|
-
try:
|
|
356
|
-
pipe = await client.pipe(pipe_name_or_id)
|
|
357
|
-
connections = await client.get_connections()
|
|
358
|
-
|
|
359
|
-
if (pipe.get("type", None) != "sink") or (not pipe.get("sink_node", None)):
|
|
360
|
-
error_message = f"Pipe {pipe_name_or_id} is not published as a Sink pipe"
|
|
361
|
-
raise Exception(FeedbackManager.error_running_on_demand_sink_job(error=error_message))
|
|
362
|
-
|
|
363
|
-
current_sink = None
|
|
364
|
-
for connection in connections:
|
|
365
|
-
for sink in connection.get("sinks", []):
|
|
366
|
-
if sink.get("resource_id") == pipe["id"]:
|
|
367
|
-
current_sink = sink
|
|
368
|
-
break
|
|
369
|
-
|
|
370
|
-
if not current_sink:
|
|
371
|
-
click.echo(FeedbackManager.warning_sink_no_connection(pipe_name=pipe.get("name", "")))
|
|
372
|
-
|
|
373
|
-
if dry_run:
|
|
374
|
-
click.echo(FeedbackManager.info_dry_sink_run())
|
|
375
|
-
return
|
|
376
|
-
|
|
377
|
-
bucket_path = (current_sink or {}).get("settings", {}).get("bucket_path", "")
|
|
378
|
-
response = await client.pipe_run_sink(pipe_name_or_id, params)
|
|
379
|
-
job_id = response["job"]["id"]
|
|
380
|
-
job_url = response["job"]["job_url"]
|
|
381
|
-
click.echo(FeedbackManager.success_sink_job_created(bucket_path=bucket_path, job_url=job_url))
|
|
382
|
-
|
|
383
|
-
if wait:
|
|
384
|
-
await wait_job(client, job_id, job_url, "** Sinking data")
|
|
385
|
-
click.echo(FeedbackManager.success_sink_job_finished(bucket_path=bucket_path))
|
|
386
|
-
|
|
387
|
-
except AuthNoTokenException:
|
|
388
|
-
raise
|
|
389
|
-
except Exception as e:
|
|
390
|
-
raise CLIPipeException(FeedbackManager.error_creating_sink_job(error=str(e)))
|
tinybird/tb/modules/project.py
CHANGED
|
@@ -46,11 +46,17 @@ class Project:
|
|
|
46
46
|
def pipes(self) -> List[str]:
|
|
47
47
|
return [Path(f).stem for f in glob.glob(f"{self.path}/**/*.pipe", recursive=True)]
|
|
48
48
|
|
|
49
|
-
def get_pipe_datafile(self, filename: str) -> Datafile:
|
|
50
|
-
|
|
49
|
+
def get_pipe_datafile(self, filename: str) -> Optional[Datafile]:
|
|
50
|
+
try:
|
|
51
|
+
return parse_pipe(filename)
|
|
52
|
+
except Exception:
|
|
53
|
+
return None
|
|
51
54
|
|
|
52
|
-
def get_datasource_datafile(self, filename: str) -> Datafile:
|
|
53
|
-
|
|
55
|
+
def get_datasource_datafile(self, filename: str) -> Optional[Datafile]:
|
|
56
|
+
try:
|
|
57
|
+
return parse_datasource(filename)
|
|
58
|
+
except Exception:
|
|
59
|
+
return None
|
|
54
60
|
|
|
55
61
|
def get_datafile(self, filename: str) -> Optional[Datafile]:
|
|
56
62
|
if filename.endswith(".pipe"):
|
tinybird/tb/modules/shell.py
CHANGED
|
@@ -245,7 +245,7 @@ class Shell:
|
|
|
245
245
|
click.echo(FeedbackManager.error(message=f"'tb {arg}' command is not available in watch mode"))
|
|
246
246
|
|
|
247
247
|
def handle_mock(self, arg):
|
|
248
|
-
subprocess.run(f"tb --folder {self.project.folder} mock {arg}", shell=True, text=True)
|
|
248
|
+
subprocess.run(f"tb --build --folder {self.project.folder} mock {arg}", shell=True, text=True)
|
|
249
249
|
|
|
250
250
|
def handle_tb(self, arg):
|
|
251
251
|
click.echo("")
|
|
@@ -259,7 +259,7 @@ class Shell:
|
|
|
259
259
|
elif arg.startswith("mock"):
|
|
260
260
|
self.handle_mock(arg)
|
|
261
261
|
else:
|
|
262
|
-
subprocess.run(f"tb --folder {self.project.folder} {arg}", shell=True, text=True)
|
|
262
|
+
subprocess.run(f"tb --build --folder {self.project.folder} {arg}", shell=True, text=True)
|
|
263
263
|
|
|
264
264
|
def default(self, argline):
|
|
265
265
|
click.echo("")
|
|
@@ -271,7 +271,7 @@ class Shell:
|
|
|
271
271
|
elif len(arg.split()) == 1 and arg in self.project.pipes + self.project.datasources:
|
|
272
272
|
self.run_sql(f"select * from {arg}")
|
|
273
273
|
else:
|
|
274
|
-
subprocess.run(f"tb --folder {self.project.folder} {arg}", shell=True, text=True)
|
|
274
|
+
subprocess.run(f"tb --build --folder {self.project.folder} {arg}", shell=True, text=True)
|
|
275
275
|
|
|
276
276
|
def run_sql(self, query, rows_limit=20):
|
|
277
277
|
try:
|
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
|
"""
|
|
@@ -96,7 +102,7 @@ async def test_create(ctx: click.Context, name_or_filename: str, prompt: str) ->
|
|
|
96
102
|
pipe_path = root_path / pipe_path
|
|
97
103
|
pipe_content = pipe_path.read_text()
|
|
98
104
|
|
|
99
|
-
client = await get_tinybird_local_client(folder)
|
|
105
|
+
client = await get_tinybird_local_client(folder, build=True)
|
|
100
106
|
pipe = await client._req(f"/v0/pipes/{pipe_name}")
|
|
101
107
|
parameters = set([param["name"] for node in pipe["nodes"] for param in node["params"]])
|
|
102
108
|
|
|
@@ -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:
|
|
@@ -155,7 +155,7 @@ def parse_file(file: str) -> Iterable[TestCase]:
|
|
|
155
155
|
)
|
|
156
156
|
except Exception as e:
|
|
157
157
|
raise CLIException(
|
|
158
|
-
f"""Error: {FeedbackManager.error_exception(error=e)} reading file, check "{file}"->"{definition.get(
|
|
158
|
+
f"""Error: {FeedbackManager.error_exception(error=e)} reading file, check "{file}"->"{definition.get("name")}" """
|
|
159
159
|
)
|
|
160
160
|
|
|
161
161
|
|
tinybird/tb/modules/update.py
CHANGED
tinybird/tb/modules/workspace.py
CHANGED
|
@@ -8,6 +8,7 @@ from typing import Optional
|
|
|
8
8
|
import click
|
|
9
9
|
from click import Context
|
|
10
10
|
|
|
11
|
+
from tinybird.client import TinyB
|
|
11
12
|
from tinybird.config import get_display_host
|
|
12
13
|
from tinybird.tb.modules.cli import cli
|
|
13
14
|
from tinybird.tb.modules.common import (
|
|
@@ -41,7 +42,7 @@ async def workspace_ls(ctx: Context) -> None:
|
|
|
41
42
|
"""List all the workspaces you have access to in the account you're currently authenticated into."""
|
|
42
43
|
|
|
43
44
|
config = CLIConfig.get_project_config()
|
|
44
|
-
client =
|
|
45
|
+
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
45
46
|
|
|
46
47
|
response = await client.user_workspaces()
|
|
47
48
|
|