tinybird 0.0.1.dev8__py3-none-any.whl → 0.0.1.dev10__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/feedback_manager.py +1 -1
- tinybird/tb/modules/build.py +125 -26
- tinybird/tb/modules/cicd.py +0 -1
- tinybird/tb/modules/cli.py +0 -3
- tinybird/tb/modules/common.py +35 -13
- tinybird/tb/modules/create.py +68 -94
- tinybird/tb/modules/datafile/build.py +13 -4
- tinybird/tb/modules/datafile/build_pipe.py +0 -1
- tinybird/tb/modules/datafile/common.py +12 -0
- tinybird/tb/modules/datafile/fixture.py +53 -0
- tinybird/tb/modules/llm.py +1 -1
- tinybird/tb/modules/local.py +36 -17
- tinybird/tb/modules/mock.py +15 -16
- tinybird/tb/modules/prompts.py +28 -2
- {tinybird-0.0.1.dev8.dist-info → tinybird-0.0.1.dev10.dist-info}/METADATA +1 -1
- {tinybird-0.0.1.dev8.dist-info → tinybird-0.0.1.dev10.dist-info}/RECORD +19 -18
- {tinybird-0.0.1.dev8.dist-info → tinybird-0.0.1.dev10.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev8.dist-info → tinybird-0.0.1.dev10.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev8.dist-info → tinybird-0.0.1.dev10.dist-info}/top_level.txt +0 -0
tinybird/feedback_manager.py
CHANGED
|
@@ -792,7 +792,7 @@ Ready? """
|
|
|
792
792
|
info_diff_resources_for_git_init = info_message(
|
|
793
793
|
"** Checking diffs between remote Workspace and local. Hint: use 'tb diff' to check if your Data Project and Workspace synced"
|
|
794
794
|
)
|
|
795
|
-
info_cicd_file_generated = info_message("
|
|
795
|
+
info_cicd_file_generated = info_message("✓ {file_path}")
|
|
796
796
|
info_available_git_providers = info_message("** List of available providers:")
|
|
797
797
|
info_git_release_init_without_diffs = info_message("** No diffs detected for '{workspace}'")
|
|
798
798
|
info_deployment_detecting_changes_header = info_message("\n** Detecting changes from last commit ...")
|
tinybird/tb/modules/build.py
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import cmd
|
|
2
3
|
import os
|
|
3
4
|
import random
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import threading
|
|
4
8
|
import time
|
|
5
9
|
from pathlib import Path
|
|
6
10
|
from typing import Any, Awaitable, Callable, List, Union
|
|
@@ -15,36 +19,79 @@ from tinybird.client import TinyB
|
|
|
15
19
|
from tinybird.config import FeatureFlags
|
|
16
20
|
from tinybird.feedback_manager import FeedbackManager, bcolors
|
|
17
21
|
from tinybird.tb.modules.cli import cli
|
|
18
|
-
from tinybird.tb.modules.common import
|
|
19
|
-
coro,
|
|
20
|
-
)
|
|
22
|
+
from tinybird.tb.modules.common import coro, push_data
|
|
21
23
|
from tinybird.tb.modules.datafile.build import folder_build
|
|
22
|
-
from tinybird.tb.modules.datafile.common import get_project_filenames, has_internal_datafiles
|
|
24
|
+
from tinybird.tb.modules.datafile.common import get_project_filenames, get_project_fixtures, has_internal_datafiles
|
|
23
25
|
from tinybird.tb.modules.datafile.exceptions import ParseException
|
|
26
|
+
from tinybird.tb.modules.datafile.fixture import build_fixture_name, get_fixture_dir
|
|
24
27
|
from tinybird.tb.modules.datafile.parse_datasource import parse_datasource
|
|
25
28
|
from tinybird.tb.modules.datafile.parse_pipe import parse_pipe
|
|
26
29
|
from tinybird.tb.modules.local import get_tinybird_local_client
|
|
27
30
|
from tinybird.tb.modules.table import format_table
|
|
28
31
|
|
|
29
32
|
|
|
33
|
+
class BuildShell(cmd.Cmd):
|
|
34
|
+
prompt = "\n\001\033[1;32m\002TB > \001\033[0m\002"
|
|
35
|
+
|
|
36
|
+
def __init__(self, folder: str):
|
|
37
|
+
super().__init__()
|
|
38
|
+
self.folder = folder
|
|
39
|
+
|
|
40
|
+
def do_exit(self, arg):
|
|
41
|
+
sys.exit(0)
|
|
42
|
+
|
|
43
|
+
def do_quit(self, arg):
|
|
44
|
+
sys.exit(0)
|
|
45
|
+
|
|
46
|
+
def default(self, argline):
|
|
47
|
+
click.echo("")
|
|
48
|
+
if argline.startswith("tb build"):
|
|
49
|
+
click.echo(FeedbackManager.error(message="Build command is already running"))
|
|
50
|
+
else:
|
|
51
|
+
arg_stripped = argline.strip().lower()
|
|
52
|
+
if not arg_stripped:
|
|
53
|
+
return
|
|
54
|
+
if arg_stripped.startswith("tb"):
|
|
55
|
+
extra_args = f" --folder {self.folder}" if arg_stripped.startswith("tb mock") else ""
|
|
56
|
+
subprocess.run(arg_stripped + extra_args, shell=True, text=True)
|
|
57
|
+
elif arg_stripped.startswith("with") or arg_stripped.startswith("select"):
|
|
58
|
+
subprocess.run(f'tb sql "{arg_stripped}"', shell=True, text=True)
|
|
59
|
+
elif arg_stripped.startswith("mock "):
|
|
60
|
+
subprocess.run(f"tb {arg_stripped} --folder {self.folder}", shell=True, text=True)
|
|
61
|
+
else:
|
|
62
|
+
click.echo(FeedbackManager.error(message="Invalid command"))
|
|
63
|
+
|
|
64
|
+
def reprint_prompt(self):
|
|
65
|
+
self.stdout.write(self.prompt)
|
|
66
|
+
self.stdout.flush()
|
|
67
|
+
|
|
68
|
+
|
|
30
69
|
class FileChangeHandler(FileSystemEventHandler):
|
|
31
70
|
def __init__(self, filenames: List[str], process: Callable[[List[str]], None]):
|
|
32
71
|
self.filenames = filenames
|
|
33
72
|
self.process = process
|
|
34
73
|
|
|
35
74
|
def on_modified(self, event: Any) -> None:
|
|
36
|
-
if not event.is_directory and any(event.src_path.endswith(ext) for ext in [".datasource", ".pipe"]):
|
|
75
|
+
if not event.is_directory and any(event.src_path.endswith(ext) for ext in [".datasource", ".pipe", ".ndjson"]):
|
|
37
76
|
filename = event.src_path.split("/")[-1]
|
|
38
|
-
click.echo(FeedbackManager.highlight(message=f"\n⟲ Changes detected in {filename}\n"))
|
|
77
|
+
click.echo(FeedbackManager.highlight(message=f"\n\n⟲ Changes detected in {filename}\n"))
|
|
39
78
|
try:
|
|
40
79
|
self.process([event.src_path])
|
|
41
80
|
except Exception as e:
|
|
42
81
|
click.echo(FeedbackManager.error_exception(error=e))
|
|
43
82
|
|
|
83
|
+
def on_created(self, event: Any) -> None:
|
|
84
|
+
click.echo(FeedbackManager.highlight(message=f"\n\n⟲ Changes detected in {event.src_path}\n"))
|
|
85
|
+
try:
|
|
86
|
+
self.process([event.src_path])
|
|
87
|
+
except Exception as e:
|
|
88
|
+
click.echo(FeedbackManager.error_exception(error=e))
|
|
89
|
+
|
|
44
90
|
|
|
45
91
|
def watch_files(
|
|
46
92
|
filenames: List[str],
|
|
47
93
|
process: Union[Callable[[List[str]], None], Callable[[List[str]], Awaitable[None]]],
|
|
94
|
+
shell: BuildShell,
|
|
48
95
|
) -> None:
|
|
49
96
|
# Handle both sync and async process functions
|
|
50
97
|
async def process_wrapper(files: List[str]) -> None:
|
|
@@ -60,6 +107,7 @@ def watch_files(
|
|
|
60
107
|
FeedbackManager.success(message="\n✓ ")
|
|
61
108
|
+ FeedbackManager.gray(message=f"Rebuild completed in {elapsed_time:.1f}s")
|
|
62
109
|
)
|
|
110
|
+
shell.reprint_prompt()
|
|
63
111
|
|
|
64
112
|
event_handler = FileChangeHandler(filenames, lambda f: asyncio.run(process_wrapper(f)))
|
|
65
113
|
observer = Observer()
|
|
@@ -110,7 +158,9 @@ async def build(
|
|
|
110
158
|
ignore_sql_errors = FeatureFlags.ignore_sql_errors()
|
|
111
159
|
context.disable_template_security_validation.set(True)
|
|
112
160
|
is_internal = has_internal_datafiles(folder)
|
|
113
|
-
|
|
161
|
+
|
|
162
|
+
folder_path = os.path.abspath(folder)
|
|
163
|
+
tb_client = await get_tinybird_local_client(folder_path)
|
|
114
164
|
|
|
115
165
|
def check_filenames(filenames: List[str]):
|
|
116
166
|
parser_matrix = {".pipe": parse_pipe, ".datasource": parse_datasource}
|
|
@@ -131,29 +181,53 @@ async def build(
|
|
|
131
181
|
parser(filename)
|
|
132
182
|
|
|
133
183
|
async def process(filenames: List[str], watch: bool = False, only_pipes: bool = False):
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
184
|
+
datafiles = [f for f in filenames if f.endswith(".datasource") or f.endswith(".pipe")]
|
|
185
|
+
if len(datafiles) > 0:
|
|
186
|
+
check_filenames(filenames=datafiles)
|
|
187
|
+
await folder_build(
|
|
188
|
+
tb_client,
|
|
189
|
+
filenames=datafiles,
|
|
190
|
+
ignore_sql_errors=ignore_sql_errors,
|
|
191
|
+
is_internal=is_internal,
|
|
192
|
+
only_pipes=only_pipes,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
filename = filenames[0]
|
|
196
|
+
if filename.endswith(".ndjson"):
|
|
197
|
+
fixture_path = Path(filename)
|
|
198
|
+
name = "_".join(fixture_path.stem.split("_")[:-1])
|
|
199
|
+
ds_path = Path(folder) / "datasources" / f"{name}.datasource"
|
|
200
|
+
if ds_path.exists():
|
|
201
|
+
await append_datasource({}, tb_client, name, str(fixture_path), silent=True)
|
|
142
202
|
|
|
143
203
|
if watch:
|
|
144
|
-
filename
|
|
145
|
-
|
|
146
|
-
|
|
204
|
+
if filename.endswith(".datasource"):
|
|
205
|
+
ds_path = Path(filename)
|
|
206
|
+
name = build_fixture_name(filename, ds_path.stem, ds_path.read_text())
|
|
207
|
+
fixture_path = get_fixture_dir() / f"{name}.ndjson"
|
|
208
|
+
if fixture_path.exists():
|
|
209
|
+
await append_datasource({}, tb_client, ds_path.stem, str(fixture_path), silent=True)
|
|
210
|
+
if not filename.endswith(".ndjson"):
|
|
211
|
+
await build_and_print_resource(tb_client, filename)
|
|
147
212
|
|
|
148
|
-
|
|
213
|
+
datafiles = get_project_filenames(folder)
|
|
214
|
+
fixtures = get_project_fixtures(folder)
|
|
215
|
+
filenames = datafiles + fixtures
|
|
149
216
|
|
|
150
217
|
async def build_once(filenames: List[str]):
|
|
151
218
|
try:
|
|
152
|
-
click.echo("⚡ Building project
|
|
219
|
+
click.echo("⚡ Building project...\n")
|
|
153
220
|
time_start = time.time()
|
|
154
221
|
await process(filenames=filenames, watch=False, only_pipes=skip_datasources)
|
|
155
222
|
time_end = time.time()
|
|
156
223
|
elapsed_time = time_end - time_start
|
|
224
|
+
for filename in filenames:
|
|
225
|
+
if filename.endswith(".datasource"):
|
|
226
|
+
ds_path = Path(filename)
|
|
227
|
+
name = build_fixture_name(filename, ds_path.stem, ds_path.read_text())
|
|
228
|
+
fixture_path = get_fixture_dir() / f"{name}.ndjson"
|
|
229
|
+
if fixture_path.exists():
|
|
230
|
+
await append_datasource({}, tb_client, ds_path.stem, str(fixture_path), silent=True)
|
|
157
231
|
click.echo(FeedbackManager.success(message=f"\n✓ Build completed in {elapsed_time:.1f}s\n"))
|
|
158
232
|
except Exception as e:
|
|
159
233
|
click.echo(FeedbackManager.error(message=str(e)))
|
|
@@ -161,16 +235,21 @@ async def build(
|
|
|
161
235
|
await build_once(filenames)
|
|
162
236
|
|
|
163
237
|
if watch:
|
|
238
|
+
shell = BuildShell(folder=folder)
|
|
164
239
|
click.echo(FeedbackManager.highlight(message="◎ Watching for changes..."))
|
|
165
|
-
watch_files(filenames, process)
|
|
240
|
+
watcher_thread = threading.Thread(target=watch_files, args=(filenames, process, shell), daemon=True)
|
|
241
|
+
watcher_thread.start()
|
|
242
|
+
shell.cmdloop()
|
|
166
243
|
|
|
167
244
|
|
|
168
|
-
async def
|
|
245
|
+
async def build_and_print_resource(tb_client: TinyB, filename: str):
|
|
169
246
|
rebuild_colors = [bcolors.FAIL, bcolors.OKBLUE, bcolors.WARNING, bcolors.OKGREEN, bcolors.HEADER]
|
|
170
247
|
rebuild_index = random.randint(0, len(rebuild_colors) - 1)
|
|
171
248
|
rebuild_color = rebuild_colors[rebuild_index % len(rebuild_colors)]
|
|
172
|
-
|
|
173
|
-
|
|
249
|
+
resource_path = Path(filename)
|
|
250
|
+
name = resource_path.stem
|
|
251
|
+
pipeline = name if filename.endswith(".pipe") else None
|
|
252
|
+
res = await tb_client.query(f"SELECT * FROM {name} FORMAT JSON", pipeline=pipeline)
|
|
174
253
|
data = []
|
|
175
254
|
limit = 5
|
|
176
255
|
for d in res["data"][:5]:
|
|
@@ -179,7 +258,6 @@ async def build_and_print_pipe(tb_client: TinyB, filename: str):
|
|
|
179
258
|
row_count = res.get("rows", 0)
|
|
180
259
|
stats = res.get("statistics", {})
|
|
181
260
|
elapsed = stats.get("elapsed", 0)
|
|
182
|
-
node_name = "endpoint"
|
|
183
261
|
cols = len(meta)
|
|
184
262
|
try:
|
|
185
263
|
|
|
@@ -189,7 +267,7 @@ async def build_and_print_pipe(tb_client: TinyB, filename: str):
|
|
|
189
267
|
table = format_table(data, meta)
|
|
190
268
|
colored_char = print_message("│", rebuild_color)
|
|
191
269
|
table_with_marker = "\n".join(f"{colored_char} {line}" for line in table.split("\n"))
|
|
192
|
-
click.echo(f"\n{colored_char} {print_message('⚡', rebuild_color)} Running {
|
|
270
|
+
click.echo(f"\n{colored_char} {print_message('⚡', rebuild_color)} Running {name}")
|
|
193
271
|
click.echo(colored_char)
|
|
194
272
|
click.echo(table_with_marker)
|
|
195
273
|
click.echo(colored_char)
|
|
@@ -207,3 +285,24 @@ async def build_and_print_pipe(tb_client: TinyB, filename: str):
|
|
|
207
285
|
click.echo("------------")
|
|
208
286
|
else:
|
|
209
287
|
raise exc
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
async def append_datasource(
|
|
291
|
+
ctx: click.Context,
|
|
292
|
+
tb_client: TinyB,
|
|
293
|
+
datasource_name: str,
|
|
294
|
+
url: str,
|
|
295
|
+
silent: bool = False,
|
|
296
|
+
):
|
|
297
|
+
await push_data(
|
|
298
|
+
ctx,
|
|
299
|
+
tb_client,
|
|
300
|
+
datasource_name,
|
|
301
|
+
url,
|
|
302
|
+
connector=None,
|
|
303
|
+
sql=None,
|
|
304
|
+
mode="append",
|
|
305
|
+
ignore_empty=False,
|
|
306
|
+
concurrency=1,
|
|
307
|
+
silent=silent,
|
|
308
|
+
)
|
tinybird/tb/modules/cicd.py
CHANGED
|
@@ -240,7 +240,6 @@ class GitLabCICDGenerator(CICDGeneratorBase):
|
|
|
240
240
|
template=GITLAB_CI_YML,
|
|
241
241
|
file_name="tinybird-ci.yml",
|
|
242
242
|
dir_path=".gitlab/tinybird",
|
|
243
|
-
warning_message="Make sure to import the file in your .gitlab-ci.yml file, e.g., `include: '.gitlab/tinybird/*.yml'`.",
|
|
244
243
|
),
|
|
245
244
|
]
|
|
246
245
|
|
tinybird/tb/modules/cli.py
CHANGED
|
@@ -158,9 +158,6 @@ async def cli(
|
|
|
158
158
|
|
|
159
159
|
latest_version = await CheckPypi().get_latest_version()
|
|
160
160
|
|
|
161
|
-
if "x.y.z" in CURRENT_VERSION:
|
|
162
|
-
click.echo(FeedbackManager.warning_development_cli())
|
|
163
|
-
|
|
164
161
|
if "x.y.z" not in CURRENT_VERSION and latest_version != CURRENT_VERSION:
|
|
165
162
|
click.echo(FeedbackManager.warning_update_version(latest_version=latest_version))
|
|
166
163
|
click.echo(FeedbackManager.warning_current_version(current_version=CURRENT_VERSION))
|
tinybird/tb/modules/common.py
CHANGED
|
@@ -11,6 +11,7 @@ import os
|
|
|
11
11
|
import re
|
|
12
12
|
import socket
|
|
13
13
|
import sys
|
|
14
|
+
import time
|
|
14
15
|
import uuid
|
|
15
16
|
from contextlib import closing
|
|
16
17
|
from copy import deepcopy
|
|
@@ -166,12 +167,12 @@ def generate_datafile(
|
|
|
166
167
|
data: Optional[bytes],
|
|
167
168
|
force: Optional[bool] = False,
|
|
168
169
|
_format: Optional[str] = "csv",
|
|
169
|
-
|
|
170
|
+
folder: Optional[str] = None,
|
|
170
171
|
):
|
|
171
172
|
p = Path(filename)
|
|
172
173
|
base = Path("datasources")
|
|
173
|
-
if
|
|
174
|
-
base = Path(
|
|
174
|
+
if folder:
|
|
175
|
+
base = Path(folder) / base
|
|
175
176
|
datasource_name = normalize_datasource_name(p.stem)
|
|
176
177
|
if not base.exists():
|
|
177
178
|
base = Path()
|
|
@@ -179,7 +180,7 @@ def generate_datafile(
|
|
|
179
180
|
if not f.exists() or force:
|
|
180
181
|
with open(f"{f}", "w") as ds_file:
|
|
181
182
|
ds_file.write(datafile)
|
|
182
|
-
click.echo(FeedbackManager.info_file_created(file=f))
|
|
183
|
+
click.echo(FeedbackManager.info_file_created(file=str(f)))
|
|
183
184
|
|
|
184
185
|
if data and (base / "fixtures").exists():
|
|
185
186
|
# Generating a fixture for Parquet files is not so trivial, since Parquet format
|
|
@@ -1078,6 +1079,7 @@ async def push_data(
|
|
|
1078
1079
|
replace_options=None,
|
|
1079
1080
|
ignore_empty: bool = False,
|
|
1080
1081
|
concurrency: int = 1,
|
|
1082
|
+
silent: bool = False,
|
|
1081
1083
|
):
|
|
1082
1084
|
if url and type(url) is tuple:
|
|
1083
1085
|
url = url[0]
|
|
@@ -1088,7 +1090,8 @@ async def push_data(
|
|
|
1088
1090
|
raise CLIException(FeedbackManager.error_connector_not_configured(connector=connector))
|
|
1089
1091
|
else:
|
|
1090
1092
|
_connector: "Connector" = ctx.obj[connector]
|
|
1091
|
-
|
|
1093
|
+
if not silent:
|
|
1094
|
+
click.echo(FeedbackManager.info_starting_export_process(connector=connector))
|
|
1092
1095
|
try:
|
|
1093
1096
|
url = _connector.export_to_gcs(sql, datasource_name, mode)
|
|
1094
1097
|
except ConnectorNothingToLoad as e:
|
|
@@ -1116,7 +1119,8 @@ async def push_data(
|
|
|
1116
1119
|
cb.First = True # type: ignore[attr-defined]
|
|
1117
1120
|
cb.prev_done = 0 # type: ignore[attr-defined]
|
|
1118
1121
|
|
|
1119
|
-
|
|
1122
|
+
if not silent:
|
|
1123
|
+
click.echo(FeedbackManager.gray(message=f"\nImporting data to {datasource_name}..."))
|
|
1120
1124
|
|
|
1121
1125
|
if isinstance(url, list):
|
|
1122
1126
|
urls = url
|
|
@@ -1187,15 +1191,14 @@ async def push_data(
|
|
|
1187
1191
|
except Exception as e:
|
|
1188
1192
|
raise CLIException(FeedbackManager.error_exception(error=e))
|
|
1189
1193
|
else:
|
|
1190
|
-
if
|
|
1191
|
-
|
|
1194
|
+
if not silent:
|
|
1195
|
+
if mode == "append" and parser and parser != "clickhouse":
|
|
1196
|
+
click.echo(FeedbackManager.success_appended_rows(appended_rows=appended_rows))
|
|
1192
1197
|
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
else:
|
|
1196
|
-
click.echo(FeedbackManager.highlight(message="» 2.57m rows x 9 cols in 852.04ms"))
|
|
1198
|
+
if mode == "replace":
|
|
1199
|
+
click.echo(FeedbackManager.success_replaced_datasource(datasource=datasource_name))
|
|
1197
1200
|
|
|
1198
|
-
|
|
1201
|
+
click.echo(FeedbackManager.success_progress_blocks())
|
|
1199
1202
|
|
|
1200
1203
|
finally:
|
|
1201
1204
|
try:
|
|
@@ -2108,3 +2111,22 @@ def get_ca_pem_content(ca_pem: Optional[str], filename: Optional[str] = None) ->
|
|
|
2108
2111
|
|
|
2109
2112
|
requests_get = sync_to_async(requests.get, thread_sensitive=False)
|
|
2110
2113
|
requests_delete = sync_to_async(requests.delete, thread_sensitive=False)
|
|
2114
|
+
|
|
2115
|
+
|
|
2116
|
+
def format_data_to_ndjson(data: List[Dict[str, Any]]) -> str:
|
|
2117
|
+
return "\n".join([json.dumps(row) for row in data])
|
|
2118
|
+
|
|
2119
|
+
|
|
2120
|
+
async def send_batch_events(
|
|
2121
|
+
client: TinyB, datasource_name: str, data: List[Dict[str, Any]], batch_size: int = 10
|
|
2122
|
+
) -> None:
|
|
2123
|
+
rows = len(data)
|
|
2124
|
+
time_start = time.time()
|
|
2125
|
+
for i in range(0, rows, batch_size):
|
|
2126
|
+
batch = data[i : i + batch_size]
|
|
2127
|
+
ndjson_data = format_data_to_ndjson(batch)
|
|
2128
|
+
await client.datasource_events(datasource_name, ndjson_data)
|
|
2129
|
+
time_end = time.time()
|
|
2130
|
+
elapsed_time = time_end - time_start
|
|
2131
|
+
cols = len(data[0].keys()) if len(data) > 0 else 0
|
|
2132
|
+
click.echo(FeedbackManager.highlight(message=f"» {rows} rows x {cols} cols in {elapsed_time:.1f}s"))
|
tinybird/tb/modules/create.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import subprocess
|
|
2
3
|
from os import getcwd
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
from typing import Optional
|
|
@@ -10,14 +11,12 @@ from tinybird.client import TinyB
|
|
|
10
11
|
from tinybird.feedback_manager import FeedbackManager
|
|
11
12
|
from tinybird.tb.modules.cicd import init_cicd
|
|
12
13
|
from tinybird.tb.modules.cli import cli
|
|
13
|
-
from tinybird.tb.modules.common import _generate_datafile, coro, generate_datafile
|
|
14
|
+
from tinybird.tb.modules.common import _generate_datafile, coro, generate_datafile
|
|
14
15
|
from tinybird.tb.modules.config import CLIConfig
|
|
15
|
-
from tinybird.tb.modules.datafile.
|
|
16
|
-
from tinybird.tb.modules.exceptions import
|
|
16
|
+
from tinybird.tb.modules.datafile.fixture import build_fixture_name, persist_fixture
|
|
17
|
+
from tinybird.tb.modules.exceptions import CLIException
|
|
17
18
|
from tinybird.tb.modules.llm import LLM
|
|
18
|
-
from tinybird.tb.modules.local import
|
|
19
|
-
get_tinybird_local_client,
|
|
20
|
-
)
|
|
19
|
+
from tinybird.tb.modules.local import get_tinybird_local_client
|
|
21
20
|
|
|
22
21
|
|
|
23
22
|
@cli.command()
|
|
@@ -39,6 +38,7 @@ from tinybird.tb.modules.local import (
|
|
|
39
38
|
type=click.Path(exists=True, file_okay=False),
|
|
40
39
|
help="Folder where datafiles will be placed",
|
|
41
40
|
)
|
|
41
|
+
@click.option("--rows", type=int, default=10, help="Number of events to send")
|
|
42
42
|
@click.pass_context
|
|
43
43
|
@coro
|
|
44
44
|
async def create(
|
|
@@ -46,22 +46,30 @@ async def create(
|
|
|
46
46
|
data: Optional[str],
|
|
47
47
|
prompt: Optional[str],
|
|
48
48
|
folder: Optional[str],
|
|
49
|
+
rows: int,
|
|
49
50
|
) -> None:
|
|
50
51
|
"""Initialize a new project."""
|
|
51
|
-
click.echo(FeedbackManager.gray(message="Setting up Tinybird Local"))
|
|
52
52
|
folder = folder or getcwd()
|
|
53
53
|
try:
|
|
54
|
-
tb_client = get_tinybird_local_client()
|
|
54
|
+
tb_client = await get_tinybird_local_client(os.path.abspath(folder))
|
|
55
55
|
click.echo(FeedbackManager.gray(message="Creating new project structure..."))
|
|
56
56
|
await project_create(tb_client, data, prompt, folder)
|
|
57
57
|
click.echo(FeedbackManager.success(message="✓ Scaffolding completed!\n"))
|
|
58
|
-
await folder_build(tb_client, folder=folder)
|
|
59
58
|
|
|
59
|
+
click.echo(FeedbackManager.gray(message="\nCreating CI/CD files for GitHub and GitLab..."))
|
|
60
|
+
init_git(folder)
|
|
60
61
|
await init_cicd(data_project_dir=os.path.relpath(folder))
|
|
62
|
+
click.echo(FeedbackManager.success(message="✓ Done!\n"))
|
|
63
|
+
|
|
64
|
+
click.echo(FeedbackManager.gray(message="Building fixtures..."))
|
|
61
65
|
|
|
62
66
|
if data:
|
|
63
67
|
ds_name = os.path.basename(data.split(".")[0])
|
|
64
|
-
|
|
68
|
+
data_content = Path(data).read_text()
|
|
69
|
+
datasource_path = Path(folder) / "datasources" / f"{ds_name}.datasource"
|
|
70
|
+
fixture_name = build_fixture_name(datasource_path.absolute(), ds_name, datasource_path.read_text())
|
|
71
|
+
click.echo(FeedbackManager.info(message=f"✓ /fixtures/{ds_name}"))
|
|
72
|
+
persist_fixture(fixture_name, data_content)
|
|
65
73
|
elif prompt:
|
|
66
74
|
datasource_files = [f for f in os.listdir(Path(folder) / "datasources") if f.endswith(".datasource")]
|
|
67
75
|
for datasource_file in datasource_files:
|
|
@@ -72,8 +80,16 @@ async def create(
|
|
|
72
80
|
datasource_content = datasource_path.read_text()
|
|
73
81
|
has_json_path = "`json:" in datasource_content
|
|
74
82
|
if has_json_path:
|
|
75
|
-
await llm.generate_sql_sample_data(tb_client, datasource_name, datasource_content)
|
|
76
|
-
|
|
83
|
+
sql = await llm.generate_sql_sample_data(tb_client, datasource_name, datasource_content, rows)
|
|
84
|
+
result = await tb_client.query(f"{sql} FORMAT JSON")
|
|
85
|
+
data = result.get("data", [])
|
|
86
|
+
fixture_name = build_fixture_name(datasource_path.absolute(), datasource_name, datasource_content)
|
|
87
|
+
persist_fixture(fixture_name, data)
|
|
88
|
+
click.echo(FeedbackManager.info(message=f"✓ /fixtures/{datasource_name}"))
|
|
89
|
+
|
|
90
|
+
click.echo(FeedbackManager.success(message="✓ Done!\n"))
|
|
91
|
+
|
|
92
|
+
click.echo(FeedbackManager.success(message="✓ Tinybird local is ready!"))
|
|
77
93
|
except Exception as e:
|
|
78
94
|
click.echo(FeedbackManager.error(message=f"Error: {str(e)}"))
|
|
79
95
|
|
|
@@ -84,31 +100,23 @@ async def project_create(
|
|
|
84
100
|
prompt: Optional[str],
|
|
85
101
|
folder: str,
|
|
86
102
|
):
|
|
87
|
-
project_paths = ["datasources", "endpoints", "materializations", "copies", "sinks"]
|
|
103
|
+
project_paths = ["datasources", "endpoints", "materializations", "copies", "sinks", "fixtures"]
|
|
88
104
|
force = True
|
|
89
105
|
for x in project_paths:
|
|
90
106
|
try:
|
|
91
107
|
f = Path(folder) / x
|
|
92
108
|
f.mkdir()
|
|
93
|
-
click.echo(FeedbackManager.info_path_created(path=x))
|
|
94
109
|
except FileExistsError:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def generate_pipe_file(name: str, content: str, parent_dir: Optional[str] = None):
|
|
98
|
-
base = Path("endpoints")
|
|
99
|
-
if parent_dir:
|
|
100
|
-
base = Path(parent_dir) / base
|
|
101
|
-
if not base.exists():
|
|
102
|
-
base = Path()
|
|
103
|
-
f = base / (f"{name}.pipe")
|
|
104
|
-
with open(f"{f}", "w") as file:
|
|
105
|
-
file.write(content)
|
|
106
|
-
click.echo(FeedbackManager.info_file_created(file=f))
|
|
110
|
+
pass
|
|
111
|
+
click.echo(FeedbackManager.info_path_created(path=x))
|
|
107
112
|
|
|
108
113
|
if data:
|
|
109
114
|
path = Path(folder) / data
|
|
110
115
|
format = path.suffix.lstrip(".")
|
|
111
|
-
|
|
116
|
+
try:
|
|
117
|
+
await _generate_datafile(str(path), client, format=format, force=force)
|
|
118
|
+
except Exception as e:
|
|
119
|
+
click.echo(FeedbackManager.error(message=f"Ersssssror: {str(e)}"))
|
|
112
120
|
name = data.split(".")[0]
|
|
113
121
|
generate_pipe_file(
|
|
114
122
|
f"{name}_endpoint",
|
|
@@ -118,6 +126,7 @@ SQL >
|
|
|
118
126
|
SELECT * from {name}
|
|
119
127
|
TYPE ENDPOINT
|
|
120
128
|
""",
|
|
129
|
+
folder,
|
|
121
130
|
)
|
|
122
131
|
elif prompt:
|
|
123
132
|
try:
|
|
@@ -126,77 +135,42 @@ TYPE ENDPOINT
|
|
|
126
135
|
result = await llm.create_project(prompt)
|
|
127
136
|
for ds in result.datasources:
|
|
128
137
|
content = ds.content.replace("```", "")
|
|
129
|
-
generate_datafile(
|
|
138
|
+
generate_datafile(
|
|
139
|
+
content, filename=f"{ds.name}.datasource", data=None, _format="ndjson", force=force, folder=folder
|
|
140
|
+
)
|
|
130
141
|
|
|
131
142
|
for pipe in result.pipes:
|
|
132
143
|
content = pipe.content.replace("```", "")
|
|
133
|
-
generate_pipe_file(pipe.name, content)
|
|
144
|
+
generate_pipe_file(pipe.name, content, folder)
|
|
134
145
|
except Exception as e:
|
|
135
146
|
click.echo(FeedbackManager.error(message=f"Error: {str(e)}"))
|
|
136
|
-
else:
|
|
137
|
-
events_ds = """
|
|
138
|
-
SCHEMA >
|
|
139
|
-
`age` Int16 `json:$.age`,
|
|
140
|
-
`airline` String `json:$.airline`,
|
|
141
|
-
`email` String `json:$.email`,
|
|
142
|
-
`extra_bags` Int16 `json:$.extra_bags`,
|
|
143
|
-
`flight_from` String `json:$.flight_from`,
|
|
144
|
-
`flight_to` String `json:$.flight_to`,
|
|
145
|
-
`meal_choice` String `json:$.meal_choice`,
|
|
146
|
-
`name` String `json:$.name`,
|
|
147
|
-
`passport_number` Int32 `json:$.passport_number`,
|
|
148
|
-
`priority_boarding` UInt8 `json:$.priority_boarding`,
|
|
149
|
-
`timestamp` DateTime `json:$.timestamp`,
|
|
150
|
-
`transaction_id` String `json:$.transaction_id`
|
|
151
|
-
|
|
152
|
-
ENGINE "MergeTree"
|
|
153
|
-
ENGINE_PARTITION_KEY "toYear(timestamp)"
|
|
154
|
-
ENGINE_SORTING_KEY "airline, timestamp"
|
|
155
|
-
"""
|
|
156
|
-
top_airlines = """
|
|
157
|
-
NODE endpoint
|
|
158
|
-
SQL >
|
|
159
|
-
SELECT airline, count() as bookings FROM events
|
|
160
|
-
GROUP BY airline
|
|
161
|
-
ORDER BY bookings DESC
|
|
162
|
-
LIMIT 5
|
|
163
|
-
TYPE ENDPOINT
|
|
164
|
-
"""
|
|
165
|
-
generate_datafile(
|
|
166
|
-
events_ds, filename="events.datasource", data=None, _format="ndjson", force=force, parent_dir=folder
|
|
167
|
-
)
|
|
168
|
-
generate_pipe_file("top_airlines", top_airlines, parent_dir=folder)
|
|
169
147
|
|
|
170
148
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
mode="append",
|
|
200
|
-
ignore_empty=ignore_empty,
|
|
201
|
-
concurrency=concurrency,
|
|
202
|
-
)
|
|
149
|
+
def init_git(folder: str):
|
|
150
|
+
try:
|
|
151
|
+
path = Path(folder)
|
|
152
|
+
gitignore_file = path / ".gitignore"
|
|
153
|
+
git_folder = path / ".git"
|
|
154
|
+
if not git_folder.exists():
|
|
155
|
+
subprocess.run(["git", "init"], cwd=path, check=True, capture_output=True)
|
|
156
|
+
|
|
157
|
+
if gitignore_file.exists():
|
|
158
|
+
content = gitignore_file.read_text()
|
|
159
|
+
if ".tinyb" not in content:
|
|
160
|
+
gitignore_file.write_text(content + "\n.tinyb\n")
|
|
161
|
+
else:
|
|
162
|
+
gitignore_file.write_text(".tinyb\n")
|
|
163
|
+
|
|
164
|
+
click.echo(FeedbackManager.info_file_created(file=".gitignore"))
|
|
165
|
+
except Exception as e:
|
|
166
|
+
raise CLIException(f"Error initializing Git: {e}")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def generate_pipe_file(name: str, content: str, folder: str):
|
|
170
|
+
base = Path(folder) / "endpoints"
|
|
171
|
+
if not base.exists():
|
|
172
|
+
base = Path()
|
|
173
|
+
f = base / (f"{name}.pipe")
|
|
174
|
+
with open(f"{f}", "w") as file:
|
|
175
|
+
file.write(content)
|
|
176
|
+
click.echo(FeedbackManager.info_file_created(file=f.relative_to(folder)))
|
|
@@ -32,7 +32,6 @@ from tinybird.tb.modules.datafile.common import (
|
|
|
32
32
|
INTERNAL_TABLES,
|
|
33
33
|
ON_DEMAND,
|
|
34
34
|
PREVIEW_CONNECTOR_SERVICES,
|
|
35
|
-
TB_LOCAL_WORKSPACE_NAME,
|
|
36
35
|
CopyModes,
|
|
37
36
|
CopyParameters,
|
|
38
37
|
DataFileExtensions,
|
|
@@ -58,6 +57,7 @@ async def folder_build(
|
|
|
58
57
|
only_pipes: bool = False,
|
|
59
58
|
is_vendor: bool = False,
|
|
60
59
|
current_ws: Optional[Dict[str, Any]] = None,
|
|
60
|
+
local_ws: Optional[Dict[str, Any]] = None,
|
|
61
61
|
workspaces: Optional[List[Dict[str, Any]]] = None,
|
|
62
62
|
):
|
|
63
63
|
if only_pipes:
|
|
@@ -117,8 +117,9 @@ async def folder_build(
|
|
|
117
117
|
user_client.token = user_token
|
|
118
118
|
|
|
119
119
|
vendor_workspaces = []
|
|
120
|
-
|
|
120
|
+
|
|
121
121
|
if vendor_path.exists() and not is_vendor:
|
|
122
|
+
user_workspaces = await user_client.user_workspaces()
|
|
122
123
|
for x in vendor_path.iterdir():
|
|
123
124
|
if x.is_dir() and x.name not in existing_workspaces:
|
|
124
125
|
if user_token:
|
|
@@ -133,15 +134,23 @@ async def folder_build(
|
|
|
133
134
|
workspace_lib_paths.append((x.name, x))
|
|
134
135
|
|
|
135
136
|
workspaces: List[Dict[str, Any]] = (await user_client.user_workspaces()).get("workspaces", [])
|
|
136
|
-
|
|
137
|
+
|
|
138
|
+
if not is_vendor:
|
|
139
|
+
local_workspace = await tb_client.workspace_info()
|
|
140
|
+
local_ws_id = local_workspace.get("id")
|
|
141
|
+
local_ws = next((ws for ws in workspaces if ws["id"] == local_ws_id), {})
|
|
142
|
+
|
|
137
143
|
current_ws: Dict[str, Any] = current_ws or local_ws
|
|
144
|
+
|
|
138
145
|
for vendor_ws in [ws for ws in workspaces if ws["name"] in [ws["name"] for ws in vendor_workspaces]]:
|
|
139
146
|
ws_client = deepcopy(tb_client)
|
|
140
147
|
ws_client.token = vendor_ws["token"]
|
|
141
148
|
shared_ws_path = Path(folder) / "vendor" / vendor_ws["name"]
|
|
142
149
|
|
|
143
150
|
if shared_ws_path.exists() and not is_vendor:
|
|
144
|
-
await folder_build(
|
|
151
|
+
await folder_build(
|
|
152
|
+
ws_client, folder=shared_ws_path.as_posix(), is_vendor=True, current_ws=vendor_ws, local_ws=local_ws
|
|
153
|
+
)
|
|
145
154
|
|
|
146
155
|
datasources: List[Dict[str, Any]] = await tb_client.datasources()
|
|
147
156
|
pipes: List[Dict[str, Any]] = await tb_client.pipes(dependencies=True)
|
|
@@ -256,7 +256,6 @@ async def new_pipe(
|
|
|
256
256
|
raise click.ClickException(FeedbackManager.error_creating_pipe(error=e))
|
|
257
257
|
else:
|
|
258
258
|
token_name = tk["token_name"]
|
|
259
|
-
click.echo(FeedbackManager.info_create_not_found_token(token=token_name))
|
|
260
259
|
try:
|
|
261
260
|
r = await tb_client.create_token(
|
|
262
261
|
token_name, [f"PIPES:{tk['permissions']}:{p['name']}"], "P", p["name"]
|
|
@@ -662,6 +662,18 @@ def get_project_filenames(folder: str, with_vendor=False) -> List[str]:
|
|
|
662
662
|
]
|
|
663
663
|
if with_vendor:
|
|
664
664
|
folders.append(f"{folder}/vendor/**/**/*.datasource")
|
|
665
|
+
|
|
666
|
+
filenames: List[str] = []
|
|
667
|
+
for x in folders:
|
|
668
|
+
filenames += glob.glob(x)
|
|
669
|
+
return filenames
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def get_project_fixtures(folder: str) -> List[str]:
|
|
673
|
+
folders: List[str] = [
|
|
674
|
+
f"{folder}/fixtures/*.ndjson",
|
|
675
|
+
f"{folder}/fixtures/*.csv",
|
|
676
|
+
]
|
|
665
677
|
filenames: List[str] = []
|
|
666
678
|
for x in folders:
|
|
667
679
|
filenames += glob.glob(x)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Dict, List, Union
|
|
4
|
+
|
|
5
|
+
from tinybird.tb.modules.common import format_data_to_ndjson
|
|
6
|
+
from tinybird.tb.modules.datafile.parse_datasource import parse_datasource
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def build_fixture_name(filename: str, datasource_name: str, datasource_content: str) -> str:
|
|
10
|
+
"""Generate a unique fixture name based on datasource properties.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
datasource_name: Name of the datasource
|
|
14
|
+
datasource_content: Content of the datasource file
|
|
15
|
+
row_count: Number of rows requested
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
str: A unique fixture name combining a hash of the inputs with the datasource name
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
doc = parse_datasource(filename, datasource_content)
|
|
22
|
+
schema = doc.nodes[0].get("schema", "").strip()
|
|
23
|
+
# Combine all inputs into a single string
|
|
24
|
+
combined = f"{datasource_name}{schema}"
|
|
25
|
+
|
|
26
|
+
# Generate hash
|
|
27
|
+
hash_obj = hashlib.sha256(combined.encode())
|
|
28
|
+
hash_str = hash_obj.hexdigest()[:8]
|
|
29
|
+
|
|
30
|
+
# Return fixture name with hash
|
|
31
|
+
return f"{datasource_name}_{hash_str}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_fixture_dir() -> Path:
|
|
35
|
+
fixture_dir = Path("fixtures")
|
|
36
|
+
if not fixture_dir.exists():
|
|
37
|
+
fixture_dir.mkdir()
|
|
38
|
+
return fixture_dir
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def persist_fixture(fixture_name: str, data: Union[List[Dict[str, Any]], str], format="ndjson") -> Path:
|
|
42
|
+
fixture_dir = get_fixture_dir()
|
|
43
|
+
fixture_file = fixture_dir / f"{fixture_name}.{format}"
|
|
44
|
+
fixture_file.write_text(data if isinstance(data, str) else format_data_to_ndjson(data))
|
|
45
|
+
return fixture_file
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_fixture(fixture_name: str, format="ndjson") -> Union[Path, None]:
|
|
49
|
+
fixture_dir = get_fixture_dir()
|
|
50
|
+
fixture_file = fixture_dir / f"{fixture_name}.{format}"
|
|
51
|
+
if not fixture_file.exists():
|
|
52
|
+
return None
|
|
53
|
+
return fixture_file
|
tinybird/tb/modules/llm.py
CHANGED
|
@@ -39,7 +39,7 @@ class LLM:
|
|
|
39
39
|
|
|
40
40
|
async def create_project(self, prompt: str) -> DataProject:
|
|
41
41
|
completion = self.client.beta.chat.completions.parse(
|
|
42
|
-
model="gpt-4o
|
|
42
|
+
model="gpt-4o",
|
|
43
43
|
messages=[{"role": "system", "content": create_project_prompt}, {"role": "user", "content": prompt}],
|
|
44
44
|
response_format=DataProject,
|
|
45
45
|
)
|
tinybird/tb/modules/local.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import hashlib
|
|
1
2
|
import os
|
|
2
3
|
import time
|
|
4
|
+
from typing import Optional
|
|
3
5
|
|
|
4
6
|
import click
|
|
5
7
|
import requests
|
|
@@ -39,7 +41,7 @@ def start_tinybird_local(
|
|
|
39
41
|
pull_show_prompt
|
|
40
42
|
and click.prompt(FeedbackManager.info(message="** New version detected, download? [y/N]")).lower() == "y"
|
|
41
43
|
):
|
|
42
|
-
click.echo(FeedbackManager.info(message="** Downloading latest version of Tinybird
|
|
44
|
+
click.echo(FeedbackManager.info(message="** Downloading latest version of Tinybird local..."))
|
|
43
45
|
pull_required = True
|
|
44
46
|
|
|
45
47
|
if pull_required:
|
|
@@ -66,7 +68,7 @@ def start_tinybird_local(
|
|
|
66
68
|
platform="linux/amd64",
|
|
67
69
|
)
|
|
68
70
|
|
|
69
|
-
click.echo(FeedbackManager.info(message="** Waiting for Tinybird
|
|
71
|
+
click.echo(FeedbackManager.info(message="** Waiting for Tinybird local to be ready..."))
|
|
70
72
|
for attempt in range(10):
|
|
71
73
|
try:
|
|
72
74
|
run = container.exec_run("tb --no-version-warning sql 'SELECT 1 AS healthcheck' --format json").output
|
|
@@ -111,16 +113,33 @@ def remove_tinybird_local(docker_client):
|
|
|
111
113
|
pass
|
|
112
114
|
|
|
113
115
|
|
|
114
|
-
def get_tinybird_local_client():
|
|
116
|
+
async def get_tinybird_local_client(path: Optional[str] = None):
|
|
115
117
|
"""Get a Tinybird client connected to the local environment."""
|
|
116
118
|
config = CLIConfig.get_project_config()
|
|
117
119
|
try:
|
|
120
|
+
# ruff: noqa: ASYNC210
|
|
118
121
|
tokens = requests.get(f"{TB_LOCAL_HOST}/tokens").json()
|
|
119
122
|
except Exception:
|
|
120
|
-
raise CLIException("Tinybird local
|
|
123
|
+
raise CLIException("Tinybird local is not running. Please run `tb local start` first.")
|
|
121
124
|
|
|
122
|
-
token = tokens["workspace_admin_token"]
|
|
123
125
|
user_token = tokens["user_token"]
|
|
126
|
+
token = tokens["workspace_admin_token"]
|
|
127
|
+
# Create a new workspace if path is provided. This is used to isolate the build in a different workspace.
|
|
128
|
+
if path:
|
|
129
|
+
folder_hash = hashlib.sha256(path.encode()).hexdigest()
|
|
130
|
+
user_client = config.get_client(host=TB_LOCAL_HOST, token=user_token)
|
|
131
|
+
|
|
132
|
+
ws_name = f"Tinybird_Local_Build_{folder_hash}"
|
|
133
|
+
|
|
134
|
+
user_workspaces = await user_client.user_workspaces()
|
|
135
|
+
ws = next((ws for ws in user_workspaces["workspaces"] if ws["name"] == ws_name), None)
|
|
136
|
+
if not ws:
|
|
137
|
+
await user_client.create_workspace(ws_name, template=None)
|
|
138
|
+
user_workspaces = await user_client.user_workspaces()
|
|
139
|
+
ws = next((ws for ws in user_workspaces["workspaces"] if ws["name"] == ws_name), None)
|
|
140
|
+
|
|
141
|
+
token = ws["token"]
|
|
142
|
+
|
|
124
143
|
config.set_token(token)
|
|
125
144
|
config.set_host(TB_LOCAL_HOST)
|
|
126
145
|
config.set_user_token(user_token)
|
|
@@ -137,39 +156,39 @@ def local(ctx):
|
|
|
137
156
|
@local.command()
|
|
138
157
|
@coro
|
|
139
158
|
async def stop() -> None:
|
|
140
|
-
"""Stop Tinybird
|
|
141
|
-
click.echo(FeedbackManager.info(message="Shutting down Tinybird
|
|
159
|
+
"""Stop Tinybird local"""
|
|
160
|
+
click.echo(FeedbackManager.info(message="Shutting down Tinybird local..."))
|
|
142
161
|
docker_client = get_docker_client()
|
|
143
162
|
stop_tinybird_local(docker_client)
|
|
144
|
-
click.echo(FeedbackManager.success(message="Tinybird
|
|
163
|
+
click.echo(FeedbackManager.success(message="Tinybird local stopped"))
|
|
145
164
|
|
|
146
165
|
|
|
147
166
|
@local.command()
|
|
148
167
|
@coro
|
|
149
168
|
async def remove() -> None:
|
|
150
|
-
"""Remove Tinybird
|
|
151
|
-
click.echo(FeedbackManager.info(message="Removing Tinybird
|
|
169
|
+
"""Remove Tinybird local"""
|
|
170
|
+
click.echo(FeedbackManager.info(message="Removing Tinybird local..."))
|
|
152
171
|
docker_client = get_docker_client()
|
|
153
172
|
remove_tinybird_local(docker_client)
|
|
154
|
-
click.echo(FeedbackManager.success(message="Tinybird
|
|
173
|
+
click.echo(FeedbackManager.success(message="Tinybird local removed"))
|
|
155
174
|
|
|
156
175
|
|
|
157
176
|
@local.command()
|
|
158
177
|
@coro
|
|
159
178
|
async def start() -> None:
|
|
160
|
-
"""Start Tinybird
|
|
161
|
-
click.echo(FeedbackManager.info(message="Starting Tinybird
|
|
179
|
+
"""Start Tinybird local"""
|
|
180
|
+
click.echo(FeedbackManager.info(message="Starting Tinybird local..."))
|
|
162
181
|
docker_client = get_docker_client()
|
|
163
182
|
start_tinybird_local(docker_client)
|
|
164
|
-
click.echo(FeedbackManager.success(message="Tinybird
|
|
183
|
+
click.echo(FeedbackManager.success(message="Tinybird local started"))
|
|
165
184
|
|
|
166
185
|
|
|
167
186
|
@local.command()
|
|
168
187
|
@coro
|
|
169
188
|
async def restart() -> None:
|
|
170
|
-
"""Restart Tinybird
|
|
171
|
-
click.echo(FeedbackManager.info(message="Restarting Tinybird
|
|
189
|
+
"""Restart Tinybird local"""
|
|
190
|
+
click.echo(FeedbackManager.info(message="Restarting Tinybird local..."))
|
|
172
191
|
docker_client = get_docker_client()
|
|
173
192
|
remove_tinybird_local(docker_client)
|
|
174
193
|
start_tinybird_local(docker_client)
|
|
175
|
-
click.echo(FeedbackManager.success(message="Tinybird
|
|
194
|
+
click.echo(FeedbackManager.success(message="Tinybird local restarted"))
|
tinybird/tb/modules/mock.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import os
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
|
|
4
4
|
import click
|
|
@@ -7,6 +7,7 @@ from tinybird.feedback_manager import FeedbackManager
|
|
|
7
7
|
from tinybird.tb.modules.cli import cli
|
|
8
8
|
from tinybird.tb.modules.common import CLIException, coro
|
|
9
9
|
from tinybird.tb.modules.config import CLIConfig
|
|
10
|
+
from tinybird.tb.modules.datafile.fixture import build_fixture_name, persist_fixture
|
|
10
11
|
from tinybird.tb.modules.llm import LLM
|
|
11
12
|
from tinybird.tb.modules.local import get_tinybird_local_client
|
|
12
13
|
|
|
@@ -15,15 +16,15 @@ from tinybird.tb.modules.local import get_tinybird_local_client
|
|
|
15
16
|
@click.argument("datasource", type=str)
|
|
16
17
|
@click.option("--rows", type=int, default=10, help="Number of events to send")
|
|
17
18
|
@click.option("--context", type=str, default="", help="Extra context to use for data generation")
|
|
19
|
+
@click.option("--folder", type=str, default=".", help="Folder where datafiles will be placed")
|
|
18
20
|
@coro
|
|
19
|
-
async def mock(datasource: str, rows: int, context: str) -> None:
|
|
20
|
-
"""Load sample data into a
|
|
21
|
+
async def mock(datasource: str, rows: int, context: str, folder: str) -> None:
|
|
22
|
+
"""Load sample data into a Data Source.
|
|
21
23
|
|
|
22
24
|
Args:
|
|
23
25
|
ctx: Click context object
|
|
24
26
|
datasource_file: Path to the datasource file to load sample data into
|
|
25
27
|
"""
|
|
26
|
-
import llm
|
|
27
28
|
|
|
28
29
|
try:
|
|
29
30
|
datasource_path = Path(datasource)
|
|
@@ -33,21 +34,19 @@ async def mock(datasource: str, rows: int, context: str) -> None:
|
|
|
33
34
|
else:
|
|
34
35
|
datasource_path = Path("datasources", f"{datasource}.datasource")
|
|
35
36
|
|
|
37
|
+
datasource_path = Path(folder) / datasource_path
|
|
38
|
+
|
|
39
|
+
click.echo(FeedbackManager.gray(message=f"Creating fixture for {datasource_name}..."))
|
|
36
40
|
datasource_content = datasource_path.read_text()
|
|
37
41
|
llm_config = CLIConfig.get_llm_config()
|
|
38
42
|
llm = LLM(key=llm_config["api_key"])
|
|
39
|
-
tb_client = get_tinybird_local_client()
|
|
40
|
-
sql = await llm.generate_sql_sample_data(tb_client, datasource_content, rows, context)
|
|
43
|
+
tb_client = await get_tinybird_local_client(os.path.abspath(folder))
|
|
44
|
+
sql = await llm.generate_sql_sample_data(tb_client, datasource_content, row_count=rows, context=context)
|
|
41
45
|
result = await tb_client.query(f"{sql} FORMAT JSON")
|
|
42
|
-
data = result.get("data", [])
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
batch = data[i : i + max_rows_per_request]
|
|
47
|
-
ndjson_data = "\n".join([json.dumps(row) for row in batch])
|
|
48
|
-
await tb_client.datasource_events(datasource_name, ndjson_data)
|
|
49
|
-
sent_rows += len(batch)
|
|
50
|
-
click.echo(f"Sent {sent_rows} events to datasource '{datasource_name}'")
|
|
46
|
+
data = result.get("data", [])[:rows]
|
|
47
|
+
fixture_name = build_fixture_name(datasource_path.absolute(), datasource_name, datasource_content)
|
|
48
|
+
persist_fixture(fixture_name, data)
|
|
49
|
+
click.echo(FeedbackManager.success(message="✓ Done!"))
|
|
51
50
|
|
|
52
51
|
except Exception as e:
|
|
53
|
-
raise CLIException(FeedbackManager.
|
|
52
|
+
raise CLIException(FeedbackManager.error(message=str(e)))
|
tinybird/tb/modules/prompts.py
CHANGED
|
@@ -43,10 +43,36 @@ SQL >
|
|
|
43
43
|
- The datasource will be the landing table for the data.
|
|
44
44
|
- Create multiple pipes to show different use cases over the same datasource.
|
|
45
45
|
- The SQL query must be a valid ClickHouse SQL query that mixes ClickHouse syntax and Tinybird templating syntax.
|
|
46
|
-
- If you
|
|
46
|
+
- If you use dynamic parameters you MUST start ALWAYS the whole sql query with "%" symbol on top. e.g: SQL >\n %\n SELECT * FROM <table> WHERE <condition> LIMIT 10
|
|
47
47
|
- The Parameter functions like this one {{String(my_param_name,default_value)}} can be one of the following: String, DateTime, Date, Float32, Float64, Int, Integer, UInt8, UInt16, UInt32, UInt64, UInt128, UInt256, Int8, Int16, Int32, Int64, Int128, Int256
|
|
48
48
|
- Parameter names must be different from column names. Pass always the param name and a default value to the function.
|
|
49
|
+
- Code inside the template {{code}} is python code but no module is allowed to be imported. So for example you can't use now() as default value for a DateTime parameter. You need an if else block like this:
|
|
50
|
+
```
|
|
51
|
+
(...)
|
|
52
|
+
AND timestamp BETWEEN {{DateTime(start_date, now() - interval 30 day)}} AND {{DateTime(end_date, now())}} --this is not valid
|
|
53
|
+
|
|
54
|
+
{%if not defined(start_date)%}
|
|
55
|
+
timestamp BETWEEN now() - interval 30 day
|
|
56
|
+
{%else%}
|
|
57
|
+
timestamp BETWEEN {{DateTime(start_date)}}
|
|
58
|
+
{%end%}
|
|
59
|
+
{%if not defined(end_date)%}
|
|
60
|
+
AND now()
|
|
61
|
+
{%else%}
|
|
62
|
+
AND {{DateTime(end_date)}}
|
|
63
|
+
{%end%} --this is valid
|
|
64
|
+
```
|
|
49
65
|
- Nodes can't have the same exact name as the Pipe they belong to.
|
|
66
|
+
- Endpoints can export Prometehus format, Node sql must have name two columns:
|
|
67
|
+
name (String): The name of the metric
|
|
68
|
+
value (Number): The numeric value for the metric.
|
|
69
|
+
and then some optional columns:
|
|
70
|
+
help (String): A description of the metric.
|
|
71
|
+
timestamp (Number): A Unix timestamp for the metric.
|
|
72
|
+
type (String): Defines the metric type (counter, gauge, histogram, summary, untyped, or empty).
|
|
73
|
+
labels (Map(String, String)): A set of key-value pairs providing metric dimensions.
|
|
74
|
+
- Use prometheus format when you are asked to monitor something
|
|
75
|
+
- Nodes do NOT use the same name as the Pipe they belong to. So if the pipe name is "my_pipe", the nodes must be named "my_pipe_node_1", "my_pipe_node_2", etc.
|
|
50
76
|
</instructions>
|
|
51
77
|
"""
|
|
52
78
|
|
|
@@ -122,7 +148,7 @@ FROM numbers({row_count})
|
|
|
122
148
|
|
|
123
149
|
- The query MUST return a random sample of data that matches the schema.
|
|
124
150
|
- The query MUST return a valid clickhouse sql query.
|
|
125
|
-
- The query MUST return a sample of {row_count} rows.
|
|
151
|
+
- The query MUST return a sample of EXACTLY {row_count} rows.
|
|
126
152
|
- The query MUST be valid for clickhouse and Tinybird.
|
|
127
153
|
- Return JUST the sql query, without any other text or symbols.
|
|
128
154
|
- Do NOT include ```clickhouse or ```sql or any other wrapping text.
|
|
@@ -5,7 +5,7 @@ tinybird/config.py,sha256=Z-BX9FrjgsLw1YwcCdF0IztLB97Zpc70VVPplO_pDSY,6089
|
|
|
5
5
|
tinybird/connectors.py,sha256=lkpVSUmSuViEZBa4QjTK7YmPHUop0a5UFoTrSmlVq6k,15244
|
|
6
6
|
tinybird/context.py,sha256=kutUQ0kCwparowI74_YLXx6wtTzGLRouJ6oGHVBPzBo,1291
|
|
7
7
|
tinybird/datatypes.py,sha256=IHyhZ86ib54Vnd1pbod9y2aS8DDvDKZm1HJGlThdbuQ,10460
|
|
8
|
-
tinybird/feedback_manager.py,sha256
|
|
8
|
+
tinybird/feedback_manager.py,sha256=-nDkg13DoiNk40AztzSJdbldbKhfuTsCZcSOviK9sik,67790
|
|
9
9
|
tinybird/git_settings.py,sha256=XUL9ZUj59-ZVQJDYmMEq4UpnuuOuQOHGlNcX3JgQHjQ,3954
|
|
10
10
|
tinybird/sql.py,sha256=gfRKjdqEygcE1WOTeQ1QV2Jal8Jzl4RSX8fftu1KSEs,45825
|
|
11
11
|
tinybird/sql_template.py,sha256=IqYRfUxDYBCoOYjqqvn--_8QXLv9FSRnJ0bInx7q1Xs,93051
|
|
@@ -18,22 +18,22 @@ tinybird/ch_utils/engine.py,sha256=OXkBhlzGjZotjD0vaT-rFIbSGV4tpiHxE8qO_ip0SyQ,4
|
|
|
18
18
|
tinybird/tb/cli.py,sha256=6Lu3wsCNepAxjJCWy4c6RhVPArBtm8TlUcSxX--TsBo,783
|
|
19
19
|
tinybird/tb/modules/auth.py,sha256=hynZ-Temot8YBsySUWKSFzZlYadtFPxG3o6lCSu1n6E,9018
|
|
20
20
|
tinybird/tb/modules/branch.py,sha256=R1tTUBGyI0p_dt2IAWbuyNOvemhjCIPwYxEmOxL3zOg,38468
|
|
21
|
-
tinybird/tb/modules/build.py,sha256=
|
|
22
|
-
tinybird/tb/modules/cicd.py,sha256=
|
|
23
|
-
tinybird/tb/modules/cli.py,sha256=
|
|
24
|
-
tinybird/tb/modules/common.py,sha256=
|
|
21
|
+
tinybird/tb/modules/build.py,sha256=qjK2hmQ4es1Et4mbG9ijr_ziAjxWex0G_U0pYqH-hd8,11606
|
|
22
|
+
tinybird/tb/modules/cicd.py,sha256=KCFfywFfvGRh24GZwqrhICiTK_arHelPs_X4EB-pXIw,7331
|
|
23
|
+
tinybird/tb/modules/cli.py,sha256=c-XNRu-idb2Hz43IT9ejd-QjsZy-xPQ3rnrdVIz0wxM,56568
|
|
24
|
+
tinybird/tb/modules/common.py,sha256=Vubc2AIR8BfEupnT5e1Y8OYGEyvNoIcjo8th-SaUflw,80111
|
|
25
25
|
tinybird/tb/modules/config.py,sha256=ppWvACHrSLkb5hOoQLYNby2w8jR76-8Kx2NBCst7ntQ,11760
|
|
26
26
|
tinybird/tb/modules/connection.py,sha256=ZSqBGoRiJedjHKEyB_fr1ybucOHtaad8d7uqGa2Q92M,28668
|
|
27
|
-
tinybird/tb/modules/create.py,sha256=
|
|
27
|
+
tinybird/tb/modules/create.py,sha256=Ky5LOyDJLgaHyWDt8un100QxKgNiQpEal-QzIW0V85I,6590
|
|
28
28
|
tinybird/tb/modules/datasource.py,sha256=tjcf5o-HYIdTkb_c1ErGUFIE-W6G992vsvCuDGcxb9Q,35818
|
|
29
29
|
tinybird/tb/modules/exceptions.py,sha256=4A2sSjCEqKUMqpP3WI00zouCWW4uLaghXXLZBSw04mY,3363
|
|
30
30
|
tinybird/tb/modules/fmt.py,sha256=UszEQO15fdzQ49QEj7Unhu68IKwSuKPsOrKhk2p2TAg,3547
|
|
31
31
|
tinybird/tb/modules/job.py,sha256=eoBVyA24lYIPonU88Jn7FF9hBKz1kScy9_w_oWreuc4,2952
|
|
32
|
-
tinybird/tb/modules/llm.py,sha256=
|
|
33
|
-
tinybird/tb/modules/local.py,sha256=
|
|
34
|
-
tinybird/tb/modules/mock.py,sha256=
|
|
32
|
+
tinybird/tb/modules/llm.py,sha256=TvJJ9BlKISAb1SVI-pnHp_PcHcxGfTyjxOE_qAz90Ck,2441
|
|
33
|
+
tinybird/tb/modules/local.py,sha256=9YBFLSVmn8AYoYeXoLwWrvEF1e6vQ7_ZJhZ35cGeeWY,6637
|
|
34
|
+
tinybird/tb/modules/mock.py,sha256=kpKN0J6jZSmQ4F_PYY93IuO1qfwhRIxCNlP6XnLyiDg,2224
|
|
35
35
|
tinybird/tb/modules/pipe.py,sha256=9wnfKbp2FkmLiJgVk3qbra76ktwsUTXghu6j9cCEahQ,31058
|
|
36
|
-
tinybird/tb/modules/prompts.py,sha256=
|
|
36
|
+
tinybird/tb/modules/prompts.py,sha256=g0cBW2ePzuftib02wV82VIcAZd59buAAusnirAbzqVE,8662
|
|
37
37
|
tinybird/tb/modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
|
|
38
38
|
tinybird/tb/modules/table.py,sha256=hG-PRDVuFp2uph41WpoLRV1yjp3RI2fi_iGGiI0rdxU,7695
|
|
39
39
|
tinybird/tb/modules/tag.py,sha256=1qQWyk1p3Btv3LzM8VbJG-k7x2-pFuAlYCg3QL6QewI,3480
|
|
@@ -42,13 +42,14 @@ tinybird/tb/modules/test.py,sha256=psINFpSYT1eGgy32-_4q6CJ7LOcdwBpAfasMA0_tNOU,4
|
|
|
42
42
|
tinybird/tb/modules/token.py,sha256=r0oeG1RpOOzHtqbUaHBiOmhE55HfNIvReAAWyKl9fJg,12695
|
|
43
43
|
tinybird/tb/modules/workspace.py,sha256=FVlh-kbiZp5Gvp6dGFxi0UD8ail77rMamXLhqdVwrZ0,10916
|
|
44
44
|
tinybird/tb/modules/workspace_members.py,sha256=08W0onEYkKLEC5TkAI07cxN9XSquEm7HnL7OkHAVDjo,8715
|
|
45
|
-
tinybird/tb/modules/datafile/build.py,sha256=
|
|
45
|
+
tinybird/tb/modules/datafile/build.py,sha256=bo5T-_9LWsw4dZoHDO2bgn4hpSNOK5u_RiiYlRGLToA,91948
|
|
46
46
|
tinybird/tb/modules/datafile/build_common.py,sha256=74547h5ja4C66DAwDMabj75FA_BUTJxTJv-24tSFmrs,4551
|
|
47
47
|
tinybird/tb/modules/datafile/build_datasource.py,sha256=fquzEGwk9NL_0K5YYG86Xtvgn4J5YHtRUoKJxbQGO0s,17344
|
|
48
|
-
tinybird/tb/modules/datafile/build_pipe.py,sha256=
|
|
49
|
-
tinybird/tb/modules/datafile/common.py,sha256=
|
|
48
|
+
tinybird/tb/modules/datafile/build_pipe.py,sha256=wxqvVY3vIlG2_IAX8__mevhxqGkOxQ4-YyoWE6v2OxE,27465
|
|
49
|
+
tinybird/tb/modules/datafile/common.py,sha256=q0XPpNE-l011Um3TXh3BmkSkUlYP5Ydkn24jXLq1I9Y,34239
|
|
50
50
|
tinybird/tb/modules/datafile/diff.py,sha256=ZaTPGjRFJWokhaad_rMSxfYT92PA96s4WhhvlZubgyA,6769
|
|
51
51
|
tinybird/tb/modules/datafile/exceptions.py,sha256=8rw2umdZjtby85QbuRKFO5ETz_eRHwUY5l7eHsy1wnI,556
|
|
52
|
+
tinybird/tb/modules/datafile/fixture.py,sha256=YHlL4tojmPwm343Y8KO6r7d5Bhsk7U3lKP-oLMeBMsY,1771
|
|
52
53
|
tinybird/tb/modules/datafile/format_common.py,sha256=zNWDXvwSKC9_T5e9R92LLj9ekDflVWwsllhGQilZsnY,2184
|
|
53
54
|
tinybird/tb/modules/datafile/format_datasource.py,sha256=tsnCjONISvhFuucKNbIHkT__UmlUbcswx5mwI9hiDQc,6216
|
|
54
55
|
tinybird/tb/modules/datafile/format_pipe.py,sha256=R5tnlEccLn3KX6ehtC_H2sGQNrthuJUiVSN9z_-KGCY,7474
|
|
@@ -64,8 +65,8 @@ tinybird/tb_cli_modules/config.py,sha256=6NTgIdwf0X132A1j6G_YrdPep87ymZ9b5pABabK
|
|
|
64
65
|
tinybird/tb_cli_modules/exceptions.py,sha256=pmucP4kTF4irIt7dXiG-FcnI-o3mvDusPmch1L8RCWk,3367
|
|
65
66
|
tinybird/tb_cli_modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
|
|
66
67
|
tinybird/tb_cli_modules/telemetry.py,sha256=iEGnMuCuNhvF6ln__j6X9MSTwL_0Hm-GgFHHHvhfknk,10466
|
|
67
|
-
tinybird-0.0.1.
|
|
68
|
-
tinybird-0.0.1.
|
|
69
|
-
tinybird-0.0.1.
|
|
70
|
-
tinybird-0.0.1.
|
|
71
|
-
tinybird-0.0.1.
|
|
68
|
+
tinybird-0.0.1.dev10.dist-info/METADATA,sha256=HP7ty3Kt_uj6hpn_ooK40yAnUMOq3OvKXzhiPHNMGnI,2405
|
|
69
|
+
tinybird-0.0.1.dev10.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
|
|
70
|
+
tinybird-0.0.1.dev10.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
|
|
71
|
+
tinybird-0.0.1.dev10.dist-info/top_level.txt,sha256=pgw6AzERHBcW3YTi2PW4arjxLkulk2msOz_SomfOEuc,45
|
|
72
|
+
tinybird-0.0.1.dev10.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|