tinybird 0.0.1.dev9__py3-none-any.whl → 0.0.1.dev11__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.

@@ -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("** File {file_path} generated for CI/CD")
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 ...")
@@ -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,29 +18,71 @@ 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"))
78
+ try:
79
+ self.process([event.src_path])
80
+ except Exception as e:
81
+ click.echo(FeedbackManager.error_exception(error=e))
82
+
83
+ def on_created(self, event: Any) -> None:
84
+ if not event.is_directory and any(event.src_path.endswith(ext) for ext in [".datasource", ".pipe", ".ndjson"]):
85
+ click.echo(FeedbackManager.highlight(message=f"\n\n⟲ Changes detected in {event.src_path}\n"))
42
86
  try:
43
87
  self.process([event.src_path])
44
88
  except Exception as e:
@@ -48,6 +92,7 @@ class FileChangeHandler(FileSystemEventHandler):
48
92
  def watch_files(
49
93
  filenames: List[str],
50
94
  process: Union[Callable[[List[str]], None], Callable[[List[str]], Awaitable[None]]],
95
+ shell: BuildShell,
51
96
  ) -> None:
52
97
  # Handle both sync and async process functions
53
98
  async def process_wrapper(files: List[str]) -> None:
@@ -61,8 +106,9 @@ def watch_files(
61
106
  elapsed_time = time_end - time_start
62
107
  click.echo(
63
108
  FeedbackManager.success(message="\n✓ ")
64
- + FeedbackManager.gray(message=f"Rebuild completed in {elapsed_time:.1f}s\n")
109
+ + FeedbackManager.gray(message=f"Rebuild completed in {elapsed_time:.1f}s")
65
110
  )
111
+ shell.reprint_prompt()
66
112
 
67
113
  event_handler = FileChangeHandler(filenames, lambda f: asyncio.run(process_wrapper(f)))
68
114
  observer = Observer()
@@ -113,7 +159,9 @@ async def build(
113
159
  ignore_sql_errors = FeatureFlags.ignore_sql_errors()
114
160
  context.disable_template_security_validation.set(True)
115
161
  is_internal = has_internal_datafiles(folder)
116
- tb_client = get_tinybird_local_client()
162
+
163
+ folder_path = os.path.abspath(folder)
164
+ tb_client = await get_tinybird_local_client(folder_path)
117
165
 
118
166
  def check_filenames(filenames: List[str]):
119
167
  parser_matrix = {".pipe": parse_pipe, ".datasource": parse_datasource}
@@ -134,29 +182,53 @@ async def build(
134
182
  parser(filename)
135
183
 
136
184
  async def process(filenames: List[str], watch: bool = False, only_pipes: bool = False):
137
- check_filenames(filenames=filenames)
138
- await folder_build(
139
- tb_client,
140
- filenames,
141
- ignore_sql_errors=ignore_sql_errors,
142
- is_internal=is_internal,
143
- only_pipes=only_pipes,
144
- )
185
+ datafiles = [f for f in filenames if f.endswith(".datasource") or f.endswith(".pipe")]
186
+ if len(datafiles) > 0:
187
+ check_filenames(filenames=datafiles)
188
+ await folder_build(
189
+ tb_client,
190
+ filenames=datafiles,
191
+ ignore_sql_errors=ignore_sql_errors,
192
+ is_internal=is_internal,
193
+ only_pipes=only_pipes,
194
+ )
195
+
196
+ filename = filenames[0]
197
+ if filename.endswith(".ndjson"):
198
+ fixture_path = Path(filename)
199
+ name = "_".join(fixture_path.stem.split("_")[:-1])
200
+ ds_path = Path(folder) / "datasources" / f"{name}.datasource"
201
+ if ds_path.exists():
202
+ await append_datasource({}, tb_client, name, str(fixture_path), silent=True)
145
203
 
146
204
  if watch:
147
- filename = filenames[0]
148
- if filename.endswith(".pipe"):
149
- await build_and_print_pipe(tb_client, filename)
150
-
151
- filenames = get_project_filenames(folder)
205
+ if filename.endswith(".datasource"):
206
+ ds_path = Path(filename)
207
+ name = build_fixture_name(filename, ds_path.stem, ds_path.read_text())
208
+ fixture_path = get_fixture_dir() / f"{name}.ndjson"
209
+ if fixture_path.exists():
210
+ await append_datasource({}, tb_client, ds_path.stem, str(fixture_path), silent=True)
211
+ if not filename.endswith(".ndjson"):
212
+ await build_and_print_resource(tb_client, filename)
213
+
214
+ datafiles = get_project_filenames(folder)
215
+ fixtures = get_project_fixtures(folder)
216
+ filenames = datafiles + fixtures
152
217
 
153
218
  async def build_once(filenames: List[str]):
154
219
  try:
155
- click.echo("⚡ Building project...")
220
+ click.echo("⚡ Building project...\n")
156
221
  time_start = time.time()
157
222
  await process(filenames=filenames, watch=False, only_pipes=skip_datasources)
158
223
  time_end = time.time()
159
224
  elapsed_time = time_end - time_start
225
+ for filename in filenames:
226
+ if filename.endswith(".datasource"):
227
+ ds_path = Path(filename)
228
+ name = build_fixture_name(filename, ds_path.stem, ds_path.read_text())
229
+ fixture_path = get_fixture_dir() / f"{name}.ndjson"
230
+ if fixture_path.exists():
231
+ await append_datasource({}, tb_client, ds_path.stem, str(fixture_path), silent=True)
160
232
  click.echo(FeedbackManager.success(message=f"\n✓ Build completed in {elapsed_time:.1f}s\n"))
161
233
  except Exception as e:
162
234
  click.echo(FeedbackManager.error(message=str(e)))
@@ -164,31 +236,21 @@ async def build(
164
236
  await build_once(filenames)
165
237
 
166
238
  if watch:
167
- click.echo(FeedbackManager.highlight(message="◎ Watching for changes...\n"))
168
- watcher_thread = threading.Thread(target=watch_files, args=(filenames, process), daemon=True)
239
+ shell = BuildShell(folder=folder)
240
+ click.echo(FeedbackManager.highlight(message="◎ Watching for changes..."))
241
+ watcher_thread = threading.Thread(target=watch_files, args=(filenames, process, shell), daemon=True)
169
242
  watcher_thread.start()
243
+ shell.cmdloop()
170
244
 
171
- # Main CLI loop
172
- while True:
173
- user_input = click.prompt("", prompt_suffix="")
174
- if user_input.lower() == "exit":
175
- break
176
-
177
- if "tb build" in user_input:
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
245
 
185
-
186
- async def build_and_print_pipe(tb_client: TinyB, filename: str):
246
+ async def build_and_print_resource(tb_client: TinyB, filename: str):
187
247
  rebuild_colors = [bcolors.FAIL, bcolors.OKBLUE, bcolors.WARNING, bcolors.OKGREEN, bcolors.HEADER]
188
248
  rebuild_index = random.randint(0, len(rebuild_colors) - 1)
189
249
  rebuild_color = rebuild_colors[rebuild_index % len(rebuild_colors)]
190
- pipe_name = Path(filename).stem
191
- res = await tb_client.query(f"SELECT * FROM {pipe_name} FORMAT JSON", pipeline=pipe_name)
250
+ resource_path = Path(filename)
251
+ name = resource_path.stem
252
+ pipeline = name if filename.endswith(".pipe") else None
253
+ res = await tb_client.query(f"SELECT * FROM {name} FORMAT JSON", pipeline=pipeline)
192
254
  data = []
193
255
  limit = 5
194
256
  for d in res["data"][:5]:
@@ -197,7 +259,6 @@ async def build_and_print_pipe(tb_client: TinyB, filename: str):
197
259
  row_count = res.get("rows", 0)
198
260
  stats = res.get("statistics", {})
199
261
  elapsed = stats.get("elapsed", 0)
200
- node_name = "endpoint"
201
262
  cols = len(meta)
202
263
  try:
203
264
 
@@ -207,7 +268,7 @@ async def build_and_print_pipe(tb_client: TinyB, filename: str):
207
268
  table = format_table(data, meta)
208
269
  colored_char = print_message("│", rebuild_color)
209
270
  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 {pipe_name} → {node_name}")
271
+ click.echo(f"\n{colored_char} {print_message('⚡', rebuild_color)} Running {name}")
211
272
  click.echo(colored_char)
212
273
  click.echo(table_with_marker)
213
274
  click.echo(colored_char)
@@ -225,3 +286,24 @@ async def build_and_print_pipe(tb_client: TinyB, filename: str):
225
286
  click.echo("------------")
226
287
  else:
227
288
  raise exc
289
+
290
+
291
+ async def append_datasource(
292
+ ctx: click.Context,
293
+ tb_client: TinyB,
294
+ datasource_name: str,
295
+ url: str,
296
+ silent: bool = False,
297
+ ):
298
+ await push_data(
299
+ ctx,
300
+ tb_client,
301
+ datasource_name,
302
+ url,
303
+ connector=None,
304
+ sql=None,
305
+ mode="append",
306
+ ignore_empty=False,
307
+ concurrency=1,
308
+ silent=silent,
309
+ )
@@ -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
 
@@ -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))
@@ -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
- parent_dir: Optional[str] = None,
170
+ folder: Optional[str] = None,
170
171
  ):
171
172
  p = Path(filename)
172
173
  base = Path("datasources")
173
- if parent_dir:
174
- base = Path(parent_dir) / base
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
- click.echo(FeedbackManager.info_starting_export_process(connector=connector))
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
- click.echo(FeedbackManager.gray(message=f"\nImporting data to {datasource_name} Data Source..."))
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 mode == "append" and parser and parser != "clickhouse":
1191
- click.echo(FeedbackManager.success_appended_rows(appended_rows=appended_rows))
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
- if mode == "replace":
1194
- click.echo(FeedbackManager.success_replaced_datasource(datasource=datasource_name))
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
- click.echo(FeedbackManager.success_progress_blocks())
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"))
@@ -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, push_data
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.build import folder_build
17
- from tinybird.tb.modules.exceptions import CLIDatasourceException
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=100, help="Number of events to send")
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
- await append_datasource(ctx, tb_client, ds_name, data, None, None, False, 1)
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
- max_rows_per_request = 100
81
- sent_rows = 0
82
- for i in range(0, len(data), max_rows_per_request):
83
- batch = data[i : i + max_rows_per_request]
84
- ndjson_data = "\n".join([json.dumps(row) for row in batch])
85
- await tb_client.datasource_events(datasource_name, ndjson_data)
86
- sent_rows += len(batch)
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
- click.echo(FeedbackManager.info_path_created(path=x))
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
- await _generate_datafile(str(path), client, format=format, force=force)
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(content, filename=f"{ds.name}.datasource", data=None, _format="ndjson", force=force)
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
- async def append_datasource(
184
- ctx: Context,
185
- tb_client: TinyB,
186
- datasource_name: str,
187
- url: str,
188
- sql: Optional[str],
189
- incremental: Optional[str],
190
- ignore_empty: bool,
191
- concurrency: int,
192
- ):
193
- if incremental:
194
- date = None
195
- source_column = incremental.split(":")[0]
196
- dest_column = incremental.split(":")[-1]
197
- result = await tb_client.query(f"SELECT max({dest_column}) as inc from {datasource_name} FORMAT JSON")
198
- try:
199
- date = result["data"][0]["inc"]
200
- except Exception as e:
201
- raise CLIDatasourceException(f"{str(e)}")
202
- if date:
203
- sql = f"{sql} WHERE {source_column} > '{date}'"
204
- await push_data(
205
- ctx,
206
- tb_client,
207
- datasource_name,
208
- url,
209
- None,
210
- sql,
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
- user_workspaces = await user_client.user_workspaces()
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
- local_ws = next((ws for ws in workspaces if ws["name"] == TB_LOCAL_WORKSPACE_NAME), {})
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(ws_client, folder=shared_ws_path.as_posix(), is_vendor=True, current_ws=vendor_ws)
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
@@ -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-mini",
42
+ model="gpt-4o",
43
43
  messages=[{"role": "system", "content": create_project_prompt}, {"role": "user", "content": prompt}],
44
44
  response_format=DataProject,
45
45
  )
@@ -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 Local..."))
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 Local to be ready..."))
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 environment is not running. Please start it with `tb local start`.")
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)
@@ -128,6 +147,14 @@ def get_tinybird_local_client():
128
147
  return config.get_client(host=TB_LOCAL_HOST, token=token)
129
148
 
130
149
 
150
+ @cli.command()
151
+ def upgrade():
152
+ """Upgrade Tinybird CLI to the latest version"""
153
+ click.echo(FeedbackManager.info(message="Upgrading Tinybird CLI..."))
154
+ os.system(f"{os.getenv('HOME')}/.local/bin/uv tool upgrade tinybird")
155
+ click.echo(FeedbackManager.success(message="Tinybird CLI upgraded"))
156
+
157
+
131
158
  @cli.group()
132
159
  @click.pass_context
133
160
  def local(ctx):
@@ -137,39 +164,39 @@ def local(ctx):
137
164
  @local.command()
138
165
  @coro
139
166
  async def stop() -> None:
140
- """Stop Tinybird development environment"""
141
- click.echo(FeedbackManager.info(message="Shutting down Tinybird development environment..."))
167
+ """Stop Tinybird local"""
168
+ click.echo(FeedbackManager.info(message="Shutting down Tinybird local..."))
142
169
  docker_client = get_docker_client()
143
170
  stop_tinybird_local(docker_client)
144
- click.echo(FeedbackManager.success(message="Tinybird development environment stopped"))
171
+ click.echo(FeedbackManager.success(message="Tinybird local stopped"))
145
172
 
146
173
 
147
174
  @local.command()
148
175
  @coro
149
176
  async def remove() -> None:
150
- """Remove Tinybird development environment"""
151
- click.echo(FeedbackManager.info(message="Removing Tinybird development environment..."))
177
+ """Remove Tinybird local"""
178
+ click.echo(FeedbackManager.info(message="Removing Tinybird local..."))
152
179
  docker_client = get_docker_client()
153
180
  remove_tinybird_local(docker_client)
154
- click.echo(FeedbackManager.success(message="Tinybird development environment removed"))
181
+ click.echo(FeedbackManager.success(message="Tinybird local removed"))
155
182
 
156
183
 
157
184
  @local.command()
158
185
  @coro
159
186
  async def start() -> None:
160
- """Start Tinybird development environment"""
161
- click.echo(FeedbackManager.info(message="Starting Tinybird development environment..."))
187
+ """Start Tinybird local"""
188
+ click.echo(FeedbackManager.info(message="Starting Tinybird local..."))
162
189
  docker_client = get_docker_client()
163
190
  start_tinybird_local(docker_client)
164
- click.echo(FeedbackManager.success(message="Tinybird development environment started"))
191
+ click.echo(FeedbackManager.success(message="Tinybird local started"))
165
192
 
166
193
 
167
194
  @local.command()
168
195
  @coro
169
196
  async def restart() -> None:
170
- """Restart Tinybird development environment"""
171
- click.echo(FeedbackManager.info(message="Restarting Tinybird development environment..."))
197
+ """Restart Tinybird local"""
198
+ click.echo(FeedbackManager.info(message="Restarting Tinybird local..."))
172
199
  docker_client = get_docker_client()
173
200
  remove_tinybird_local(docker_client)
174
201
  start_tinybird_local(docker_client)
175
- click.echo(FeedbackManager.success(message="Tinybird development environment restarted"))
202
+ click.echo(FeedbackManager.success(message="Tinybird local restarted"))
@@ -1,4 +1,4 @@
1
- import json
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 datasource.
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)
@@ -32,22 +33,30 @@ async def mock(datasource: str, rows: int, context: str) -> None:
32
33
  datasource_name = datasource_path.stem
33
34
  else:
34
35
  datasource_path = Path("datasources", f"{datasource}.datasource")
36
+ datasource_path = Path(folder) / datasource_path
37
+
38
+ context_path = Path(folder) / "fixtures" / f"{datasource_name}.prompt"
39
+ if not context:
40
+ # load the context from the fixture.prompt file if it exists
41
+ if context_path.exists():
42
+ click.echo(FeedbackManager.gray(message=f"Using context for {context_path}..."))
43
+ context = context_path.read_text()
44
+ else:
45
+ click.echo(FeedbackManager.gray(message=f"Overriding context for {datasource_name}..."))
46
+ context_path.write_text(context)
47
+
35
48
 
49
+ click.echo(FeedbackManager.gray(message=f"Creating fixture for {datasource_name}..."))
36
50
  datasource_content = datasource_path.read_text()
37
51
  llm_config = CLIConfig.get_llm_config()
38
52
  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)
53
+ tb_client = await get_tinybird_local_client(os.path.abspath(folder))
54
+ sql = await llm.generate_sql_sample_data(tb_client, datasource_content, row_count=rows, context=context)
41
55
  result = await tb_client.query(f"{sql} FORMAT JSON")
42
- data = result.get("data", [])
43
- max_rows_per_request = 100
44
- sent_rows = 0
45
- for i in range(0, len(data), max_rows_per_request):
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}'")
56
+ data = result.get("data", [])[:rows]
57
+ fixture_name = build_fixture_name(datasource_path.absolute(), datasource_name, datasource_content)
58
+ persist_fixture(fixture_name, data)
59
+ click.echo(FeedbackManager.success(message="✓ Done!"))
51
60
 
52
61
  except Exception as e:
53
- raise CLIException(FeedbackManager.error_exception(error=str(e)))
62
+ raise CLIException(FeedbackManager.error(message=str(e)))
@@ -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 need to use dynamic parameters you must start the whole sql query ALWAYS with "%" symbol on top. e.g: SQL >\n %\n SELECT * FROM <table> WHERE <condition> LIMIT 10
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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tinybird
3
- Version: 0.0.1.dev9
3
+ Version: 0.0.1.dev11
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -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=qX8yrjoHUL4kjb_IKzLZPPfg4m54XWUZkdVOrv1ktpo,67814
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=SAtWf40kH0HCkSiSm2bPD0YJbLmpwALt-UI9JqnO6SQ,8393
22
- tinybird/tb/modules/cicd.py,sha256=mIMU1gNGpN3f5K3rBYN9rFoOT3RogDxIs_zB3LY-iO4,7463
23
- tinybird/tb/modules/cli.py,sha256=Yw6ooemOAIUZR-IKk05BG_D7RgoljTgoHBYW7aq4XaM,56674
24
- tinybird/tb/modules/common.py,sha256=vb69xKJXbRjoYOlCwerXEU3488Od_zfR2N2yP5VyNw8,79372
21
+ tinybird/tb/modules/build.py,sha256=aqVmey7jkb-ZDRaxl5h66u3g9bmI46_j-u3hXEPPVho,11746
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=PXCdmyhkG9-XjWGNpyR24nuLLnLEBIs2v9ycxCk10aw,7456
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=D6ShlqCorZpRLjE5srI7Ws4VUTH6kSotTi88CvUwoGw,2446
33
- tinybird/tb/modules/local.py,sha256=R7L0Inn23OXVs7nssra8mto52fbP7rU2eUU4Bj6_AXU,5998
34
- tinybird/tb/modules/mock.py,sha256=dYxm8_1zXGZnf4X8OYmG9hK1I-csHakD64Hag8mAn4Q,2069
32
+ tinybird/tb/modules/llm.py,sha256=TvJJ9BlKISAb1SVI-pnHp_PcHcxGfTyjxOE_qAz90Ck,2441
33
+ tinybird/tb/modules/local.py,sha256=sImiZwUMsvJRGBVZovOGBqxXo0SBWYwpZ7b8zVG_QNc,6943
34
+ tinybird/tb/modules/mock.py,sha256=RohmEhNfudVryn2pJrI4fASE74inovNxzN0ew85Y830,2747
35
35
  tinybird/tb/modules/pipe.py,sha256=9wnfKbp2FkmLiJgVk3qbra76ktwsUTXghu6j9cCEahQ,31058
36
- tinybird/tb/modules/prompts.py,sha256=u_-VSsqk8fXJTsLO3sCv9DR1Qx2V_M_Z_gBfiyvn5jA,7251
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=SWPkmMQYde11LOt0B1JuYoyU8SsSTLjjVp_tPSgMoTM,91754
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=X4a-UM_GSOmR8ks2kBITgebXz-aE-iEdG5F1eEUzIyc,27555
49
- tinybird/tb/modules/datafile/common.py,sha256=hoIm6mvXO49Jy2W2XPGE7NgOj-GmYw7zALXREQ0C3eQ,33969
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.dev9.dist-info/METADATA,sha256=YbZtwUdfek1Uzww7BFCLXsumr1cHtYOH7ceOQnKLjlo,2404
68
- tinybird-0.0.1.dev9.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
69
- tinybird-0.0.1.dev9.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
70
- tinybird-0.0.1.dev9.dist-info/top_level.txt,sha256=pgw6AzERHBcW3YTi2PW4arjxLkulk2msOz_SomfOEuc,45
71
- tinybird-0.0.1.dev9.dist-info/RECORD,,
68
+ tinybird-0.0.1.dev11.dist-info/METADATA,sha256=F7NlFSAcq7ZbchPPjfnBdz5R-QohYQSy_MwXY3YWroI,2405
69
+ tinybird-0.0.1.dev11.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
70
+ tinybird-0.0.1.dev11.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
71
+ tinybird-0.0.1.dev11.dist-info/top_level.txt,sha256=pgw6AzERHBcW3YTi2PW4arjxLkulk2msOz_SomfOEuc,45
72
+ tinybird-0.0.1.dev11.dist-info/RECORD,,