tinybird 0.0.1.dev9__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 -44
- 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 +64 -102
- 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.dev9.dist-info → tinybird-0.0.1.dev10.dist-info}/METADATA +1 -1
- {tinybird-0.0.1.dev9.dist-info → tinybird-0.0.1.dev10.dist-info}/RECORD +19 -18
- {tinybird-0.0.1.dev9.dist-info → tinybird-0.0.1.dev10.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev9.dist-info → tinybird-0.0.1.dev10.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev9.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,7 +1,9 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import cmd
|
|
2
3
|
import os
|
|
3
4
|
import random
|
|
4
5
|
import subprocess
|
|
6
|
+
import sys
|
|
5
7
|
import threading
|
|
6
8
|
import time
|
|
7
9
|
from pathlib import Path
|
|
@@ -16,38 +18,80 @@ import tinybird.context as context
|
|
|
16
18
|
from tinybird.client import TinyB
|
|
17
19
|
from tinybird.config import FeatureFlags
|
|
18
20
|
from tinybird.feedback_manager import FeedbackManager, bcolors
|
|
19
|
-
from tinybird.syncasync import sync_to_async
|
|
20
21
|
from tinybird.tb.modules.cli import cli
|
|
21
|
-
from tinybird.tb.modules.common import
|
|
22
|
-
coro,
|
|
23
|
-
)
|
|
22
|
+
from tinybird.tb.modules.common import coro, push_data
|
|
24
23
|
from tinybird.tb.modules.datafile.build import folder_build
|
|
25
|
-
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
|
|
26
25
|
from tinybird.tb.modules.datafile.exceptions import ParseException
|
|
26
|
+
from tinybird.tb.modules.datafile.fixture import build_fixture_name, get_fixture_dir
|
|
27
27
|
from tinybird.tb.modules.datafile.parse_datasource import parse_datasource
|
|
28
28
|
from tinybird.tb.modules.datafile.parse_pipe import parse_pipe
|
|
29
29
|
from tinybird.tb.modules.local import get_tinybird_local_client
|
|
30
30
|
from tinybird.tb.modules.table import format_table
|
|
31
31
|
|
|
32
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
|
+
|
|
33
69
|
class FileChangeHandler(FileSystemEventHandler):
|
|
34
70
|
def __init__(self, filenames: List[str], process: Callable[[List[str]], None]):
|
|
35
71
|
self.filenames = filenames
|
|
36
72
|
self.process = process
|
|
37
73
|
|
|
38
74
|
def on_modified(self, event: Any) -> None:
|
|
39
|
-
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"]):
|
|
40
76
|
filename = event.src_path.split("/")[-1]
|
|
41
|
-
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"))
|
|
42
78
|
try:
|
|
43
79
|
self.process([event.src_path])
|
|
44
80
|
except Exception as e:
|
|
45
81
|
click.echo(FeedbackManager.error_exception(error=e))
|
|
46
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
|
+
|
|
47
90
|
|
|
48
91
|
def watch_files(
|
|
49
92
|
filenames: List[str],
|
|
50
93
|
process: Union[Callable[[List[str]], None], Callable[[List[str]], Awaitable[None]]],
|
|
94
|
+
shell: BuildShell,
|
|
51
95
|
) -> None:
|
|
52
96
|
# Handle both sync and async process functions
|
|
53
97
|
async def process_wrapper(files: List[str]) -> None:
|
|
@@ -61,8 +105,9 @@ def watch_files(
|
|
|
61
105
|
elapsed_time = time_end - time_start
|
|
62
106
|
click.echo(
|
|
63
107
|
FeedbackManager.success(message="\n✓ ")
|
|
64
|
-
+ FeedbackManager.gray(message=f"Rebuild completed in {elapsed_time:.1f}s
|
|
108
|
+
+ FeedbackManager.gray(message=f"Rebuild completed in {elapsed_time:.1f}s")
|
|
65
109
|
)
|
|
110
|
+
shell.reprint_prompt()
|
|
66
111
|
|
|
67
112
|
event_handler = FileChangeHandler(filenames, lambda f: asyncio.run(process_wrapper(f)))
|
|
68
113
|
observer = Observer()
|
|
@@ -113,7 +158,9 @@ async def build(
|
|
|
113
158
|
ignore_sql_errors = FeatureFlags.ignore_sql_errors()
|
|
114
159
|
context.disable_template_security_validation.set(True)
|
|
115
160
|
is_internal = has_internal_datafiles(folder)
|
|
116
|
-
|
|
161
|
+
|
|
162
|
+
folder_path = os.path.abspath(folder)
|
|
163
|
+
tb_client = await get_tinybird_local_client(folder_path)
|
|
117
164
|
|
|
118
165
|
def check_filenames(filenames: List[str]):
|
|
119
166
|
parser_matrix = {".pipe": parse_pipe, ".datasource": parse_datasource}
|
|
@@ -134,29 +181,53 @@ async def build(
|
|
|
134
181
|
parser(filename)
|
|
135
182
|
|
|
136
183
|
async def process(filenames: List[str], watch: bool = False, only_pipes: bool = False):
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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)
|
|
145
202
|
|
|
146
203
|
if watch:
|
|
147
|
-
filename
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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)
|
|
212
|
+
|
|
213
|
+
datafiles = get_project_filenames(folder)
|
|
214
|
+
fixtures = get_project_fixtures(folder)
|
|
215
|
+
filenames = datafiles + fixtures
|
|
152
216
|
|
|
153
217
|
async def build_once(filenames: List[str]):
|
|
154
218
|
try:
|
|
155
|
-
click.echo("⚡ Building project
|
|
219
|
+
click.echo("⚡ Building project...\n")
|
|
156
220
|
time_start = time.time()
|
|
157
221
|
await process(filenames=filenames, watch=False, only_pipes=skip_datasources)
|
|
158
222
|
time_end = time.time()
|
|
159
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)
|
|
160
231
|
click.echo(FeedbackManager.success(message=f"\n✓ Build completed in {elapsed_time:.1f}s\n"))
|
|
161
232
|
except Exception as e:
|
|
162
233
|
click.echo(FeedbackManager.error(message=str(e)))
|
|
@@ -164,31 +235,21 @@ async def build(
|
|
|
164
235
|
await build_once(filenames)
|
|
165
236
|
|
|
166
237
|
if watch:
|
|
167
|
-
|
|
168
|
-
|
|
238
|
+
shell = BuildShell(folder=folder)
|
|
239
|
+
click.echo(FeedbackManager.highlight(message="◎ Watching for changes..."))
|
|
240
|
+
watcher_thread = threading.Thread(target=watch_files, args=(filenames, process, shell), daemon=True)
|
|
169
241
|
watcher_thread.start()
|
|
242
|
+
shell.cmdloop()
|
|
170
243
|
|
|
171
|
-
# Main CLI loop
|
|
172
|
-
while True:
|
|
173
|
-
user_input = click.prompt("", prompt_suffix="")
|
|
174
|
-
if user_input.lower() == "exit":
|
|
175
|
-
break
|
|
176
244
|
|
|
177
|
-
|
|
178
|
-
click.echo(FeedbackManager.error(message="Build command is already running"))
|
|
179
|
-
else:
|
|
180
|
-
# Process the user command
|
|
181
|
-
await sync_to_async(subprocess.run, thread_sensitive=True)(user_input, shell=True, text=True)
|
|
182
|
-
|
|
183
|
-
click.echo(FeedbackManager.highlight(message="\n◎ Watching for changes...\n"))
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
async def build_and_print_pipe(tb_client: TinyB, filename: str):
|
|
245
|
+
async def build_and_print_resource(tb_client: TinyB, filename: str):
|
|
187
246
|
rebuild_colors = [bcolors.FAIL, bcolors.OKBLUE, bcolors.WARNING, bcolors.OKGREEN, bcolors.HEADER]
|
|
188
247
|
rebuild_index = random.randint(0, len(rebuild_colors) - 1)
|
|
189
248
|
rebuild_color = rebuild_colors[rebuild_index % len(rebuild_colors)]
|
|
190
|
-
|
|
191
|
-
|
|
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)
|
|
192
253
|
data = []
|
|
193
254
|
limit = 5
|
|
194
255
|
for d in res["data"][:5]:
|
|
@@ -197,7 +258,6 @@ async def build_and_print_pipe(tb_client: TinyB, filename: str):
|
|
|
197
258
|
row_count = res.get("rows", 0)
|
|
198
259
|
stats = res.get("statistics", {})
|
|
199
260
|
elapsed = stats.get("elapsed", 0)
|
|
200
|
-
node_name = "endpoint"
|
|
201
261
|
cols = len(meta)
|
|
202
262
|
try:
|
|
203
263
|
|
|
@@ -207,7 +267,7 @@ async def build_and_print_pipe(tb_client: TinyB, filename: str):
|
|
|
207
267
|
table = format_table(data, meta)
|
|
208
268
|
colored_char = print_message("│", rebuild_color)
|
|
209
269
|
table_with_marker = "\n".join(f"{colored_char} {line}" for line in table.split("\n"))
|
|
210
|
-
click.echo(f"\n{colored_char} {print_message('⚡', rebuild_color)} Running {
|
|
270
|
+
click.echo(f"\n{colored_char} {print_message('⚡', rebuild_color)} Running {name}")
|
|
211
271
|
click.echo(colored_char)
|
|
212
272
|
click.echo(table_with_marker)
|
|
213
273
|
click.echo(colored_char)
|
|
@@ -225,3 +285,24 @@ async def build_and_print_pipe(tb_client: TinyB, filename: str):
|
|
|
225
285
|
click.echo("------------")
|
|
226
286
|
else:
|
|
227
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,5 +1,5 @@
|
|
|
1
|
-
import json
|
|
2
1
|
import os
|
|
2
|
+
import subprocess
|
|
3
3
|
from os import getcwd
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Optional
|
|
@@ -11,14 +11,12 @@ from tinybird.client import TinyB
|
|
|
11
11
|
from tinybird.feedback_manager import FeedbackManager
|
|
12
12
|
from tinybird.tb.modules.cicd import init_cicd
|
|
13
13
|
from tinybird.tb.modules.cli import cli
|
|
14
|
-
from tinybird.tb.modules.common import _generate_datafile, coro, generate_datafile
|
|
14
|
+
from tinybird.tb.modules.common import _generate_datafile, coro, generate_datafile
|
|
15
15
|
from tinybird.tb.modules.config import CLIConfig
|
|
16
|
-
from tinybird.tb.modules.datafile.
|
|
17
|
-
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
|
|
18
18
|
from tinybird.tb.modules.llm import LLM
|
|
19
|
-
from tinybird.tb.modules.local import
|
|
20
|
-
get_tinybird_local_client,
|
|
21
|
-
)
|
|
19
|
+
from tinybird.tb.modules.local import get_tinybird_local_client
|
|
22
20
|
|
|
23
21
|
|
|
24
22
|
@cli.command()
|
|
@@ -40,7 +38,7 @@ from tinybird.tb.modules.local import (
|
|
|
40
38
|
type=click.Path(exists=True, file_okay=False),
|
|
41
39
|
help="Folder where datafiles will be placed",
|
|
42
40
|
)
|
|
43
|
-
@click.option("--rows", type=int, default=
|
|
41
|
+
@click.option("--rows", type=int, default=10, help="Number of events to send")
|
|
44
42
|
@click.pass_context
|
|
45
43
|
@coro
|
|
46
44
|
async def create(
|
|
@@ -53,17 +51,25 @@ async def create(
|
|
|
53
51
|
"""Initialize a new project."""
|
|
54
52
|
folder = folder or getcwd()
|
|
55
53
|
try:
|
|
56
|
-
tb_client = get_tinybird_local_client()
|
|
54
|
+
tb_client = await get_tinybird_local_client(os.path.abspath(folder))
|
|
57
55
|
click.echo(FeedbackManager.gray(message="Creating new project structure..."))
|
|
58
56
|
await project_create(tb_client, data, prompt, folder)
|
|
59
57
|
click.echo(FeedbackManager.success(message="✓ Scaffolding completed!\n"))
|
|
60
|
-
await folder_build(tb_client, folder=folder)
|
|
61
58
|
|
|
59
|
+
click.echo(FeedbackManager.gray(message="\nCreating CI/CD files for GitHub and GitLab..."))
|
|
60
|
+
init_git(folder)
|
|
62
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..."))
|
|
63
65
|
|
|
64
66
|
if data:
|
|
65
67
|
ds_name = os.path.basename(data.split(".")[0])
|
|
66
|
-
|
|
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)
|
|
67
73
|
elif prompt:
|
|
68
74
|
datasource_files = [f for f in os.listdir(Path(folder) / "datasources") if f.endswith(".datasource")]
|
|
69
75
|
for datasource_file in datasource_files:
|
|
@@ -77,15 +83,13 @@ async def create(
|
|
|
77
83
|
sql = await llm.generate_sql_sample_data(tb_client, datasource_name, datasource_content, rows)
|
|
78
84
|
result = await tb_client.query(f"{sql} FORMAT JSON")
|
|
79
85
|
data = result.get("data", [])
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
click.echo(f"Sent {sent_rows} rows to datasource '{datasource_name}'")
|
|
88
|
-
click.echo(FeedbackManager.success(message="\n✓ Tinybird Local is ready!"))
|
|
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!"))
|
|
89
93
|
except Exception as e:
|
|
90
94
|
click.echo(FeedbackManager.error(message=f"Error: {str(e)}"))
|
|
91
95
|
|
|
@@ -96,31 +100,23 @@ async def project_create(
|
|
|
96
100
|
prompt: Optional[str],
|
|
97
101
|
folder: str,
|
|
98
102
|
):
|
|
99
|
-
project_paths = ["datasources", "endpoints", "materializations", "copies", "sinks"]
|
|
103
|
+
project_paths = ["datasources", "endpoints", "materializations", "copies", "sinks", "fixtures"]
|
|
100
104
|
force = True
|
|
101
105
|
for x in project_paths:
|
|
102
106
|
try:
|
|
103
107
|
f = Path(folder) / x
|
|
104
108
|
f.mkdir()
|
|
105
|
-
click.echo(FeedbackManager.info_path_created(path=x))
|
|
106
109
|
except FileExistsError:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def generate_pipe_file(name: str, content: str, parent_dir: Optional[str] = None):
|
|
110
|
-
base = Path("endpoints")
|
|
111
|
-
if parent_dir:
|
|
112
|
-
base = Path(parent_dir) / base
|
|
113
|
-
if not base.exists():
|
|
114
|
-
base = Path()
|
|
115
|
-
f = base / (f"{name}.pipe")
|
|
116
|
-
with open(f"{f}", "w") as file:
|
|
117
|
-
file.write(content)
|
|
118
|
-
click.echo(FeedbackManager.info_file_created(file=f))
|
|
110
|
+
pass
|
|
111
|
+
click.echo(FeedbackManager.info_path_created(path=x))
|
|
119
112
|
|
|
120
113
|
if data:
|
|
121
114
|
path = Path(folder) / data
|
|
122
115
|
format = path.suffix.lstrip(".")
|
|
123
|
-
|
|
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)}"))
|
|
124
120
|
name = data.split(".")[0]
|
|
125
121
|
generate_pipe_file(
|
|
126
122
|
f"{name}_endpoint",
|
|
@@ -130,6 +126,7 @@ SQL >
|
|
|
130
126
|
SELECT * from {name}
|
|
131
127
|
TYPE ENDPOINT
|
|
132
128
|
""",
|
|
129
|
+
folder,
|
|
133
130
|
)
|
|
134
131
|
elif prompt:
|
|
135
132
|
try:
|
|
@@ -138,77 +135,42 @@ TYPE ENDPOINT
|
|
|
138
135
|
result = await llm.create_project(prompt)
|
|
139
136
|
for ds in result.datasources:
|
|
140
137
|
content = ds.content.replace("```", "")
|
|
141
|
-
generate_datafile(
|
|
138
|
+
generate_datafile(
|
|
139
|
+
content, filename=f"{ds.name}.datasource", data=None, _format="ndjson", force=force, folder=folder
|
|
140
|
+
)
|
|
142
141
|
|
|
143
142
|
for pipe in result.pipes:
|
|
144
143
|
content = pipe.content.replace("```", "")
|
|
145
|
-
generate_pipe_file(pipe.name, content)
|
|
144
|
+
generate_pipe_file(pipe.name, content, folder)
|
|
146
145
|
except Exception as e:
|
|
147
146
|
click.echo(FeedbackManager.error(message=f"Error: {str(e)}"))
|
|
148
|
-
else:
|
|
149
|
-
events_ds = """
|
|
150
|
-
SCHEMA >
|
|
151
|
-
`age` Int16 `json:$.age`,
|
|
152
|
-
`airline` String `json:$.airline`,
|
|
153
|
-
`email` String `json:$.email`,
|
|
154
|
-
`extra_bags` Int16 `json:$.extra_bags`,
|
|
155
|
-
`flight_from` String `json:$.flight_from`,
|
|
156
|
-
`flight_to` String `json:$.flight_to`,
|
|
157
|
-
`meal_choice` String `json:$.meal_choice`,
|
|
158
|
-
`name` String `json:$.name`,
|
|
159
|
-
`passport_number` Int32 `json:$.passport_number`,
|
|
160
|
-
`priority_boarding` UInt8 `json:$.priority_boarding`,
|
|
161
|
-
`timestamp` DateTime `json:$.timestamp`,
|
|
162
|
-
`transaction_id` String `json:$.transaction_id`
|
|
163
|
-
|
|
164
|
-
ENGINE "MergeTree"
|
|
165
|
-
ENGINE_PARTITION_KEY "toYear(timestamp)"
|
|
166
|
-
ENGINE_SORTING_KEY "airline, timestamp"
|
|
167
|
-
"""
|
|
168
|
-
top_airlines = """
|
|
169
|
-
NODE endpoint
|
|
170
|
-
SQL >
|
|
171
|
-
SELECT airline, count() as bookings FROM events
|
|
172
|
-
GROUP BY airline
|
|
173
|
-
ORDER BY bookings DESC
|
|
174
|
-
LIMIT 5
|
|
175
|
-
TYPE ENDPOINT
|
|
176
|
-
"""
|
|
177
|
-
generate_datafile(
|
|
178
|
-
events_ds, filename="events.datasource", data=None, _format="ndjson", force=force, parent_dir=folder
|
|
179
|
-
)
|
|
180
|
-
generate_pipe_file("top_airlines", top_airlines, parent_dir=folder)
|
|
181
147
|
|
|
182
148
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
mode="append",
|
|
212
|
-
ignore_empty=ignore_empty,
|
|
213
|
-
concurrency=concurrency,
|
|
214
|
-
)
|
|
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
|