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 +2 -2
- tinybird/check_pypi.py +1 -1
- tinybird/datafile.py +1 -1
- tinybird/feedback_manager.py +11 -6
- tinybird/tb_cli.py +1 -0
- tinybird/tb_cli_modules/build.py +50 -51
- tinybird/tb_cli_modules/cli.py +2 -69
- tinybird/tb_cli_modules/common.py +24 -7
- tinybird/tb_cli_modules/config.py +8 -0
- tinybird/tb_cli_modules/create.py +21 -45
- tinybird/tb_cli_modules/llm.py +50 -8
- tinybird/tb_cli_modules/local.py +85 -25
- tinybird/tb_cli_modules/mock.py +53 -0
- tinybird/tb_cli_modules/prompts.py +3 -0
- tinybird/tb_cli_modules/table.py +185 -0
- {tinybird-0.0.1.dev3.dist-info → tinybird-0.0.1.dev5.dist-info}/METADATA +1 -1
- {tinybird-0.0.1.dev3.dist-info → tinybird-0.0.1.dev5.dist-info}/RECORD +20 -18
- {tinybird-0.0.1.dev3.dist-info → tinybird-0.0.1.dev5.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev3.dist-info → tinybird-0.0.1.dev5.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev3.dist-info → tinybird-0.0.1.dev5.dist-info}/top_level.txt +0 -0
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.
|
|
8
|
-
__revision__ = '
|
|
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
|
|
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
|
|
tinybird/feedback_manager.py
CHANGED
|
@@ -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
|
|
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
|
|
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("
|
|
716
|
-
info_file_created = info_message("
|
|
717
|
-
info_path_already_exists = info_message("
|
|
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("
|
|
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
|
tinybird/tb_cli_modules/build.py
CHANGED
|
@@ -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,
|
|
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(
|
|
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(
|
|
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
|
-
"--
|
|
102
|
+
"--skip-datasources",
|
|
104
103
|
is_flag=True,
|
|
105
|
-
help="
|
|
104
|
+
help="Skip rebuilding datasources.",
|
|
106
105
|
)
|
|
107
106
|
@coro
|
|
108
107
|
async def build(
|
|
109
108
|
folder: str,
|
|
110
109
|
watch: bool,
|
|
111
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
186
|
+
limit = 5
|
|
187
|
+
for d in res["data"][:5]:
|
|
198
188
|
data.append(d.values())
|
|
199
189
|
meta = res["meta"]
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
tinybird/tb_cli_modules/cli.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
1174
|
-
|
|
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
|
|
20
|
-
|
|
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.
|
|
50
|
+
click.echo(FeedbackManager.gray(message="Setting up Tinybird Local"))
|
|
52
51
|
folder = folder or getcwd()
|
|
53
52
|
try:
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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", "
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
123
|
-
|
|
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
|
tinybird/tb_cli_modules/llm.py
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
|
|
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.
|
|
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,
|
|
21
|
-
self.
|
|
22
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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)
|
tinybird/tb_cli_modules/local.py
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
29
|
-
|
|
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
|
|
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
|
|
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)))
|
|
@@ -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,40 +1,42 @@
|
|
|
1
|
-
tinybird/__cli__.py,sha256=
|
|
2
|
-
tinybird/check_pypi.py,sha256=
|
|
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=
|
|
7
|
+
tinybird/datafile.py,sha256=dZIIp-V99yS9bnOpH4yAiohG76qKOcJF2wDj7WroT34,253516
|
|
8
8
|
tinybird/datatypes.py,sha256=IHyhZ86ib54Vnd1pbod9y2aS8DDvDKZm1HJGlThdbuQ,10460
|
|
9
|
-
tinybird/feedback_manager.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
25
|
-
tinybird/tb_cli_modules/common.py,sha256=
|
|
26
|
-
tinybird/tb_cli_modules/config.py,sha256=
|
|
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=
|
|
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=
|
|
34
|
-
tinybird/tb_cli_modules/local.py,sha256=
|
|
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=
|
|
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.
|
|
47
|
-
tinybird-0.0.1.
|
|
48
|
-
tinybird-0.0.1.
|
|
49
|
-
tinybird-0.0.1.
|
|
50
|
-
tinybird-0.0.1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|