tinybird 0.0.1.dev3__py3-none-any.whl → 0.0.1.dev5__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/__cli__.py CHANGED
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
4
4
  __url__ = 'https://www.tinybird.co/docs/cli/introduction.html'
5
5
  __author__ = 'Tinybird'
6
6
  __author_email__ = 'support@tinybird.co'
7
- __version__ = '0.0.1.dev3'
8
- __revision__ = '50bec29'
7
+ __version__ = '0.0.1.dev5'
8
+ __revision__ = 'b69dd20'
tinybird/check_pypi.py CHANGED
@@ -6,7 +6,7 @@ from tinybird.feedback_manager import FeedbackManager
6
6
  from tinybird.syncasync import sync_to_async
7
7
  from tinybird.tb_cli_modules.common import CLIException, getenv_bool
8
8
 
9
- PYPY_URL = "https://pypi.org/pypi/tinybird-cli/json"
9
+ PYPY_URL = "https://pypi.org/pypi/tinybird/json"
10
10
  requests_get = sync_to_async(requests.get, thread_sensitive=False)
11
11
 
12
12
 
tinybird/datafile.py CHANGED
@@ -3125,7 +3125,7 @@ async def new_pipe(
3125
3125
  except Exception as e:
3126
3126
  raise click.ClickException(FeedbackManager.error_creating_pipe(error=e))
3127
3127
 
3128
- if data.get("type") == "endpoint":
3128
+ if data.get("type") == "endpoint" and t:
3129
3129
  click.echo(FeedbackManager.success_test_endpoint(host=host, pipe=p["name"], token=t["token"]))
3130
3130
 
3131
3131
 
@@ -51,6 +51,10 @@ def prompt_message(message: str) -> Callable[..., str]:
51
51
  return print_message(message, bcolors.HEADER)
52
52
 
53
53
 
54
+ def gray_message(message: str) -> Callable[..., str]:
55
+ return print_message(message, bcolors.CGREY)
56
+
57
+
54
58
  class FeedbackManager:
55
59
  error_exception = error_message("{error}")
56
60
  simple_error_exception = simple_error_message("{error}")
@@ -561,9 +565,9 @@ Ready? """
561
565
  )
562
566
  warning_fixture_not_found = warning_message("** Warning: No fixture found for the datasource {datasource_name}")
563
567
  warning_update_version = warning_message(
564
- '** UPDATE AVAILABLE: please run "pip install tinybird-cli=={latest_version}" to update or `export TB_VERSION_WARNING=0` to skip the check.'
568
+ '** UPDATE AVAILABLE: please run "pip install tinybird=={latest_version}" to update or `export TB_VERSION_WARNING=0` to skip the check.'
565
569
  )
566
- warning_current_version = warning_message("** current: tinybird-cli {current_version}\n")
570
+ warning_current_version = warning_message("** current: tinybird {current_version}\n")
567
571
  warning_confirm_truncate_datasource = prompt_message(
568
572
  "Do you want to truncate {datasource}? Once truncated, your data can't be recovered"
569
573
  )
@@ -712,9 +716,9 @@ Ready? """
712
716
  info_removing_pipe = info_message("** Removing pipe {pipe}")
713
717
  info_removing_pipe_not_found = info_message("** {pipe} not found")
714
718
  info_dry_removing_pipe = info_message("** [DRY RUN] Removing pipe {pipe}")
715
- info_path_created = info_message("** - /{path} created")
716
- info_file_created = info_message("** - {file} created")
717
- info_path_already_exists = info_message("** - /{path} already exists, skipping")
719
+ info_path_created = info_message(" /{path}")
720
+ info_file_created = info_message(" /{file}")
721
+ info_path_already_exists = info_message(" /{path} already exists, skipping")
718
722
  info_dottinyb_already_ignored = info_message("** - '.tinyb' already in .gitignore, skipping")
719
723
  info_dotdifftemp_already_ignored = info_message("** - '.diff_tmp' not found or already in .gitignore, skipping")
720
724
  info_dottinyenv_already_exists = info_message("** - '.tinyenv' already exists, skipping")
@@ -935,7 +939,7 @@ Ready? """
935
939
  success_create = success_message("** '{name}' created")
936
940
  success_delete = success_message("** '{name}' deleted")
937
941
  success_dynamodb_initial_load = success_message("** Initial load of DynamoDB table started: {job_url}")
938
- success_progress_blocks = success_message("** \N{FRONT-FACING BABY CHICK} done")
942
+ success_progress_blocks = success_message(" Done!")
939
943
  success_now_using_config = success_message("** Now using {name} ({id})")
940
944
  success_connector_config = success_message(
941
945
  "** {connector} configuration written to {file_name} file, consider adding it to .gitignore"
@@ -1026,3 +1030,4 @@ Ready? """
1026
1030
  info = info_message("{message}")
1027
1031
  highlight = info_highlight_message("{message}")
1028
1032
  error = error_message("{message}")
1033
+ gray = gray_message("{message}")
tinybird/tb_cli.py CHANGED
@@ -14,6 +14,7 @@ import tinybird.tb_cli_modules.create
14
14
  import tinybird.tb_cli_modules.datasource
15
15
  import tinybird.tb_cli_modules.fmt
16
16
  import tinybird.tb_cli_modules.job
17
+ import tinybird.tb_cli_modules.mock
17
18
  import tinybird.tb_cli_modules.pipe
18
19
  import tinybird.tb_cli_modules.tag
19
20
  import tinybird.tb_cli_modules.test
@@ -1,11 +1,12 @@
1
1
  import asyncio
2
- import json
3
2
  import os
3
+ import random
4
4
  import time
5
5
  from pathlib import Path
6
6
  from typing import Any, Awaitable, Callable, Dict, List, Union
7
7
 
8
8
  import click
9
+ import humanfriendly
9
10
  from watchdog.events import FileSystemEventHandler
10
11
  from watchdog.observers import Observer
11
12
 
@@ -20,20 +21,15 @@ from tinybird.datafile import (
20
21
  parse_datasource,
21
22
  parse_pipe,
22
23
  )
23
- from tinybird.feedback_manager import FeedbackManager, info_highlight_message, success_message
24
+ from tinybird.feedback_manager import FeedbackManager, bcolors
24
25
  from tinybird.tb_cli_modules.cli import cli
25
26
  from tinybird.tb_cli_modules.common import (
26
27
  coro,
27
- echo_safe_humanfriendly_tables_format_smart_table,
28
28
  )
29
- from tinybird.tb_cli_modules.create import generate_sample_data_from_columns
30
29
  from tinybird.tb_cli_modules.local import (
31
- get_docker_client,
32
30
  get_tinybird_local_client,
33
- remove_tinybird_local,
34
- start_tinybird_local,
35
- stop_tinybird_local,
36
31
  )
32
+ from tinybird.tb_cli_modules.table import format_table
37
33
 
38
34
 
39
35
  class FileChangeHandler(FileSystemEventHandler):
@@ -44,7 +40,7 @@ class FileChangeHandler(FileSystemEventHandler):
44
40
  def on_modified(self, event: Any) -> None:
45
41
  if not event.is_directory and any(event.src_path.endswith(ext) for ext in [".datasource", ".pipe"]):
46
42
  filename = event.src_path.split("/")[-1]
47
- click.echo(info_highlight_message(f"\n⟲ Changes detected in {filename}\n")())
43
+ click.echo(FeedbackManager.highlight(message=f"\n⟲ Changes detected in {filename}\n"))
48
44
  try:
49
45
  self.process([event.src_path])
50
46
  except Exception as e:
@@ -65,7 +61,10 @@ def watch_files(
65
61
  process(files, watch=True)
66
62
  time_end = time.time()
67
63
  elapsed_time = time_end - time_start
68
- click.echo(success_message(f"\n✓ Rebuild completed in {elapsed_time:.1f}s")())
64
+ click.echo(
65
+ FeedbackManager.success(message="\n✓ ")
66
+ + FeedbackManager.gray(message=f"Rebuild completed in {elapsed_time:.1f}s")
67
+ )
69
68
 
70
69
  event_handler = FileChangeHandler(filenames, lambda f: asyncio.run(process_wrapper(f)))
71
70
  observer = Observer()
@@ -100,23 +99,19 @@ def watch_files(
100
99
  help="Watch for changes in the files and re-check them.",
101
100
  )
102
101
  @click.option(
103
- "--restart",
102
+ "--skip-datasources",
104
103
  is_flag=True,
105
- help="Restart the Tinybird development environment before building the first time.",
104
+ help="Skip rebuilding datasources.",
106
105
  )
107
106
  @coro
108
107
  async def build(
109
108
  folder: str,
110
109
  watch: bool,
111
- restart: bool,
110
+ skip_datasources: bool,
112
111
  ) -> None:
113
112
  """
114
113
  Watch for changes in the files and re-check them.
115
114
  """
116
- docker_client = get_docker_client()
117
- if restart:
118
- remove_tinybird_local(docker_client)
119
- start_tinybird_local(docker_client)
120
115
  ignore_sql_errors = FeatureFlags.ignore_sql_errors()
121
116
  context.disable_template_security_validation.set(True)
122
117
  is_internal = has_internal_datafiles(folder)
@@ -156,15 +151,6 @@ async def build(
156
151
  only_pipes=only_pipes,
157
152
  )
158
153
 
159
- for filename in filenames:
160
- if filename.endswith(".datasource"):
161
- ds_path = Path(filename)
162
- ds_name = ds_path.stem
163
- datasource_content = ds_path.read_text()
164
- sample_data = await generate_sample_data_from_columns(tb_client, datasource_content)
165
- ndjson_data = "\n".join([json.dumps(row) for row in sample_data])
166
- await tb_client.datasource_events(ds_name, ndjson_data)
167
-
168
154
  if watch:
169
155
  filename = filenames[0]
170
156
  if filename.endswith(".pipe"):
@@ -176,7 +162,7 @@ async def build(
176
162
  try:
177
163
  click.echo("⚡ Building project...")
178
164
  time_start = time.time()
179
- await process(filenames=filenames, watch=False)
165
+ await process(filenames=filenames, watch=False, only_pipes=skip_datasources)
180
166
  time_end = time.time()
181
167
  elapsed_time = time_end - time_start
182
168
  click.echo(FeedbackManager.success(message=f"\n✓ Build completed in {elapsed_time:.1f}s\n"))
@@ -191,31 +177,44 @@ async def build(
191
177
 
192
178
 
193
179
  async def build_and_print_pipe(tb_client: TinyB, filename: str):
194
- pipe_name = os.path.basename(filename.split(".")[0])
195
- res = await tb_client.query(f"SELECT * FROM {pipe_name} LIMIT 5 FORMAT JSON", pipeline=pipe_name)
180
+ rebuild_colors = [bcolors.FAIL, bcolors.OKBLUE, bcolors.WARNING, bcolors.OKGREEN, bcolors.HEADER]
181
+ rebuild_index = random.randint(0, len(rebuild_colors) - 1)
182
+ rebuild_color = rebuild_colors[rebuild_index % len(rebuild_colors)]
183
+ pipe_name = Path(filename).stem
184
+ res = await tb_client.query(f"SELECT * FROM {pipe_name} FORMAT JSON", pipeline=pipe_name)
196
185
  data = []
197
- for d in res["data"]:
186
+ limit = 5
187
+ for d in res["data"][:5]:
198
188
  data.append(d.values())
199
189
  meta = res["meta"]
200
- column_names = [col["name"] for col in meta]
201
- echo_safe_humanfriendly_tables_format_smart_table(data, column_names=column_names)
202
-
203
-
204
- @cli.command()
205
- @coro
206
- async def stop() -> None:
207
- """Stop Tinybird development environment"""
208
- click.echo(FeedbackManager.info(message="Shutting down Tinybird development environment..."))
209
- docker_client = get_docker_client()
210
- stop_tinybird_local(docker_client)
211
- click.echo(FeedbackManager.success(message="Tinybird development environment stopped"))
212
-
190
+ row_count = res.get("rows", 0)
191
+ stats = res.get("statistics", {})
192
+ elapsed = stats.get("elapsed", 0)
193
+ node_name = "endpoint"
194
+ cols = len(meta)
195
+ try:
213
196
 
214
- @cli.command()
215
- @coro
216
- async def start() -> None:
217
- """Start Tinybird development environment"""
218
- click.echo(FeedbackManager.info(message="Starting Tinybird development environment..."))
219
- docker_client = get_docker_client()
220
- start_tinybird_local(docker_client)
221
- click.echo(FeedbackManager.success(message="Tinybird development environment started"))
197
+ def print_message(message: str, color=bcolors.CGREY):
198
+ return f"{color}{message}{bcolors.ENDC}"
199
+
200
+ table = format_table(data, meta)
201
+ colored_char = print_message("│", rebuild_color)
202
+ table_with_marker = "\n".join(f"{colored_char} {line}" for line in table.split("\n"))
203
+ click.echo(f"\n{colored_char} {print_message('⚡', rebuild_color)} Running {pipe_name} → {node_name}")
204
+ click.echo(colored_char)
205
+ click.echo(table_with_marker)
206
+ click.echo(colored_char)
207
+ rows_read = humanfriendly.format_number(stats.get("rows_read", 0))
208
+ bytes_read = humanfriendly.format_size(stats.get("bytes_read", 0))
209
+ elapsed = humanfriendly.format_timespan(elapsed) if elapsed >= 1 else f"{elapsed * 1000:.2f}ms"
210
+ stats_message = f"» {bytes_read} ({rows_read} rows x {cols} cols) in {elapsed}"
211
+ rows_message = f"» Showing {limit} first rows" if row_count > limit else "» Showing all rows"
212
+ click.echo(f"{colored_char} {print_message(stats_message, bcolors.OKGREEN)}")
213
+ click.echo(f"{colored_char} {print_message(rows_message, bcolors.CGREY)}")
214
+ except ValueError as exc:
215
+ if str(exc) == "max() arg is an empty sequence":
216
+ click.echo("------------")
217
+ click.echo("Empty")
218
+ click.echo("------------")
219
+ else:
220
+ raise exc
@@ -10,14 +10,12 @@ import pprint
10
10
  import re
11
11
  import shutil
12
12
  import sys
13
- from datetime import datetime
14
13
  from os import environ, getcwd
15
14
  from pathlib import Path
16
15
  from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
17
16
 
18
17
  import click
19
18
  import humanfriendly
20
- import requests
21
19
  from click import Context
22
20
  from packaging import version
23
21
 
@@ -58,7 +56,7 @@ from tinybird.tb_cli_modules.common import (
58
56
  _get_tb_client,
59
57
  coro,
60
58
  create_tb_client,
61
- echo_safe_humanfriendly_tables_format_smart_table,
59
+ echo_safe_format_table,
62
60
  folder_init,
63
61
  get_current_main_workspace,
64
62
  getenv_bool,
@@ -69,7 +67,6 @@ from tinybird.tb_cli_modules.common import (
69
67
  try_update_config_with_remote,
70
68
  )
71
69
  from tinybird.tb_cli_modules.config import CLIConfig
72
- from tinybird.tb_cli_modules.prompts import sample_data_prompt
73
70
  from tinybird.tb_cli_modules.telemetry import add_telemetry_event
74
71
 
75
72
  __old_click_echo = click.echo
@@ -927,7 +924,7 @@ async def sql(
927
924
  dd = []
928
925
  for d in res["data"]:
929
926
  dd.append(d.values())
930
- echo_safe_humanfriendly_tables_format_smart_table(dd, column_names=res["data"][0].keys())
927
+ echo_safe_format_table(dd, columns=res["meta"])
931
928
  else:
932
929
  click.echo(FeedbackManager.info_no_rows())
933
930
 
@@ -1572,67 +1569,3 @@ async def deploy(
1572
1569
  raise
1573
1570
  except Exception as e:
1574
1571
  raise CLIException(str(e))
1575
-
1576
-
1577
- @cli.command()
1578
- @click.argument("datasource_file", type=click.Path(exists=True))
1579
- @click.option("--count", type=int, default=10, help="Number of events to send")
1580
- @click.option("--model", type=str, default=None, help="Model to use for data generation")
1581
- @click.option("--print-data", is_flag=True, default=False, help="Print the data being sent")
1582
- @click.pass_context
1583
- def load_sample_data(ctx: Context, datasource_file: str, count: int, model: Optional[str], print_data: bool) -> None:
1584
- """Load sample data into a datasource.
1585
-
1586
- Args:
1587
- ctx: Click context object
1588
- datasource_file: Path to the datasource file to load sample data into
1589
- """
1590
- import llm
1591
-
1592
- try:
1593
- # TODO(eclbg): allow passing a datasource name instead of a file
1594
- datasource_path = Path(datasource_file)
1595
- if datasource_path.suffix != ".datasource":
1596
- raise CLIException(FeedbackManager.error_file_extension(filename=datasource_file))
1597
-
1598
- datasource_name = datasource_path.stem
1599
-
1600
- response = requests.get("http://localhost:80/tokens")
1601
- token = response.json()["workspace_admin_token"]
1602
-
1603
- with open(datasource_file) as f:
1604
- content = f.read()
1605
- schema_start = next(i for i, line in enumerate(content.splitlines()) if line.strip().startswith("SCHEMA >"))
1606
- schema_end = next(
1607
- i
1608
- for i, line in enumerate(content.splitlines()[schema_start + 1 :], schema_start + 1)
1609
- if not line.strip()
1610
- )
1611
- schema = "\n".join(content.splitlines()[schema_start:schema_end])
1612
- llm_model = llm.get_model(model)
1613
- click.echo(f"Using model: {model}")
1614
- prompt = sample_data_prompt.format(current_datetime=datetime.now().isoformat(), row_count=count)
1615
- # prompt = sample_data_with_errors_prompt.format(current_datetime=datetime.now().isoformat()) # This prompt will generate data with errors
1616
- full_prompt = prompt + "\n\n" + schema
1617
- sent_events = 0
1618
- while sent_events < count:
1619
- click.echo(f"Generating data for '{datasource_name}'")
1620
- data = llm_model.prompt(full_prompt)
1621
-
1622
- click.echo(f"Sending data to '{datasource_name}'")
1623
- headers = {"Authorization": f"Bearer {token}"}
1624
- if print_data:
1625
- click.echo(f"Data: {data}")
1626
- response = requests.post(
1627
- f"http://localhost:80/v0/events?name={datasource_name}",
1628
- data=data,
1629
- headers=headers,
1630
- )
1631
- if response.status_code not in (200, 202):
1632
- raise CLIException(f"Failed to send data: {response.text}")
1633
- click.echo(f"Response: {response.text}")
1634
- sent_events += 10
1635
- click.echo(f"Sent 10 events to datasource '{datasource_name}'")
1636
-
1637
- except Exception as e:
1638
- raise CLIException(FeedbackManager.error_exception(error=str(e)))
@@ -53,6 +53,7 @@ from tinybird.config import (
53
53
  get_display_host,
54
54
  write_config,
55
55
  )
56
+ from tinybird.tb_cli_modules.table import format_table
56
57
 
57
58
  if TYPE_CHECKING:
58
59
  from tinybird.connectors import Connector
@@ -134,6 +135,23 @@ def echo_safe_humanfriendly_tables_format_smart_table(data: Iterable[Any], colum
134
135
  raise exc
135
136
 
136
137
 
138
+ def echo_safe_format_table(data: Iterable[Any], columns) -> None:
139
+ """
140
+ There is a bug in the humanfriendly library: it breaks to render the small table for small terminals
141
+ (`format_robust_table`) if we call format_smart_table with an empty dataset. This catches the error and prints
142
+ what we would call an empty "robust_table".
143
+ """
144
+ try:
145
+ click.echo(format_table(data, columns))
146
+ except ValueError as exc:
147
+ if str(exc) == "max() arg is an empty sequence":
148
+ click.echo("------------")
149
+ click.echo("Empty")
150
+ click.echo("------------")
151
+ else:
152
+ raise exc
153
+
154
+
137
155
  def normalize_datasource_name(s: str) -> str:
138
156
  s = re.sub(r"[^0-9a-zA-Z_]", "_", s)
139
157
  if s[0] in "0123456789":
@@ -153,7 +171,7 @@ def generate_datafile(
153
171
  if not f.exists() or force:
154
172
  with open(f"{f}", "w") as ds_file:
155
173
  ds_file.write(datafile)
156
- click.echo(FeedbackManager.success(message=f"** Generated {f}"))
174
+ click.echo(FeedbackManager.info_file_created(file=f))
157
175
 
158
176
  if data and (base / "fixtures").exists():
159
177
  # Generating a fixture for Parquet files is not so trivial, since Parquet format
@@ -1090,7 +1108,7 @@ async def push_data(
1090
1108
  cb.First = True # type: ignore[attr-defined]
1091
1109
  cb.prev_done = 0 # type: ignore[attr-defined]
1092
1110
 
1093
- click.echo(FeedbackManager.info_starting_import_process())
1111
+ click.echo(FeedbackManager.gray(message=f"\nImporting data to {datasource_name} Data Source..."))
1094
1112
 
1095
1113
  if isinstance(url, list):
1096
1114
  urls = url
@@ -1161,17 +1179,16 @@ async def push_data(
1161
1179
  except Exception as e:
1162
1180
  raise CLIException(FeedbackManager.error_exception(error=e))
1163
1181
  else:
1164
- click.echo(FeedbackManager.success_progress_blocks())
1165
1182
  if mode == "append" and parser and parser != "clickhouse":
1166
1183
  click.echo(FeedbackManager.success_appended_rows(appended_rows=appended_rows))
1167
1184
 
1168
- click.echo(FeedbackManager.success_total_rows(datasource=datasource_name, total_rows=total_rows))
1169
-
1170
1185
  if mode == "replace":
1171
1186
  click.echo(FeedbackManager.success_replaced_datasource(datasource=datasource_name))
1172
1187
  else:
1173
- click.echo(FeedbackManager.success_appended_datasource(datasource=datasource_name))
1174
- click.echo(FeedbackManager.info_data_pushed(datasource=datasource_name))
1188
+ click.echo(FeedbackManager.highlight(message="» 2.57m rows x 9 cols in 852.04ms"))
1189
+
1190
+ click.echo(FeedbackManager.success_progress_blocks())
1191
+
1175
1192
  finally:
1176
1193
  try:
1177
1194
  for url in urls:
@@ -338,6 +338,14 @@ class CLIConfig:
338
338
  CLIConfig._projects[working_dir] = result
339
339
  return result
340
340
 
341
+ @staticmethod
342
+ def get_llm_config(working_dir: Optional[str] = None) -> Dict[str, Any]:
343
+ return (
344
+ CLIConfig.get_project_config(working_dir)
345
+ .get("llms", {})
346
+ .get("openai", {"model": "gpt-4o-mini", "api_key": None})
347
+ )
348
+
341
349
  @staticmethod
342
350
  def reset() -> None:
343
351
  CLIConfig._global = None
@@ -1,4 +1,3 @@
1
- import json
2
1
  import os
3
2
  from os import getcwd
4
3
  from pathlib import Path
@@ -6,7 +5,6 @@ from typing import Any, Dict, List, Optional
6
5
 
7
6
  import click
8
7
  from click import Context
9
- from openai import OpenAI
10
8
 
11
9
  from tinybird.client import TinyB
12
10
  from tinybird.datafile import folder_build
@@ -16,8 +14,9 @@ from tinybird.tb_cli_modules.common import _generate_datafile, coro, generate_da
16
14
  from tinybird.tb_cli_modules.config import CLIConfig
17
15
  from tinybird.tb_cli_modules.exceptions import CLIDatasourceException
18
16
  from tinybird.tb_cli_modules.llm import LLM
19
- from tinybird.tb_cli_modules.local import get_docker_client, set_up_tinybird_local
20
- from tinybird.tb_cli_modules.prompts import sample_data_sql_prompt
17
+ from tinybird.tb_cli_modules.local import (
18
+ get_tinybird_local_client,
19
+ )
21
20
 
22
21
 
23
22
  @cli.command()
@@ -48,12 +47,13 @@ async def create(
48
47
  folder: Optional[str],
49
48
  ) -> None:
50
49
  """Initialize a new project."""
51
- click.echo(FeedbackManager.highlight(message="Setting up Tinybird development environment..."))
50
+ click.echo(FeedbackManager.gray(message="Setting up Tinybird Local"))
52
51
  folder = folder or getcwd()
53
52
  try:
54
- docker_client = get_docker_client()
55
- tb_client = set_up_tinybird_local(docker_client)
53
+ tb_client = get_tinybird_local_client()
54
+ click.echo(FeedbackManager.gray(message="Creating new project structure..."))
56
55
  await project_create(tb_client, data, prompt, folder)
56
+ click.echo(FeedbackManager.success(message="✓ Scaffolding completed!\n"))
57
57
  workspaces: List[Dict[str, Any]] = (await tb_client.user_workspaces()).get("workspaces", [])
58
58
  datasources = await tb_client.datasources()
59
59
  pipes = await tb_client.pipes(dependencies=True)
@@ -69,11 +69,15 @@ async def create(
69
69
  elif prompt:
70
70
  datasource_files = [f for f in os.listdir(Path(folder) / "datasources") if f.endswith(".datasource")]
71
71
  for datasource_file in datasource_files:
72
- datasource_content = Path(folder) / "datasources" / datasource_file
73
- sample_data = await generate_sample_data_from_columns(tb_client, datasource_content)
74
- ndjson_data = "\n".join([json.dumps(row) for row in sample_data])
75
- await tb_client.datasource_events(datasource_file, ndjson_data)
76
- click.echo(FeedbackManager.success(message="\n✔ Tinybird development environment is ready"))
72
+ datasource_path = Path(folder) / "datasources" / datasource_file
73
+ llm_config = CLIConfig.get_llm_config()
74
+ llm = LLM(key=llm_config["api_key"])
75
+ datasource_name = datasource_path.stem
76
+ datasource_content = datasource_path.read_text()
77
+ has_json_path = "`json:" in datasource_content
78
+ if has_json_path:
79
+ await llm.generate_sql_sample_data(tb_client, datasource_name, datasource_content)
80
+ click.echo(FeedbackManager.success(message="\n✓ Tinybird Local is ready!"))
77
81
  except Exception as e:
78
82
  click.echo(FeedbackManager.error(message=f"Error: {str(e)}"))
79
83
 
@@ -84,7 +88,7 @@ async def project_create(
84
88
  prompt: Optional[str],
85
89
  folder: str,
86
90
  ):
87
- project_paths = ["datasources", "endpoints", "copies", "sinks", "playgrounds", "materializations"]
91
+ project_paths = ["datasources", "endpoints", "materializations", "copies", "sinks"]
88
92
  force = True
89
93
  for x in project_paths:
90
94
  try:
@@ -92,7 +96,7 @@ async def project_create(
92
96
  f.mkdir()
93
97
  click.echo(FeedbackManager.info_path_created(path=x))
94
98
  except FileExistsError:
95
- pass
99
+ click.echo(FeedbackManager.info_path_created(path=x))
96
100
 
97
101
  def generate_pipe_file(name: str, content: str):
98
102
  base = Path("endpoints")
@@ -101,7 +105,7 @@ async def project_create(
101
105
  f = base / (f"{name}.pipe")
102
106
  with open(f"{f}", "w") as file:
103
107
  file.write(content)
104
- click.echo(FeedbackManager.success(message=f"** Generated {f}"))
108
+ click.echo(FeedbackManager.info_file_created(file=f))
105
109
 
106
110
  if data:
107
111
  path = Path(folder) / data
@@ -119,10 +123,8 @@ TYPE ENDPOINT
119
123
  )
120
124
  elif prompt:
121
125
  try:
122
- config = CLIConfig.get_project_config()
123
- model = config.get("llms", {}).get("openai", {}).get("model", "gpt-4o-mini")
124
- api_key = config.get("llms", {}).get("openai", {}).get("api_key", None)
125
- llm = LLM(model=model, key=api_key)
126
+ llm_config = CLIConfig.get_llm_config()
127
+ llm = LLM(key=llm_config["api_key"])
126
128
  result = await llm.create_project(prompt)
127
129
  for ds in result.datasources:
128
130
  content = ds.content.replace("```", "")
@@ -198,29 +200,3 @@ async def append_datasource(
198
200
  ignore_empty=ignore_empty,
199
201
  concurrency=concurrency,
200
202
  )
201
-
202
-
203
- def generate_sql_sample_data(datasource_content: str, row_count: int, model: str, api_key: str) -> str:
204
- client = OpenAI(api_key=api_key)
205
-
206
- response = client.chat.completions.create(
207
- model=model,
208
- messages=[
209
- {"role": "system", "content": sample_data_sql_prompt.format(row_count=row_count)},
210
- {"role": "user", "content": datasource_content},
211
- ],
212
- )
213
-
214
- return response.choices[0].message.content or ""
215
-
216
-
217
- async def generate_sample_data_from_columns(
218
- tb_client: TinyB, datasource_content: str, row_count: int = 20
219
- ) -> List[Dict[str, Any]]:
220
- config = CLIConfig.get_project_config()
221
- model = config.get("llms", {}).get("openai", {}).get("model", "gpt-4o-mini")
222
- api_key = config.get("llms", {}).get("openai", {}).get("api_key", None)
223
- sql = generate_sql_sample_data(datasource_content, row_count, model, api_key)
224
- result = await tb_client.query(f"{sql} FORMAT JSON")
225
- data = result.get("data", [])
226
- return data
@@ -1,9 +1,12 @@
1
- from typing import List, Optional
1
+ import asyncio
2
+ from datetime import datetime
3
+ from typing import Awaitable, Callable, List
2
4
 
3
5
  from openai import OpenAI
4
6
  from pydantic import BaseModel
5
7
 
6
- from tinybird.tb_cli_modules.prompts import create_project_prompt
8
+ from tinybird.client import TinyB
9
+ from tinybird.tb_cli_modules.prompts import create_project_prompt, sample_data_sql_prompt
7
10
 
8
11
 
9
12
  class DataFile(BaseModel):
@@ -17,15 +20,54 @@ class DataProject(BaseModel):
17
20
 
18
21
 
19
22
  class LLM:
20
- def __init__(self, model: Optional[str] = None, key: Optional[str] = None):
21
- self.model = model
22
- self.key = key
23
+ def __init__(self, key: str):
24
+ self.client = OpenAI(api_key=key)
25
+
26
+ async def _execute(self, action_fn: Callable[[], Awaitable[str]], checker_fn: Callable[[str], bool]):
27
+ is_valid = False
28
+ times = 0
29
+
30
+ while not is_valid and times < 5:
31
+ result = await action_fn()
32
+ if asyncio.iscoroutinefunction(checker_fn):
33
+ is_valid = await checker_fn(result)
34
+ else:
35
+ is_valid = checker_fn(result)
36
+ times += 1
37
+
38
+ return result
23
39
 
24
40
  async def create_project(self, prompt: str) -> DataProject:
25
- client = OpenAI(api_key=self.key)
26
- completion = client.beta.chat.completions.parse(
27
- model=self.model,
41
+ completion = self.client.beta.chat.completions.parse(
42
+ model="gpt-4o-mini",
28
43
  messages=[{"role": "system", "content": create_project_prompt}, {"role": "user", "content": prompt}],
29
44
  response_format=DataProject,
30
45
  )
31
46
  return completion.choices[0].message.parsed or DataProject(datasources=[], pipes=[])
47
+
48
+ async def generate_sql_sample_data(
49
+ self, tb_client: TinyB, datasource_content: str, row_count: int = 20, context: str = ""
50
+ ) -> str:
51
+ async def action_fn():
52
+ response = self.client.chat.completions.create(
53
+ model="gpt-4o-mini",
54
+ messages=[
55
+ {
56
+ "role": "system",
57
+ "content": sample_data_sql_prompt.format(
58
+ current_datetime=datetime.now().isoformat(), row_count=row_count, context=context
59
+ ),
60
+ },
61
+ {"role": "user", "content": datasource_content},
62
+ ],
63
+ )
64
+ return response.choices[0].message.content or ""
65
+
66
+ async def checker_fn(sql: str):
67
+ try:
68
+ result = await tb_client.query(f"DESCRIBE ({sql}) FORMAT JSON")
69
+ return len(result.get("data", [])) > 0
70
+ except Exception:
71
+ return False
72
+
73
+ return await self._execute(action_fn, checker_fn)
@@ -1,3 +1,4 @@
1
+ import os
1
2
  import time
2
3
 
3
4
  import click
@@ -5,13 +6,17 @@ import requests
5
6
 
6
7
  import docker
7
8
  from tinybird.feedback_manager import FeedbackManager
8
- from tinybird.tb_cli_modules.common import CLIException
9
+ from tinybird.tb_cli_modules.cli import cli
10
+ from tinybird.tb_cli_modules.common import (
11
+ coro,
12
+ )
9
13
  from tinybird.tb_cli_modules.config import CLIConfig
14
+ from tinybird.tb_cli_modules.exceptions import CLIException
10
15
 
11
16
  # TODO: Use the official Tinybird image once it's available 'tinybirdco/tinybird-local:latest'
12
17
  TB_IMAGE_NAME = "registry.gitlab.com/tinybird/analytics/tinybird-local-jammy-3.11:latest"
13
18
  TB_CONTAINER_NAME = "tinybird-local"
14
- TB_LOCAL_PORT = 80
19
+ TB_LOCAL_PORT = int(os.getenv("TB_LOCAL_PORT", 80))
15
20
  TB_LOCAL_HOST = f"http://localhost:{TB_LOCAL_PORT}"
16
21
 
17
22
 
@@ -19,26 +24,38 @@ def start_tinybird_local(
19
24
  docker_client,
20
25
  ):
21
26
  """Start the Tinybird container."""
27
+ pull_show_prompt = False
28
+ pull_required = False
29
+ try:
30
+ local_image = docker_client.images.get(TB_IMAGE_NAME)
31
+ local_image_id = local_image.attrs["RepoDigests"][0].split("@")[1]
32
+ remote_image = docker_client.images.get_registry_data(TB_IMAGE_NAME)
33
+ pull_show_prompt = local_image_id != remote_image.id
34
+ except Exception:
35
+ pull_show_prompt = False
36
+ pull_required = True
37
+
38
+ if (
39
+ pull_show_prompt
40
+ and click.prompt(FeedbackManager.info(message="** New version detected, download? [y/N]")).lower() == "y"
41
+ ):
42
+ click.echo(FeedbackManager.info(message="** Downloading latest version of Tinybird Local..."))
43
+ pull_required = True
44
+
45
+ if pull_required:
46
+ docker_client.images.pull(TB_IMAGE_NAME, platform="linux/amd64")
47
+
48
+ container = None
22
49
  containers = docker_client.containers.list(all=True, filters={"name": TB_CONTAINER_NAME})
23
50
  if containers:
24
- # Container `start` is idempotent. It's safe to call it even if the container is already running.
25
51
  container = containers[0]
52
+
53
+ if container and not pull_required:
54
+ # Container `start` is idempotent. It's safe to call it even if the container is already running.
26
55
  container.start()
27
56
  else:
28
- pull_required = False
29
- try:
30
- local_image = docker_client.images.get(TB_IMAGE_NAME)
31
- local_image_id = local_image.attrs["RepoDigests"][0].split("@")[1]
32
- remote_image = docker_client.images.get_registry_data(TB_IMAGE_NAME)
33
- pull_required = local_image_id != remote_image.id
34
- except Exception:
35
- pull_required = True
36
-
37
- if pull_required:
38
- click.echo(
39
- FeedbackManager.info(message="** Downloading latest version of Tinybird development environment...")
40
- )
41
- docker_client.images.pull(TB_IMAGE_NAME, platform="linux/amd64")
57
+ if container:
58
+ container.remove(force=True)
42
59
 
43
60
  container = docker_client.containers.run(
44
61
  TB_IMAGE_NAME,
@@ -49,7 +66,7 @@ def start_tinybird_local(
49
66
  platform="linux/amd64",
50
67
  )
51
68
 
52
- click.echo(FeedbackManager.info(message="** Waiting for Tinybird development environment to be ready..."))
69
+ click.echo(FeedbackManager.info(message="** Waiting for Tinybird Local to be ready..."))
53
70
  for attempt in range(10):
54
71
  try:
55
72
  run = container.exec_run("tb --no-version-warning sql 'SELECT 1 AS healthcheck' --format json").output
@@ -60,9 +77,11 @@ def start_tinybird_local(
60
77
  raise RuntimeError("Unexpected response from Tinybird")
61
78
  except Exception:
62
79
  if attempt == 9: # Last attempt
63
- raise CLIException("Tinybird local environment not ready yet. Please try again in a few seconds.")
80
+ raise CLIException("Tinybird local not ready yet. Please try again in a few seconds.")
64
81
  time.sleep(5) # Wait 5 seconds before retrying
65
82
 
83
+ click.echo(FeedbackManager.success(message="✓ All set!\n"))
84
+
66
85
 
67
86
  def get_docker_client():
68
87
  """Check if Docker is installed and running."""
@@ -92,12 +111,6 @@ def remove_tinybird_local(docker_client):
92
111
  pass
93
112
 
94
113
 
95
- def set_up_tinybird_local(docker_client):
96
- """Set up the Tinybird local environment."""
97
- start_tinybird_local(docker_client)
98
- return get_tinybird_local_client()
99
-
100
-
101
114
  def get_tinybird_local_client():
102
115
  """Get a Tinybird client connected to the local environment."""
103
116
  config = CLIConfig.get_project_config()
@@ -106,3 +119,50 @@ def get_tinybird_local_client():
106
119
  config.set_token(token)
107
120
  config.set_host(TB_LOCAL_HOST)
108
121
  return config.get_client(host=TB_LOCAL_HOST, token=token)
122
+
123
+
124
+ @cli.group()
125
+ @click.pass_context
126
+ def local(ctx):
127
+ """Local commands"""
128
+
129
+
130
+ @local.command()
131
+ @coro
132
+ async def stop() -> None:
133
+ """Stop Tinybird development environment"""
134
+ click.echo(FeedbackManager.info(message="Shutting down Tinybird development environment..."))
135
+ docker_client = get_docker_client()
136
+ stop_tinybird_local(docker_client)
137
+ click.echo(FeedbackManager.success(message="Tinybird development environment stopped"))
138
+
139
+
140
+ @local.command()
141
+ @coro
142
+ async def remove() -> None:
143
+ """Remove Tinybird development environment"""
144
+ click.echo(FeedbackManager.info(message="Removing Tinybird development environment..."))
145
+ docker_client = get_docker_client()
146
+ remove_tinybird_local(docker_client)
147
+ click.echo(FeedbackManager.success(message="Tinybird development environment removed"))
148
+
149
+
150
+ @local.command()
151
+ @coro
152
+ async def start() -> None:
153
+ """Start Tinybird development environment"""
154
+ click.echo(FeedbackManager.info(message="Starting Tinybird development environment..."))
155
+ docker_client = get_docker_client()
156
+ start_tinybird_local(docker_client)
157
+ click.echo(FeedbackManager.success(message="Tinybird development environment started"))
158
+
159
+
160
+ @local.command()
161
+ @coro
162
+ async def restart() -> None:
163
+ """Restart Tinybird development environment"""
164
+ click.echo(FeedbackManager.info(message="Restarting Tinybird development environment..."))
165
+ docker_client = get_docker_client()
166
+ remove_tinybird_local(docker_client)
167
+ start_tinybird_local(docker_client)
168
+ click.echo(FeedbackManager.success(message="Tinybird development environment restarted"))
@@ -0,0 +1,53 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ import click
5
+
6
+ from tinybird.feedback_manager import FeedbackManager
7
+ from tinybird.tb_cli_modules.cli import cli
8
+ from tinybird.tb_cli_modules.common import CLIException, coro
9
+ from tinybird.tb_cli_modules.config import CLIConfig
10
+ from tinybird.tb_cli_modules.llm import LLM
11
+ from tinybird.tb_cli_modules.local import get_tinybird_local_client
12
+
13
+
14
+ @cli.command()
15
+ @click.argument("datasource", type=str)
16
+ @click.option("--rows", type=int, default=10, help="Number of events to send")
17
+ @click.option("--context", type=str, default="", help="Extra context to use for data generation")
18
+ @coro
19
+ async def mock(datasource: str, rows: int, context: str) -> None:
20
+ """Load sample data into a datasource.
21
+
22
+ Args:
23
+ ctx: Click context object
24
+ datasource_file: Path to the datasource file to load sample data into
25
+ """
26
+ import llm
27
+
28
+ try:
29
+ datasource_path = Path(datasource)
30
+ datasource_name = datasource
31
+ if datasource_path.suffix == ".datasource":
32
+ datasource_name = datasource_path.stem
33
+ else:
34
+ datasource_path = Path("datasources", f"{datasource}.datasource")
35
+
36
+ datasource_content = datasource_path.read_text()
37
+ llm_config = CLIConfig.get_llm_config()
38
+ 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)
41
+ 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}'")
51
+
52
+ except Exception as e:
53
+ raise CLIException(FeedbackManager.error_exception(error=str(e)))
@@ -130,4 +130,7 @@ FROM numbers({row_count})
130
130
  - Do NOT add a semicolon at the end of the query
131
131
  - Do NOT add any FORMAT at the end of the query, because it will be added later by Tinybird.
132
132
 
133
+ # Extra context:
134
+ {context}
135
+
133
136
  """
@@ -0,0 +1,185 @@
1
+ # Standard library modules.
2
+ import collections
3
+ import re
4
+
5
+ # Modules included in our package.
6
+ from humanfriendly.compat import coerce_string
7
+ from humanfriendly.tables import format_robust_table
8
+ from humanfriendly.terminal import (
9
+ ansi_strip,
10
+ ansi_width,
11
+ ansi_wrap,
12
+ find_terminal_size,
13
+ terminal_supports_colors,
14
+ )
15
+
16
+ from tinybird.feedback_manager import bcolors
17
+
18
+ NUMERIC_DATA_PATTERN = re.compile(r"^\d+(\.\d+)?$")
19
+
20
+
21
+ def format_table(data, columns):
22
+ """
23
+ Render tabular data using the most appropriate representation.
24
+
25
+ :param data: An iterable (e.g. a :func:`tuple` or :class:`list`)
26
+ containing the rows of the table, where each row is an
27
+ iterable containing the columns of the table (strings).
28
+ :param column_names: An iterable of column names (strings).
29
+ :returns: The rendered table (a string).
30
+
31
+ If you want an easy way to render tabular data on a terminal in a human
32
+ friendly format then this function is for you! It works as follows:
33
+
34
+ - If the input data doesn't contain any line breaks the function
35
+ :func:`format_pretty_table()` is used to render a pretty table. If the
36
+ resulting table fits in the terminal without wrapping the rendered pretty
37
+ table is returned.
38
+
39
+ - If the input data does contain line breaks or if a pretty table would
40
+ wrap (given the width of the terminal) then the function
41
+ :func:`format_robust_table()` is used to render a more robust table that
42
+ can deal with data containing line breaks and long text.
43
+ """
44
+ # Normalize the input in case we fall back from a pretty table to a robust
45
+ # table (in which case we'll definitely iterate the input more than once).
46
+ data = [normalize_columns(r) for r in data]
47
+
48
+ column_names = []
49
+ column_types = []
50
+
51
+ for c in columns:
52
+ column_names.append(c["name"])
53
+ column_types.append(c["type"])
54
+
55
+ column_names = normalize_columns(column_names)
56
+ # Make sure the input data doesn't contain any line breaks (because pretty
57
+ # tables break horribly when a column's text contains a line break :-).
58
+ if not any(any("\n" in c for c in r) for r in data):
59
+ # Render a pretty table.
60
+ pretty_table = format_pretty_table(data, column_names, column_types)
61
+ # Check if the pretty table fits in the terminal.
62
+ table_width = max(map(ansi_width, pretty_table.splitlines()))
63
+ num_rows, num_columns = find_terminal_size()
64
+ if table_width <= num_columns:
65
+ # The pretty table fits in the terminal without wrapping!
66
+ return pretty_table
67
+ # Fall back to a robust table when a pretty table won't work.
68
+ return format_robust_table(data, column_names)
69
+
70
+
71
+ def format_pretty_table(data, column_names=None, column_types=None, horizontal_bar="─", vertical_bar=" "):
72
+ """
73
+ Render a table using characters like dashes and vertical bars to emulate borders.
74
+
75
+ :param data: An iterable (e.g. a :func:`tuple` or :class:`list`)
76
+ containing the rows of the table, where each row is an
77
+ iterable containing the columns of the table (strings).
78
+ :param column_names: An iterable of column names (strings).
79
+ :param column_types: An iterable of column types (strings).
80
+ :param horizontal_bar: The character used to represent a horizontal bar (a
81
+ string).
82
+ :param vertical_bar: The character used to represent a vertical bar (a
83
+ string).
84
+ :returns: The rendered table (a string).
85
+
86
+ Here's an example:
87
+
88
+ >>> from humanfriendly.tables import format_pretty_table
89
+ >>> column_names = ['Version', 'Uploaded on', 'Downloads']
90
+ >>> humanfriendly_releases = [
91
+ ... ['1.23', '2015-05-25', '218'],
92
+ ... ['1.23.1', '2015-05-26', '1354'],
93
+ ... ['1.24', '2015-05-26', '223'],
94
+ ... ['1.25', '2015-05-26', '4319'],
95
+ ... ['1.25.1', '2015-06-02', '197'],
96
+ ... ]
97
+ >>> print(format_pretty_table(humanfriendly_releases, column_names))
98
+ -------------------------------------
99
+ | Version | Uploaded on | Downloads |
100
+ -------------------------------------
101
+ | 1.23 | 2015-05-25 | 218 |
102
+ | 1.23.1 | 2015-05-26 | 1354 |
103
+ | 1.24 | 2015-05-26 | 223 |
104
+ | 1.25 | 2015-05-26 | 4319 |
105
+ | 1.25.1 | 2015-06-02 | 197 |
106
+ -------------------------------------
107
+
108
+ Notes about the resulting table:
109
+
110
+ - If a column contains numeric data (integer and/or floating point
111
+ numbers) in all rows (ignoring column names of course) then the content
112
+ of that column is right-aligned, as can be seen in the example above. The
113
+ idea here is to make it easier to compare the numbers in different
114
+ columns to each other.
115
+
116
+ - The column names are highlighted in color so they stand out a bit more
117
+ (see also :data:`.HIGHLIGHT_COLOR`). The following screen shot shows what
118
+ that looks like (my terminals are always set to white text on a black
119
+ background):
120
+
121
+ .. image:: images/pretty-table.png
122
+ """
123
+ # Normalize the input because we'll have to iterate it more than once.
124
+ data = [normalize_columns(r, expandtabs=True) for r in data]
125
+ if column_names is not None:
126
+ column_names = normalize_columns(column_names)
127
+ if column_names:
128
+ if terminal_supports_colors():
129
+ column_names = [highlight_column_name(n) for n in column_names]
130
+ data.insert(0, column_names)
131
+ if column_types is not None:
132
+ column_types = normalize_columns(column_types)
133
+ column_types = [highlight_column_type(t) for t in column_types]
134
+ data.insert(1, column_types)
135
+ # Calculate the maximum width of each column.
136
+ widths = collections.defaultdict(int)
137
+ numeric_data = collections.defaultdict(list)
138
+ for row_index, row in enumerate(data):
139
+ for column_index, column in enumerate(row):
140
+ widths[column_index] = max(widths[column_index], ansi_width(column))
141
+ if not (column_names and row_index <= 1): # Skip both header rows for numeric check
142
+ numeric_data[column_index].append(bool(NUMERIC_DATA_PATTERN.match(ansi_strip(column))))
143
+
144
+ horizontal_bar = highlight_horizontal_bar(horizontal_bar)
145
+ # Create a horizontal bar of dashes as a delimiter.
146
+ line_delimiter = horizontal_bar * (sum(widths.values()) + len(widths) * 3 + 1)
147
+ # Start the table with a vertical bar.
148
+ lines = []
149
+ # Format the rows and columns.
150
+ for row_index, row in enumerate(data):
151
+ line = [vertical_bar]
152
+ for column_index, column in enumerate(row):
153
+ padding = " " * (widths[column_index] - ansi_width(column))
154
+ if all(numeric_data[column_index]):
155
+ line.append(" " + padding + column + " ")
156
+ else:
157
+ line.append(" " + column + padding + " ")
158
+ line.append(vertical_bar)
159
+ lines.append("".join(line))
160
+ if column_names and column_types and row_index > 0 and row_index < len(data) - 1:
161
+ lines.append(line_delimiter)
162
+ # Join the lines, returning a single string.
163
+ return "\n".join(lines)
164
+
165
+
166
+ def normalize_columns(row, expandtabs=False):
167
+ results = []
168
+ for value in row:
169
+ text = coerce_string(value)
170
+ if expandtabs:
171
+ text = text.expandtabs()
172
+ results.append(text)
173
+ return results
174
+
175
+
176
+ def highlight_column_name(name):
177
+ return ansi_wrap(name)
178
+
179
+
180
+ def highlight_column_type(type):
181
+ return f"{bcolors.CGREY}\033[3m{type}\033[23m{bcolors.ENDC}"
182
+
183
+
184
+ def highlight_horizontal_bar(horizontal_bar):
185
+ return f"{bcolors.CGREY}\033[3m{horizontal_bar}\033[23m{bcolors.ENDC}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tinybird
3
- Version: 0.0.1.dev3
3
+ Version: 0.0.1.dev5
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -1,40 +1,42 @@
1
- tinybird/__cli__.py,sha256=bgGjpK7NkAkSj1v-IXZIG9nlQnRLi4faLhm9wfdnePo,250
2
- tinybird/check_pypi.py,sha256=_4NkharLyR_ELrAdit-ftqIWvOf7jZNPt3i76frlo9g,975
1
+ tinybird/__cli__.py,sha256=P6C6n43jJIAsTyrXiOcbX-PiRKUCtqwYqXix7BHTRR8,250
2
+ tinybird/check_pypi.py,sha256=RIhZcTg9dk5SFuvV3yVtFgBael_5_VYl4GzOZJojCfU,971
3
3
  tinybird/client.py,sha256=nd97gD2-8Ap8yDonBcVwk9eXDAL43hmIYdo-Pse43RE,50738
4
4
  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
- tinybird/datafile.py,sha256=JpZX33Ilq71PiuDBs4TNF1xJoUMtTPLynIbHuo7Bn80,253510
7
+ tinybird/datafile.py,sha256=dZIIp-V99yS9bnOpH4yAiohG76qKOcJF2wDj7WroT34,253516
8
8
  tinybird/datatypes.py,sha256=IHyhZ86ib54Vnd1pbod9y2aS8DDvDKZm1HJGlThdbuQ,10460
9
- tinybird/feedback_manager.py,sha256=FT5wJ06gYPGofVgpR-XdZBxUlV9oV94rLx-vzh9ArB8,67724
9
+ tinybird/feedback_manager.py,sha256=qX8yrjoHUL4kjb_IKzLZPPfg4m54XWUZkdVOrv1ktpo,67814
10
10
  tinybird/git_settings.py,sha256=XUL9ZUj59-ZVQJDYmMEq4UpnuuOuQOHGlNcX3JgQHjQ,3954
11
11
  tinybird/sql.py,sha256=gfRKjdqEygcE1WOTeQ1QV2Jal8Jzl4RSX8fftu1KSEs,45825
12
12
  tinybird/sql_template.py,sha256=IqYRfUxDYBCoOYjqqvn--_8QXLv9FSRnJ0bInx7q1Xs,93051
13
13
  tinybird/sql_template_fmt.py,sha256=1z-PuqSZXtzso8Z_mPqUc-NxIxUrNUcVIPezNieZk-M,10196
14
14
  tinybird/sql_toolset.py,sha256=xS_yD5N_TZT5d4uPcXdeIYX4GQPz7-7wHywMGdfgqcM,13794
15
15
  tinybird/syncasync.py,sha256=fAvq0qkRgqXqXMKwbY2iJNYqLT_r6mDsh1MRpGKrdRU,27763
16
- tinybird/tb_cli.py,sha256=cTPJfd_wOWPCwwFz_LoMREV3YSa8pltU_y-ITP13IpY,819
16
+ tinybird/tb_cli.py,sha256=z8TUjtki-2YXj04kry_uKGTRto35qPSibNH1Up6ooRE,855
17
17
  tinybird/tornado_template.py,sha256=o2HguxrL1Evnt8o3IvrsI8Zm6JtRQ3zhLJKf1XyR3SQ,41965
18
18
  tinybird/ch_utils/constants.py,sha256=aYvg2C_WxYWsnqPdZB1ZFoIr8ZY-XjUXYyHKE9Ansj0,3890
19
19
  tinybird/ch_utils/engine.py,sha256=OXkBhlzGjZotjD0vaT-rFIbSGV4tpiHxE8qO_ip0SyQ,40454
20
20
  tinybird/tb_cli_modules/auth.py,sha256=cqxfGgFheuTmenQ3UwPBXTqwMm8JD7uzgLfoIRXdnyQ,9038
21
21
  tinybird/tb_cli_modules/branch.py,sha256=Ik8rRVPXvhyChHBqdPVNtI_C-0gLYjUHaznNWE04Ecw,39120
22
- tinybird/tb_cli_modules/build.py,sha256=pF6Ts2k0qCwCi1ktrGyJo1NcdWsU9O3okDdb3SiFdZo,7615
22
+ tinybird/tb_cli_modules/build.py,sha256=HLcjBiBB2bjWHZK2cbdI_4tCnld_D8dHVrWA0sQEnH0,7803
23
23
  tinybird/tb_cli_modules/cicd.py,sha256=0lMkb6CVOFZl5HOwgY8mK4T4mgI7O8335UngLXtCc-c,13851
24
- tinybird/tb_cli_modules/cli.py,sha256=a738xz4DUNep_mzHTEUExd0NAuf7cHkUAlMsEus9xaM,65072
25
- tinybird/tb_cli_modules/common.py,sha256=pS7kisPe4vChirSY2og5e0Cu-RNDow7X3Qdk1BEHL78,78571
26
- tinybird/tb_cli_modules/config.py,sha256=6NTgIdwf0X132A1j6G_YrdPep87ymZ9b5pABabKLzh4,11484
24
+ tinybird/tb_cli_modules/cli.py,sha256=jBex1ocNkth1REPHxvvyUaPKj5CTkqg8TLeVFtAw5B8,61973
25
+ tinybird/tb_cli_modules/common.py,sha256=JfQp02DZFtlMoJyfaXkRAGBetadPqda2x-C1kvWC87U,79110
26
+ tinybird/tb_cli_modules/config.py,sha256=ppWvACHrSLkb5hOoQLYNby2w8jR76-8Kx2NBCst7ntQ,11760
27
27
  tinybird/tb_cli_modules/connection.py,sha256=YggP34Qh3hCjD41lp1ZHVCwHPjI_-um2spvEV8F_tSU,28684
28
- tinybird/tb_cli_modules/create.py,sha256=wcr-GIgCz_t135UIDFt_gNxUQ0huI3oXGM5bIuC665E,7808
28
+ tinybird/tb_cli_modules/create.py,sha256=glQiRQEVGTOw9VmoICEF5_7YWuraJrk3BSUDSTSUfP0,6796
29
29
  tinybird/tb_cli_modules/datasource.py,sha256=BVYwPkKvdnEv2YMkxof7n7FOffyJm-QK7Pk9Ui-MC3A,35816
30
30
  tinybird/tb_cli_modules/exceptions.py,sha256=pmucP4kTF4irIt7dXiG-FcnI-o3mvDusPmch1L8RCWk,3367
31
31
  tinybird/tb_cli_modules/fmt.py,sha256=_xL2AyGo3us7NpmfAXtSIhn1cGAC-A4PspdhUjm58jY,3382
32
32
  tinybird/tb_cli_modules/job.py,sha256=AG69LPb9MbobA1awwJFZJvxqarDKfRlsBjw2V1zvYqc,2964
33
- tinybird/tb_cli_modules/llm.py,sha256=RyUBtyLJcG5DFBjkalFLgwwX-415_Uom8Hq_oKiiyBs,904
34
- tinybird/tb_cli_modules/local.py,sha256=uamCRYoU0MhCaPUXtvCFfbzHJGwTaPMTTHRQyw0Da3k,3870
33
+ tinybird/tb_cli_modules/llm.py,sha256=U6GaLVi4tOq39W60A0Rjfr9JOkGcJoTfeWLixNA06zY,2450
34
+ tinybird/tb_cli_modules/local.py,sha256=cHT8xtF4aYsR7kLKT6hqzhass3W1dl-VCYavJphmnUM,5761
35
+ tinybird/tb_cli_modules/mock.py,sha256=JGB-Hid1qWYuUk-F7NR3tltDSVf5mz6tOEPYXVhPaFs,2089
35
36
  tinybird/tb_cli_modules/pipe.py,sha256=BCLAQ3ZuWKGAih2mupnw_Y2S5B5cNS-epF317whsSEE,30989
36
- tinybird/tb_cli_modules/prompts.py,sha256=Esjhet-KUM1bT6RStk2voCCVjvAOUD4hDDiKpn-AYNY,7227
37
+ tinybird/tb_cli_modules/prompts.py,sha256=12Abh1kZItxMa7r3WbTi_TgTYtlV30HcaG_JiodafIA,7255
37
38
  tinybird/tb_cli_modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
39
+ tinybird/tb_cli_modules/table.py,sha256=hG-PRDVuFp2uph41WpoLRV1yjp3RI2fi_iGGiI0rdxU,7695
38
40
  tinybird/tb_cli_modules/tag.py,sha256=9YHnruPnUNp1IJUe4qcSEMg9EbquIRo--Nxcsbvkvq8,3488
39
41
  tinybird/tb_cli_modules/telemetry.py,sha256=iEGnMuCuNhvF6ln__j6X9MSTwL_0Hm-GgFHHHvhfknk,10466
40
42
  tinybird/tb_cli_modules/test.py,sha256=Vf8oK96V81HdKGsT79y6MUz6oz_VrYIwTbRnzzJs4rQ,4350
@@ -43,8 +45,8 @@ tinybird/tb_cli_modules/workspace.py,sha256=N8f1dl4BYc34ucmJ6oNZc4XZMGKd8m0Fhq7o
43
45
  tinybird/tb_cli_modules/workspace_members.py,sha256=ksXsjd233y9-sNlz4Qb-meZbX4zn1B84e_bSm2i8rhg,8731
44
46
  tinybird/tb_cli_modules/tinyunit/tinyunit.py,sha256=_ydOD4WxmKy7_h-d3fG62w_0_lD0uLl9w4EbEYtwd0U,11720
45
47
  tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py,sha256=hGh1ZaXC1af7rKnX7222urkj0QJMhMWclqMy59dOqwE,1922
46
- tinybird-0.0.1.dev3.dist-info/METADATA,sha256=6J__4w3BceyDNH4mybjldWRwFGgNaDLSyvugF9Se_E4,2404
47
- tinybird-0.0.1.dev3.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
48
- tinybird-0.0.1.dev3.dist-info/entry_points.txt,sha256=PKPKuPmA4IfJYnCFHHUiw-aAWZuBomFvwCklv1OyCjE,43
49
- tinybird-0.0.1.dev3.dist-info/top_level.txt,sha256=pgw6AzERHBcW3YTi2PW4arjxLkulk2msOz_SomfOEuc,45
50
- tinybird-0.0.1.dev3.dist-info/RECORD,,
48
+ tinybird-0.0.1.dev5.dist-info/METADATA,sha256=0262NudObi6F7EKjaFVPfSl5_ZQWPd8r4OYG-4TMsMg,2404
49
+ tinybird-0.0.1.dev5.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
50
+ tinybird-0.0.1.dev5.dist-info/entry_points.txt,sha256=PKPKuPmA4IfJYnCFHHUiw-aAWZuBomFvwCklv1OyCjE,43
51
+ tinybird-0.0.1.dev5.dist-info/top_level.txt,sha256=pgw6AzERHBcW3YTi2PW4arjxLkulk2msOz_SomfOEuc,45
52
+ tinybird-0.0.1.dev5.dist-info/RECORD,,