tinybird 0.0.1.dev15__py3-none-any.whl → 0.0.1.dev16__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/client.py CHANGED
@@ -188,7 +188,9 @@ class TinyB:
188
188
  if response.status_code == 599:
189
189
  raise TimeoutException("timeout")
190
190
  if "Content-Type" in response.headers and (
191
- response.headers["Content-Type"] == "text/plain" or "text/csv" in response.headers["Content-Type"]
191
+ response.headers["Content-Type"] == "text/plain"
192
+ or "text/csv" in response.headers["Content-Type"]
193
+ or "ndjson" in response.headers["Content-Type"]
192
194
  ):
193
195
  return response.content.decode("utf-8")
194
196
  if response.status_code >= 400 and response.status_code not in [400, 403, 404, 409, 429]:
@@ -1,5 +1,6 @@
1
1
  from collections import namedtuple
2
2
  from typing import Any, Callable
3
+ import os
3
4
 
4
5
  FeedbackMessage = namedtuple("FeedbackMessage", "message")
5
6
 
@@ -26,6 +27,16 @@ def print_message(message: str, color: str = bcolors.ENDC) -> Callable[..., str]
26
27
  def error_message(message: str) -> Callable[..., str]:
27
28
  return print_message(f"\n** {message}", bcolors.FAIL)
28
29
 
30
+ def error_exception(message: Exception) -> Callable[..., str]:
31
+ def formatter(**kwargs: Any) -> str:
32
+ color = bcolors.FAIL
33
+ if isinstance(kwargs.get('error', ''), Exception) and os.environ.get('TB_DEBUG', '') != '':
34
+ import traceback
35
+ stack_trace = f"Traceback:\n{''.join(traceback.format_tb(kwargs['error'].__traceback__))}"
36
+ return f"{color}{message.format(**kwargs)}{stack_trace}{bcolors.ENDC}"
37
+ return f"{color}{message.format(**kwargs)}{bcolors.ENDC}"
38
+ return formatter
39
+
29
40
 
30
41
  def simple_error_message(message: str) -> Callable[..., str]:
31
42
  return print_message(f"{message}", bcolors.FAIL)
@@ -56,7 +67,7 @@ def gray_message(message: str) -> Callable[..., str]:
56
67
 
57
68
 
58
69
  class FeedbackManager:
59
- error_exception = error_message("{error}")
70
+ error_exception = error_exception("{error}")
60
71
  simple_error_exception = simple_error_message("{error}")
61
72
  error_exception_trace = error_message("{error}\n** Trace:\n{trace}")
62
73
  error_notoken = error_message(
@@ -76,68 +76,7 @@ SQL >
76
76
  </instructions>
77
77
  """
78
78
 
79
- create_test_calls_prompt = """
80
- You are a Tinybird expert. You will be given a pipe endpoint containing different nodes with SQL and Tinybird templating syntax. You will generate URLs to test it with different parameters combinations.
81
-
82
- <test>
83
- <test_1>:
84
- name: <test_name_1>
85
- description: <description_1>
86
- parameters: <url_encoded_parameters_1>
87
- <test_2>:
88
- name: <test_name_2>
89
- description: <description_2>
90
- parameters: <url_encoded_parameters_2>
91
- </test>
92
- <instructions>
93
- - The test name must be unique.
94
- - The test command must be a valid Tinybird command that can be run in the terminal.
95
- - The test command can have as many parameters as are needed to test the pipe.
96
- - The parameter within Tinybird templating syntax looks like this one {{String(my_param_name, default_value)}}.
97
- - If there are no parameters in the , you can omit parametrs and generate a single test command.
98
- - Extra context: {context}
99
- </instructions>
100
- """
101
-
102
- sample_data_prompt = """Your role is to generate sample data for a given data source. A data source is a table in Tinybird. You will be given a schema for the data source, and you will need to generate a set of rows that fit that schema.
103
-
104
- The schema will be provided to you at the end of this prompt in the following format:
105
-
106
- ```
107
- SCHEMA >
108
- `span_id` String `json:$.span_id`,
109
- `operation_id` String `json:$.operation_name`,
110
- `duration` Nullable(Float32) `json:$.duration`,
111
- ```
112
-
113
- In the example above, `span_id` are column names, String and Float32 are column types, and `json:$.duration` is a JSON path expression that points to the duration value in the JSON data. Use this information to generate valid data.
114
-
115
- You will generate {row_count} rows of data in JSON format, newlines separated. Your reponse will only contain the {row_count} rows of data, without any other text. Do not format the data in any way, and do not place it inside any markers for a code block of any kind. Most importantly, do not use backticks on the first or last line or anywhere else in the response. This is extremely important, ensure that your response doesn't include any backticks.
116
-
117
- Remember that for a JSON to be valid, there can't be a trailing comma after the last value in an object.
118
-
119
- Make sure to include all columns in the schema, and use valid JSON. You may leave some columns empty only if the column type allows it (e.g. Nullable(String)). Use realistic values for the data, and make sure to include a variety of values in each column.
120
-
121
- For columns that represent timestamps, use values that are close to the current date and time. The current date and time is {current_datetime}"""
122
-
123
- sample_data_with_errors_prompt = """Your role is to generate incorrect sample data for a given data source. A data source is a table in Tinybird. You will be given a schema for the data source, and you will need to generate a set of rows that mostly fit that schema, but with some errors.
124
-
125
- The schema will be provided to you at the end of this prompt in the following format:
126
-
127
- ```
128
- SCHEMA >
129
- `span_id` String `json:$.span_id`,
130
- `operation_id` String `json:$.operation_name`,
131
- `duration` Nullable(Float32) `json:$.duration`,
132
- ```
133
-
134
- In the example above, `span_id` are column names, String and Float32 are column types, and `json:$.duration` is a JSON path expression that points to the duration value in the JSON data. Use this information to generate valid data.
135
-
136
- You will generate {row_count} rows of data in JSON format, newlines separated. Your reponse will be a single string containing all {row_count} rows, without any other text. Make sure to include all columns in the schema, and use valid JSON. Use realistic values for the data, and make sure to include a variety of values in each column. In every set of {row_count} rows, there should be at least one row that has an error. The error could be in any column, and it could be a type error (e.g. a string where a number is expected), or it could be an error in the structure of the JSON (e.g. a value in the wrong place in the document).
137
-
138
- For columns that represent timestamps, use values that are close to the current date and time. The current date and time is {current_datetime}"""
139
-
140
- sample_data_sql_prompt = """
79
+ generate_sql_mock_data_prompt = """
141
80
  Given the schema for a Tinybird datasource, return a can you create a clickhouse sql query to generate some random data that matches that schema.
142
81
 
143
82
  Response format MUST be just a valid clickhouse sql query.
@@ -164,13 +103,13 @@ SELECT
164
103
  rand() % 50 AS pvp_kills, -- Random PvP kills between 0 and 49
165
104
  rand() % 200 AS quest_completions, -- Random quest completions between 0 and 199
166
105
  now() - rand() % 86400 AS timestamp -- Random timestamp within the last day
167
- FROM numbers({row_count})
106
+ FROM numbers({rows})
168
107
 
169
108
  # Instructions:
170
109
 
171
110
  - The query MUST return a random sample of data that matches the schema.
172
111
  - The query MUST return a valid clickhouse sql query.
173
- - The query MUST return a sample of EXACTLY {row_count} rows.
112
+ - The query MUST return a sample of EXACTLY {rows} rows.
174
113
  - The query MUST be valid for clickhouse and Tinybird.
175
114
  - Return JUST the sql query, without any other text or symbols.
176
115
  - Do NOT include ```clickhouse or ```sql or any other wrapping text.
@@ -182,3 +121,26 @@ FROM numbers({row_count})
182
121
  {context}
183
122
 
184
123
  """
124
+
125
+ create_test_calls_prompt = """
126
+ You are a Tinybird expert. You will be given a pipe endpoint containing different nodes with SQL and Tinybird templating syntax. You will generate URLs to test it with different parameters combinations.
127
+
128
+ <test>
129
+ <test_1>:
130
+ name: <test_name_1>
131
+ description: <description_1>
132
+ parameters: <url_encoded_parameters_1>
133
+ <test_2>:
134
+ name: <test_name_2>
135
+ description: <description_2>
136
+ parameters: <url_encoded_parameters_2>
137
+ </test>
138
+ <instructions>
139
+ - The test name must be unique.
140
+ - The test command must be a valid Tinybird command that can be run in the terminal.
141
+ - The test command can have as many parameters as are needed to test the pipe.
142
+ - The parameter within Tinybird templating syntax looks like this one {{String(my_param_name, default_value)}}.
143
+ - If there are no parameters in the , you can omit parametrs and generate a single test command.
144
+ - Extra context: {context}
145
+ </instructions>
146
+ """
tinybird/tb/cli.py CHANGED
@@ -14,10 +14,12 @@ import tinybird.tb.modules.create
14
14
  import tinybird.tb.modules.datasource
15
15
  import tinybird.tb.modules.fmt
16
16
  import tinybird.tb.modules.job
17
+ import tinybird.tb.modules.local
17
18
  import tinybird.tb.modules.login
18
19
  import tinybird.tb.modules.mock
19
20
  import tinybird.tb.modules.pipe
20
21
  import tinybird.tb.modules.tag
22
+ import tinybird.tb.modules.test
21
23
  import tinybird.tb.modules.token
22
24
  import tinybird.tb.modules.workspace
23
25
  import tinybird.tb.modules.workspace_members
@@ -1,26 +1,20 @@
1
1
  import asyncio
2
- import cmd
3
2
  import os
4
- import random
5
- import subprocess
6
- import sys
7
3
  import threading
8
4
  import time
9
5
  from pathlib import Path
10
6
  from typing import Any, Awaitable, Callable, List, Union
11
7
 
12
8
  import click
13
- import humanfriendly
14
9
  from click import Context
15
10
  from watchdog.events import FileSystemEventHandler
16
11
  from watchdog.observers import Observer
17
12
 
18
13
  import tinybird.context as context
19
- from tinybird.syncasync import async_to_sync
20
- from tinybird.client import TinyB, AuthNoTokenException
21
- from tinybird.tb.modules.common import CLIException
14
+ from tinybird.client import TinyB
22
15
  from tinybird.config import FeatureFlags
23
- from tinybird.feedback_manager import FeedbackManager, bcolors
16
+ from tinybird.feedback_manager import FeedbackManager
17
+ from tinybird.tb.modules.build_shell import BuildShell, print_table_formatted
24
18
  from tinybird.tb.modules.cli import cli
25
19
  from tinybird.tb.modules.common import coro, push_data
26
20
  from tinybird.tb.modules.datafile.build import folder_build
@@ -29,48 +23,7 @@ from tinybird.tb.modules.datafile.exceptions import ParseException
29
23
  from tinybird.tb.modules.datafile.fixture import build_fixture_name, get_fixture_dir
30
24
  from tinybird.tb.modules.datafile.parse_datasource import parse_datasource
31
25
  from tinybird.tb.modules.datafile.parse_pipe import parse_pipe
32
- from tinybird.tb.modules.local import get_tinybird_local_client
33
- from tinybird.tb.modules.table import format_table
34
-
35
-
36
- class BuildShell(cmd.Cmd):
37
- prompt = "\n\001\033[1;32m\002TB > \001\033[0m\002"
38
-
39
- def __init__(self, folder: str, client: TinyB):
40
- super().__init__()
41
- self.folder = folder
42
- self.client = client
43
- def do_exit(self, arg):
44
- sys.exit(0)
45
-
46
- def do_quit(self, arg):
47
- sys.exit(0)
48
-
49
- def default(self, argline):
50
- click.echo("")
51
- if argline.startswith("tb build"):
52
- click.echo(FeedbackManager.error(message="Build command is already running"))
53
- else:
54
- arg_stripped = argline.strip().lower()
55
- if not arg_stripped:
56
- return
57
- if arg_stripped.startswith("tb"):
58
- extra_args = f" --folder {self.folder}" if arg_stripped.startswith("tb mock") else ""
59
- subprocess.run(arg_stripped + extra_args, shell=True, text=True)
60
- elif arg_stripped.startswith("with") or arg_stripped.startswith("select"):
61
- try:
62
- run_sql(self.client, argline)
63
- except Exception as e:
64
- click.echo(FeedbackManager.error(message=str(e)))
65
-
66
- elif arg_stripped.startswith("mock "):
67
- subprocess.run(f"tb {arg_stripped} --folder {self.folder}", shell=True, text=True)
68
- else:
69
- click.echo(FeedbackManager.error(message="Invalid command"))
70
-
71
- def reprint_prompt(self):
72
- self.stdout.write(self.prompt)
73
- self.stdout.flush()
26
+ from tinybird.tb.modules.local_common import get_tinybird_local_client
74
27
 
75
28
 
76
29
  class FileChangeHandler(FileSystemEventHandler):
@@ -131,44 +84,6 @@ def watch_files(
131
84
  observer.join()
132
85
 
133
86
 
134
-
135
- def run_sql(client: TinyB, query, rows_limit=20):
136
- try:
137
- q = query.strip()
138
- if q.startswith("insert"):
139
- click.echo(FeedbackManager.info_append_data())
140
- raise CLIException(FeedbackManager.error_invalid_query())
141
- if q.startswith("delete"):
142
- raise CLIException(FeedbackManager.error_invalid_query())
143
-
144
- # fuck my life
145
- def run_query_in_thread():
146
- loop = asyncio.new_event_loop()
147
- asyncio.set_event_loop(loop)
148
- try:
149
- return loop.run_until_complete(client.query(f"SELECT * FROM ({query}) LIMIT {rows_limit} FORMAT JSON"))
150
- finally:
151
- loop.close()
152
-
153
- # Run the query in a separate thread
154
- import concurrent.futures
155
- with concurrent.futures.ThreadPoolExecutor() as executor:
156
- res = executor.submit(run_query_in_thread).result()
157
-
158
- except AuthNoTokenException:
159
- raise
160
- except Exception as e:
161
- raise CLIException(FeedbackManager.error_exception(error=str(e)))
162
-
163
- if isinstance(res, dict) and "error" in res:
164
- raise CLIException(FeedbackManager.error_exception(error=res["error"]))
165
-
166
- if isinstance(res, dict) and "data" in res and res["data"]:
167
- print_table_formatted(res, 'QUERY')
168
- else:
169
- click.echo(FeedbackManager.info_no_rows())
170
-
171
-
172
87
  @cli.command()
173
88
  @click.option(
174
89
  "--folder",
@@ -286,46 +201,6 @@ async def build_and_print_resource(tb_client: TinyB, filename: str):
286
201
  res = await tb_client.query(f"SELECT * FROM {name} FORMAT JSON", pipeline=pipeline)
287
202
  print_table_formatted(res, name)
288
203
 
289
- def print_table_formatted(res: dict, name: str):
290
- rebuild_colors = [bcolors.FAIL, bcolors.OKBLUE, bcolors.WARNING, bcolors.OKGREEN, bcolors.HEADER]
291
- rebuild_index = random.randint(0, len(rebuild_colors) - 1)
292
- rebuild_color = rebuild_colors[rebuild_index % len(rebuild_colors)]
293
- data = []
294
- limit = 5
295
- for d in res["data"][:5]:
296
- data.append(d.values())
297
- meta = res["meta"]
298
- row_count = res.get("rows", 0)
299
- stats = res.get("statistics", {})
300
- elapsed = stats.get("elapsed", 0)
301
- cols = len(meta)
302
- try:
303
-
304
- def print_message(message: str, color=bcolors.CGREY):
305
- return f"{color}{message}{bcolors.ENDC}"
306
-
307
- table = format_table(data, meta)
308
- colored_char = print_message("│", rebuild_color)
309
- table_with_marker = "\n".join(f"{colored_char} {line}" for line in table.split("\n"))
310
- click.echo(f"\n{colored_char} {print_message('⚡', rebuild_color)} Running {name}")
311
- click.echo(colored_char)
312
- click.echo(table_with_marker)
313
- click.echo(colored_char)
314
- rows_read = humanfriendly.format_number(stats.get("rows_read", 0))
315
- bytes_read = humanfriendly.format_size(stats.get("bytes_read", 0))
316
- elapsed = humanfriendly.format_timespan(elapsed) if elapsed >= 1 else f"{elapsed * 1000:.2f}ms"
317
- stats_message = f"» {bytes_read} ({rows_read} rows x {cols} cols) in {elapsed}"
318
- rows_message = f"» Showing {limit} first rows" if row_count > limit else "» Showing all rows"
319
- click.echo(f"{colored_char} {print_message(stats_message, bcolors.OKGREEN)}")
320
- click.echo(f"{colored_char} {print_message(rows_message, bcolors.CGREY)}")
321
- except ValueError as exc:
322
- if str(exc) == "max() arg is an empty sequence":
323
- click.echo("------------")
324
- click.echo("Empty")
325
- click.echo("------------")
326
- else:
327
- raise exc
328
-
329
204
 
330
205
  async def append_datasource(
331
206
  ctx: click.Context,
@@ -0,0 +1,133 @@
1
+ import asyncio
2
+ import cmd
3
+ import random
4
+ import subprocess
5
+ import sys
6
+
7
+ import click
8
+ import humanfriendly
9
+
10
+ from tinybird.client import TinyB
11
+ from tinybird.feedback_manager import FeedbackManager, bcolors
12
+ from tinybird.tb.modules.exceptions import CLIException
13
+ from tinybird.tb.modules.table import format_table
14
+
15
+
16
+ class BuildShell(cmd.Cmd):
17
+ prompt = "\n\001\033[1;32m\002TB > \001\033[0m\002"
18
+
19
+ def __init__(self, folder: str, client: TinyB):
20
+ super().__init__()
21
+ self.folder = folder
22
+ self.client = client
23
+
24
+ def do_exit(self, arg):
25
+ sys.exit(0)
26
+
27
+ def do_quit(self, arg):
28
+ sys.exit(0)
29
+
30
+ def default(self, argline):
31
+ click.echo("")
32
+ if argline.startswith("tb build"):
33
+ click.echo(FeedbackManager.error(message="Build command is already running"))
34
+ else:
35
+ arg_stripped = argline.strip().lower()
36
+ if not arg_stripped:
37
+ return
38
+ if arg_stripped.startswith("tb"):
39
+ arg_stripped = arg_stripped.replace("tb", "tb --local")
40
+ extra_args = f" --folder {self.folder}" if arg_stripped.startswith("tb mock") else ""
41
+ subprocess.run(arg_stripped + extra_args, shell=True, text=True)
42
+ elif arg_stripped.startswith("with") or arg_stripped.startswith("select"):
43
+ try:
44
+ self.run_sql(self.client, argline)
45
+ except Exception as e:
46
+ click.echo(FeedbackManager.error(message=str(e)))
47
+
48
+ elif arg_stripped.startswith("mock "):
49
+ subprocess.run(f"tb {arg_stripped} --folder {self.folder}", shell=True, text=True)
50
+ else:
51
+ click.echo(FeedbackManager.error(message="Invalid command"))
52
+
53
+ def reprint_prompt(self):
54
+ self.stdout.write(self.prompt)
55
+ self.stdout.flush()
56
+
57
+ def run_sql(self, query, rows_limit=20):
58
+ try:
59
+ q = query.strip()
60
+ if q.startswith("insert"):
61
+ click.echo(FeedbackManager.info_append_data())
62
+ raise CLIException(FeedbackManager.error_invalid_query())
63
+ if q.startswith("delete"):
64
+ raise CLIException(FeedbackManager.error_invalid_query())
65
+
66
+ # fuck my life
67
+ def run_query_in_thread():
68
+ loop = asyncio.new_event_loop()
69
+ asyncio.set_event_loop(loop)
70
+ try:
71
+ return loop.run_until_complete(
72
+ self.client.query(f"SELECT * FROM ({query}) LIMIT {rows_limit} FORMAT JSON")
73
+ )
74
+ finally:
75
+ loop.close()
76
+
77
+ # Run the query in a separate thread
78
+ import concurrent.futures
79
+
80
+ with concurrent.futures.ThreadPoolExecutor() as executor:
81
+ res = executor.submit(run_query_in_thread).result()
82
+
83
+ except Exception as e:
84
+ raise CLIException(FeedbackManager.error_exception(error=str(e)))
85
+
86
+ if isinstance(res, dict) and "error" in res:
87
+ raise CLIException(FeedbackManager.error_exception(error=res["error"]))
88
+
89
+ if isinstance(res, dict) and "data" in res and res["data"]:
90
+ print_table_formatted(res, "QUERY")
91
+ else:
92
+ click.echo(FeedbackManager.info_no_rows())
93
+
94
+
95
+ def print_table_formatted(res: dict, name: str):
96
+ rebuild_colors = [bcolors.FAIL, bcolors.OKBLUE, bcolors.WARNING, bcolors.OKGREEN, bcolors.HEADER]
97
+ rebuild_index = random.randint(0, len(rebuild_colors) - 1)
98
+ rebuild_color = rebuild_colors[rebuild_index % len(rebuild_colors)]
99
+ data = []
100
+ limit = 5
101
+ for d in res["data"][:5]:
102
+ data.append(d.values())
103
+ meta = res["meta"]
104
+ row_count = res.get("rows", 0)
105
+ stats = res.get("statistics", {})
106
+ elapsed = stats.get("elapsed", 0)
107
+ cols = len(meta)
108
+ try:
109
+
110
+ def print_message(message: str, color=bcolors.CGREY):
111
+ return f"{color}{message}{bcolors.ENDC}"
112
+
113
+ table = format_table(data, meta)
114
+ colored_char = print_message("│", rebuild_color)
115
+ table_with_marker = "\n".join(f"{colored_char} {line}" for line in table.split("\n"))
116
+ click.echo(f"\n{colored_char} {print_message('⚡', rebuild_color)} Running {name}")
117
+ click.echo(colored_char)
118
+ click.echo(table_with_marker)
119
+ click.echo(colored_char)
120
+ rows_read = humanfriendly.format_number(stats.get("rows_read", 0))
121
+ bytes_read = humanfriendly.format_size(stats.get("bytes_read", 0))
122
+ elapsed = humanfriendly.format_timespan(elapsed) if elapsed >= 1 else f"{elapsed * 1000:.2f}ms"
123
+ stats_message = f"» {bytes_read} ({rows_read} rows x {cols} cols) in {elapsed}"
124
+ rows_message = f"» Showing {limit} first rows" if row_count > limit else "» Showing all rows"
125
+ click.echo(f"{colored_char} {print_message(stats_message, bcolors.OKGREEN)}")
126
+ click.echo(f"{colored_char} {print_message(rows_message, bcolors.CGREY)}")
127
+ except ValueError as exc:
128
+ if str(exc) == "max() arg is an empty sequence":
129
+ click.echo("------------")
130
+ click.echo("Empty")
131
+ click.echo("------------")
132
+ else:
133
+ raise exc
@@ -63,6 +63,7 @@ from tinybird.tb.modules.datafile.exceptions import (
63
63
  from tinybird.tb.modules.datafile.parse_datasource import parse_datasource
64
64
  from tinybird.tb.modules.datafile.parse_pipe import parse_pipe
65
65
  from tinybird.tb.modules.datafile.pull import folder_pull
66
+ from tinybird.tb.modules.local_common import get_tinybird_local_client
66
67
  from tinybird.tb.modules.telemetry import add_telemetry_event
67
68
 
68
69
  __old_click_echo = click.echo
@@ -108,6 +109,7 @@ DEFAULT_PATTERNS: List[Tuple[str, Union[str, Callable[[str], str]]]] = [
108
109
  "--with-headers", help="Flag to enable connector to export with headers", is_flag=True, default=False, hidden=True
109
110
  )
110
111
  @click.option("--show-tokens", is_flag=True, default=False, help="Enable the output of tokens")
112
+ @click.option("--local", is_flag=True, default=False, help="Run in local mode")
111
113
  @click.version_option(version=VERSION)
112
114
  @click.pass_context
113
115
  @coro
@@ -131,6 +133,7 @@ async def cli(
131
133
  sf_stage,
132
134
  with_headers: bool,
133
135
  show_tokens: bool,
136
+ local: bool,
134
137
  ) -> None:
135
138
  """
136
139
  Use `OBFUSCATE_REGEX_PATTERN` and `OBFUSCATE_PATTERN_SEPARATOR` environment variables to define a regex pattern and a separator (in case of a single string with multiple regex) to obfuscate secrets in the CLI output.
@@ -228,7 +231,11 @@ async def cli(
228
231
 
229
232
  logging.debug("debug enabled")
230
233
 
231
- ctx.ensure_object(dict)["client"] = _get_tb_client(config.get("token", None), config["host"], semver)
234
+ ctx.ensure_object(dict)["client"] = (
235
+ await get_tinybird_local_client()
236
+ if local
237
+ else _get_tb_client(config.get("token", None), config["host"], semver)
238
+ )
232
239
 
233
240
  for connector in SUPPORTED_CONNECTORS:
234
241
  load_connector_config(ctx, connector, debug, check_uninstalled=True)
@@ -19,6 +19,11 @@ from tinybird.tb.modules.llm import LLM
19
19
 
20
20
 
21
21
  @cli.command()
22
+ @click.option(
23
+ "--demo",
24
+ is_flag=True,
25
+ help="Demo data and files to get started",
26
+ )
22
27
  @click.option(
23
28
  "--data",
24
29
  type=click.Path(exists=True),
@@ -42,6 +47,7 @@ from tinybird.tb.modules.llm import LLM
42
47
  @coro
43
48
  async def create(
44
49
  ctx: Context,
50
+ demo: bool,
45
51
  data: Optional[str],
46
52
  prompt: Optional[str],
47
53
  folder: Optional[str],
@@ -63,7 +69,240 @@ async def create(
63
69
 
64
70
  click.echo(FeedbackManager.gray(message="Building fixtures..."))
65
71
 
66
- if data:
72
+ if demo:
73
+ # Users datasource
74
+ ds_name = "users"
75
+ datasource_path = Path(folder) / "datasources" / f"{ds_name}.datasource"
76
+ datasource_content = """SCHEMA >
77
+ `id` String `json:$.id`,
78
+ `organization_id` String `json:$.organization_id`,
79
+ `tier` String `json:$.tier`
80
+
81
+ ENGINE "MergeTree"
82
+ ENGINE_SORTING_KEY "id, organization_id"
83
+ """
84
+ datasource_path.write_text(datasource_content)
85
+ click.echo(FeedbackManager.info(message=f"✓ /datasources/{ds_name}.datasource"))
86
+
87
+ # Events datasource
88
+ ds_name = "events"
89
+ datasource_path = Path(folder) / "datasources" / f"{ds_name}.datasource"
90
+ datasource_content = """SCHEMA >
91
+ `request_id` String `json:$.request_id`,
92
+ `timestamp` DateTime64(3) `json:$.timestamp`,
93
+ `user_id` String `json:$.user_id`,
94
+ `type` String `json:$.request.type`,
95
+ `endpoint` String `json:$.request.endpoint`,
96
+ `model` String `json:$.request.model`,
97
+ `request_options_temperature` String `json:$.request.options.temperature`,
98
+ `request_options_max_tokens` Int32 `json:$.request.options.max_tokens`,
99
+ `request_options_stream` Boolean `json:$.request.options.stream`,
100
+ `usage_prompt_tokens` Int32 `json:$.usage.prompt_tokens`,
101
+ `usage_completion_tokens` Int32 `json:$.usage.completion_tokens`,
102
+ `usage_total_tokens` Int32 `json:$.usage.total_tokens`
103
+
104
+ ENGINE "MergeTree"
105
+ ENGINE_SORTING_KEY "user_id, timestamp"
106
+ """
107
+ datasource_path.write_text(datasource_content)
108
+ click.echo(FeedbackManager.info(message=f"✓ /datasources/{ds_name}.datasource"))
109
+
110
+ ds_name = "users"
111
+ ds_content = """
112
+ {"id":"usr_1","organization_id":"org_202","tier":"pro"}
113
+ {"id":"usr_2","organization_id":"org_49","tier":"enterprise"}
114
+ {"id":"usr_3","organization_id":"org_181","tier":"pro"}
115
+ {"id":"usr_4","organization_id":"org_194","tier":"pro"}
116
+ {"id":"usr_5","organization_id":"org_180","tier":"enterprise"}
117
+ {"id":"usr_6","organization_id":"org_112","tier":"enterprise"}
118
+ {"id":"usr_7","organization_id":"org_291","tier":"pro"}
119
+ {"id":"usr_8","organization_id":"org_150","tier":"free"}
120
+ {"id":"usr_9","organization_id":"org_127","tier":"enterprise"}
121
+ {"id":"usr_10","organization_id":"org_279","tier":"enterprise"}
122
+ {"id":"usr_11","organization_id":"org_58","tier":"pro"}
123
+ {"id":"usr_12","organization_id":"org_183","tier":"pro"}
124
+ {"id":"usr_13","organization_id":"org_2","tier":"pro"}
125
+ {"id":"usr_14","organization_id":"org_15","tier":"free"}
126
+ {"id":"usr_15","organization_id":"org_54","tier":"pro"}
127
+ {"id":"usr_16","organization_id":"org_24","tier":"pro"}
128
+ {"id":"usr_17","organization_id":"org_135","tier":"free"}
129
+ {"id":"usr_18","organization_id":"org_76","tier":"enterprise"}
130
+ {"id":"usr_19","organization_id":"org_166","tier":"enterprise"}
131
+ {"id":"usr_20","organization_id":"org_203","tier":"pro"}
132
+ {"id":"usr_21","organization_id":"org_103","tier":"pro"}
133
+ {"id":"usr_22","organization_id":"org_178","tier":"pro"}
134
+ {"id":"usr_23","organization_id":"org_41","tier":"pro"}
135
+ {"id":"usr_24","organization_id":"org_197","tier":"enterprise"}
136
+ {"id":"usr_25","organization_id":"org_21","tier":"pro"}
137
+ {"id":"usr_26","organization_id":"org_180","tier":"enterprise"}
138
+ {"id":"usr_27","organization_id":"org_216","tier":"enterprise"}
139
+ {"id":"usr_28","organization_id":"org_117","tier":"enterprise"}
140
+ {"id":"usr_29","organization_id":"org_73","tier":"pro"}
141
+ {"id":"usr_30","organization_id":"org_92","tier":"enterprise"}
142
+ {"id":"usr_31","organization_id":"org_272","tier":"enterprise"}
143
+ {"id":"usr_32","organization_id":"org_58","tier":"pro"}
144
+ {"id":"usr_33","organization_id":"org_158","tier":"pro"}
145
+ {"id":"usr_34","organization_id":"org_12","tier":"free"}
146
+ {"id":"usr_35","organization_id":"org_119","tier":"free"}
147
+ {"id":"usr_36","organization_id":"org_272","tier":"pro"}
148
+ {"id":"usr_37","organization_id":"org_1","tier":"enterprise"}
149
+ {"id":"usr_38","organization_id":"org_262","tier":"enterprise"}
150
+ {"id":"usr_39","organization_id":"org_189","tier":"pro"}
151
+ {"id":"usr_40","organization_id":"org_126","tier":"free"}
152
+ {"id":"usr_41","organization_id":"org_94","tier":"free"}
153
+ {"id":"usr_42","organization_id":"org_156","tier":"free"}
154
+ {"id":"usr_43","organization_id":"org_155","tier":"enterprise"}
155
+ {"id":"usr_44","organization_id":"org_151","tier":"pro"}
156
+ {"id":"usr_45","organization_id":"org_217","tier":"pro"}
157
+ {"id":"usr_46","organization_id":"org_111","tier":"enterprise"}
158
+ {"id":"usr_47","organization_id":"org_274","tier":"enterprise"}
159
+ {"id":"usr_48","organization_id":"org_117","tier":"enterprise"}
160
+ {"id":"usr_49","organization_id":"org_125","tier":"pro"}
161
+ {"id":"usr_50","organization_id":"org_42","tier":"pro"}
162
+ """
163
+ persist_fixture(f"{ds_name}", ds_content)
164
+ click.echo(FeedbackManager.info(message=f"✓ /fixtures/{ds_name}"))
165
+
166
+ ds_name = "events"
167
+ ds_content = """
168
+ {"request_id":"req_1","timestamp":"2024-11-14T12:11:14.896Z","user_id":"usr_42","request":{"type":"chat","endpoint":"/v1/completions","model":"gpt-4","options":{"temperature":"0.8","max_tokens":1582,"stream":false}},"usage":{"prompt_tokens":499,"completion_tokens":123,"total_tokens":622}}
169
+ {"request_id":"req_2","timestamp":"2024-10-06T10:32:09.164Z","user_id":"usr_20","request":{"type":"chat","endpoint":"/v1/images/generations","model":"gpt-3.5-turbo","options":{"temperature":"0.9","max_tokens":650,"stream":true}},"usage":{"prompt_tokens":384,"completion_tokens":103,"total_tokens":487}}
170
+ {"request_id":"req_3","timestamp":"2024-11-19T03:49:26.569Z","user_id":"usr_27","request":{"type":"chat","endpoint":"/v1/images/generations","model":"gpt-4","options":{"temperature":"0.3","max_tokens":764,"stream":true}},"usage":{"prompt_tokens":63,"completion_tokens":284,"total_tokens":347}}
171
+ {"request_id":"req_4","timestamp":"2024-11-09T09:13:05.359Z","user_id":"usr_8","request":{"type":"chat","endpoint":"/v1/completions","model":"claude-3-sonnet","options":{"temperature":"0.8","max_tokens":1790,"stream":true}},"usage":{"prompt_tokens":405,"completion_tokens":231,"total_tokens":636}}
172
+ {"request_id":"req_5","timestamp":"2024-11-24T17:48:03.640Z","user_id":"usr_45","request":{"type":"chat","endpoint":"/v1/images/generations","model":"claude-3-sonnet","options":{"temperature":"1.0","max_tokens":1701,"stream":false}},"usage":{"prompt_tokens":342,"completion_tokens":258,"total_tokens":600}}
173
+ {"request_id":"req_6","timestamp":"2024-11-10T02:07:53.241Z","user_id":"usr_49","request":{"type":"chat","endpoint":"/v1/completions","model":"gpt-3.5-turbo","options":{"temperature":"0.4","max_tokens":1049,"stream":false}},"usage":{"prompt_tokens":120,"completion_tokens":165,"total_tokens":285}}
174
+ {"request_id":"req_7","timestamp":"2024-10-14T06:43:14.759Z","user_id":"usr_48","request":{"type":"chat","endpoint":"/v1/chat/completions","model":"gpt-4","options":{"temperature":"0.9","max_tokens":1548,"stream":true}},"usage":{"prompt_tokens":61,"completion_tokens":104,"total_tokens":165}}
175
+ {"request_id":"req_8","timestamp":"2024-11-21T20:52:03.378Z","user_id":"usr_30","request":{"type":"chat","endpoint":"/v1/chat/completions","model":"gpt-3.5-turbo","options":{"temperature":"0.5","max_tokens":1742,"stream":true}},"usage":{"prompt_tokens":326,"completion_tokens":236,"total_tokens":562}}
176
+ {"request_id":"req_9","timestamp":"2024-10-24T22:23:11.463Z","user_id":"usr_34","request":{"type":"chat","endpoint":"/v1/images/generations","model":"gpt-4","options":{"temperature":"0.5","max_tokens":748,"stream":true}},"usage":{"prompt_tokens":488,"completion_tokens":148,"total_tokens":636}}
177
+ {"request_id":"req_10","timestamp":"2024-11-13T05:42:38.971Z","user_id":"usr_24","request":{"type":"chat","endpoint":"/v1/completions","model":"claude-3-sonnet","options":{"temperature":"0.3","max_tokens":1729,"stream":true}},"usage":{"prompt_tokens":262,"completion_tokens":162,"total_tokens":424}}
178
+ {"request_id":"req_11","timestamp":"2024-10-21T07:20:51.693Z","user_id":"usr_1","request":{"type":"chat","endpoint":"/v1/chat/completions","model":"claude-3-opus","options":{"temperature":"0.3","max_tokens":1811,"stream":false}},"usage":{"prompt_tokens":342,"completion_tokens":274,"total_tokens":616}}
179
+ {"request_id":"req_12","timestamp":"2024-11-15T06:03:07.775Z","user_id":"usr_21","request":{"type":"chat","endpoint":"/v1/chat/completions","model":"gpt-4","options":{"temperature":"0.3","max_tokens":1484,"stream":true}},"usage":{"prompt_tokens":273,"completion_tokens":266,"total_tokens":539}}
180
+ {"request_id":"req_13","timestamp":"2024-11-29T18:31:23.125Z","user_id":"usr_41","request":{"type":"chat","endpoint":"/v1/images/generations","model":"claude-3-sonnet","options":{"temperature":"0.8","max_tokens":1812,"stream":true}},"usage":{"prompt_tokens":265,"completion_tokens":102,"total_tokens":367}}
181
+ {"request_id":"req_14","timestamp":"2024-11-13T23:50:03.888Z","user_id":"usr_13","request":{"type":"chat","endpoint":"/v1/chat/completions","model":"gpt-3.5-turbo","options":{"temperature":"0.9","max_tokens":1694,"stream":false}},"usage":{"prompt_tokens":76,"completion_tokens":290,"total_tokens":366}}
182
+ {"request_id":"req_15","timestamp":"2024-11-01T14:27:22.401Z","user_id":"usr_32","request":{"type":"chat","endpoint":"/v1/chat/completions","model":"claude-3-sonnet","options":{"temperature":"0.4","max_tokens":949,"stream":true}},"usage":{"prompt_tokens":105,"completion_tokens":197,"total_tokens":302}}
183
+ {"request_id":"req_16","timestamp":"2024-11-12T09:13:06.447Z","user_id":"usr_2","request":{"type":"chat","endpoint":"/v1/images/generations","model":"gpt-4","options":{"temperature":"0.1","max_tokens":651,"stream":false}},"usage":{"prompt_tokens":348,"completion_tokens":80,"total_tokens":428}}
184
+ {"request_id":"req_17","timestamp":"2024-11-09T21:09:51.044Z","user_id":"usr_24","request":{"type":"chat","endpoint":"/v1/completions","model":"claude-3-sonnet","options":{"temperature":"0.4","max_tokens":692,"stream":false}},"usage":{"prompt_tokens":195,"completion_tokens":178,"total_tokens":373}}
185
+ {"request_id":"req_18","timestamp":"2024-11-06T13:38:08.873Z","user_id":"usr_49","request":{"type":"chat","endpoint":"/v1/chat/completions","model":"claude-3-opus","options":{"temperature":"0.4","max_tokens":1303,"stream":true}},"usage":{"prompt_tokens":304,"completion_tokens":73,"total_tokens":377}}
186
+ {"request_id":"req_19","timestamp":"2024-11-06T20:47:35.718Z","user_id":"usr_33","request":{"type":"chat","endpoint":"/v1/images/generations","model":"claude-3-opus","options":{"temperature":"0.8","max_tokens":1359,"stream":true}},"usage":{"prompt_tokens":55,"completion_tokens":258,"total_tokens":313}}
187
+ {"request_id":"req_20","timestamp":"2024-11-15T09:59:27.956Z","user_id":"usr_9","request":{"type":"chat","endpoint":"/v1/completions","model":"claude-3-opus","options":{"temperature":"0.6","max_tokens":1768,"stream":true}},"usage":{"prompt_tokens":310,"completion_tokens":62,"total_tokens":372}}
188
+ {"request_id":"req_21","timestamp":"2024-11-17T03:46:10.219Z","user_id":"usr_6","request":{"type":"chat","endpoint":"/v1/images/generations","model":"claude-3-opus","options":{"temperature":"0.8","max_tokens":1967,"stream":true}},"usage":{"prompt_tokens":87,"completion_tokens":275,"total_tokens":362}}
189
+ {"request_id":"req_22","timestamp":"2024-10-03T06:52:21.902Z","user_id":"usr_13","request":{"type":"chat","endpoint":"/v1/completions","model":"gpt-3.5-turbo","options":{"temperature":"0.8","max_tokens":1338,"stream":true}},"usage":{"prompt_tokens":332,"completion_tokens":137,"total_tokens":469}}
190
+ {"request_id":"req_23","timestamp":"2024-11-10T02:53:06.630Z","user_id":"usr_16","request":{"type":"chat","endpoint":"/v1/chat/completions","model":"claude-3-opus","options":{"temperature":"0.1","max_tokens":1958,"stream":true}},"usage":{"prompt_tokens":444,"completion_tokens":294,"total_tokens":738}}
191
+ {"request_id":"req_24","timestamp":"2024-10-10T04:11:45.410Z","user_id":"usr_44","request":{"type":"chat","endpoint":"/v1/images/generations","model":"gpt-3.5-turbo","options":{"temperature":"1.0","max_tokens":1241,"stream":false}},"usage":{"prompt_tokens":414,"completion_tokens":227,"total_tokens":641}}
192
+ {"request_id":"req_25","timestamp":"2024-10-15T09:53:09.379Z","user_id":"usr_24","request":{"type":"chat","endpoint":"/v1/images/generations","model":"gpt-4","options":{"temperature":"0.4","max_tokens":1015,"stream":true}},"usage":{"prompt_tokens":433,"completion_tokens":119,"total_tokens":552}}
193
+ {"request_id":"req_26","timestamp":"2024-11-16T16:40:38.042Z","user_id":"usr_8","request":{"type":"chat","endpoint":"/v1/images/generations","model":"gpt-3.5-turbo","options":{"temperature":"0.3","max_tokens":514,"stream":true}},"usage":{"prompt_tokens":86,"completion_tokens":241,"total_tokens":327}}
194
+ {"request_id":"req_27","timestamp":"2024-10-06T11:34:58.073Z","user_id":"usr_40","request":{"type":"chat","endpoint":"/v1/completions","model":"gpt-3.5-turbo","options":{"temperature":"0.7","max_tokens":1752,"stream":true}},"usage":{"prompt_tokens":312,"completion_tokens":262,"total_tokens":574}}
195
+ {"request_id":"req_28","timestamp":"2024-10-21T05:35:11.046Z","user_id":"usr_22","request":{"type":"chat","endpoint":"/v1/completions","model":"claude-3-sonnet","options":{"temperature":"0.5","max_tokens":1425,"stream":true}},"usage":{"prompt_tokens":50,"completion_tokens":182,"total_tokens":232}}
196
+ {"request_id":"req_29","timestamp":"2024-10-11T06:14:52.973Z","user_id":"usr_4","request":{"type":"chat","endpoint":"/v1/chat/completions","model":"claude-3-sonnet","options":{"temperature":"0.4","max_tokens":1660,"stream":true}},"usage":{"prompt_tokens":384,"completion_tokens":124,"total_tokens":508}}
197
+ {"request_id":"req_30","timestamp":"2024-11-13T10:48:53.729Z","user_id":"usr_22","request":{"type":"chat","endpoint":"/v1/completions","model":"gpt-4","options":{"temperature":"0.4","max_tokens":1425,"stream":true}},"usage":{"prompt_tokens":299,"completion_tokens":57,"total_tokens":356}}
198
+ {"request_id":"req_31","timestamp":"2024-10-01T03:26:07.133Z","user_id":"usr_41","request":{"type":"chat","endpoint":"/v1/images/generations","model":"gpt-3.5-turbo","options":{"temperature":"0.8","max_tokens":1975,"stream":true}},"usage":{"prompt_tokens":471,"completion_tokens":245,"total_tokens":716}}
199
+ {"request_id":"req_32","timestamp":"2024-11-26T12:13:53.221Z","user_id":"usr_33","request":{"type":"chat","endpoint":"/v1/chat/completions","model":"claude-3-opus","options":{"temperature":"0.5","max_tokens":557,"stream":false}},"usage":{"prompt_tokens":75,"completion_tokens":92,"total_tokens":167}}
200
+ {"request_id":"req_33","timestamp":"2024-10-14T07:41:11.783Z","user_id":"usr_18","request":{"type":"chat","endpoint":"/v1/completions","model":"claude-3-opus","options":{"temperature":"1.0","max_tokens":843,"stream":false}},"usage":{"prompt_tokens":321,"completion_tokens":258,"total_tokens":579}}
201
+ {"request_id":"req_34","timestamp":"2024-10-21T11:20:47.679Z","user_id":"usr_37","request":{"type":"chat","endpoint":"/v1/completions","model":"gpt-3.5-turbo","options":{"temperature":"0.7","max_tokens":1148,"stream":false}},"usage":{"prompt_tokens":425,"completion_tokens":245,"total_tokens":670}}
202
+ {"request_id":"req_35","timestamp":"2024-11-18T12:02:12.268Z","user_id":"usr_38","request":{"type":"chat","endpoint":"/v1/images/generations","model":"claude-3-opus","options":{"temperature":"0.2","max_tokens":1987,"stream":false}},"usage":{"prompt_tokens":50,"completion_tokens":223,"total_tokens":273}}
203
+ {"request_id":"req_36","timestamp":"2024-10-03T02:17:56.042Z","user_id":"usr_38","request":{"type":"chat","endpoint":"/v1/completions","model":"gpt-3.5-turbo","options":{"temperature":"0.2","max_tokens":1339,"stream":false}},"usage":{"prompt_tokens":102,"completion_tokens":112,"total_tokens":214}}
204
+ {"request_id":"req_37","timestamp":"2024-11-02T19:52:17.904Z","user_id":"usr_34","request":{"type":"chat","endpoint":"/v1/images/generations","model":"claude-3-sonnet","options":{"temperature":"0.3","max_tokens":1050,"stream":false}},"usage":{"prompt_tokens":485,"completion_tokens":189,"total_tokens":674}}
205
+ {"request_id":"req_38","timestamp":"2024-11-17T22:26:34.434Z","user_id":"usr_5","request":{"type":"chat","endpoint":"/v1/images/generations","model":"claude-3-sonnet","options":{"temperature":"0.6","max_tokens":857,"stream":false}},"usage":{"prompt_tokens":442,"completion_tokens":224,"total_tokens":666}}
206
+ {"request_id":"req_39","timestamp":"2024-11-17T01:20:40.223Z","user_id":"usr_36","request":{"type":"chat","endpoint":"/v1/completions","model":"claude-3-sonnet","options":{"temperature":"0.9","max_tokens":698,"stream":true}},"usage":{"prompt_tokens":430,"completion_tokens":185,"total_tokens":615}}
207
+ {"request_id":"req_40","timestamp":"2024-11-20T21:38:04.662Z","user_id":"usr_31","request":{"type":"chat","endpoint":"/v1/completions","model":"claude-3-sonnet","options":{"temperature":"0.3","max_tokens":1226,"stream":true}},"usage":{"prompt_tokens":201,"completion_tokens":65,"total_tokens":266}}
208
+ {"request_id":"req_41","timestamp":"2024-11-07T11:20:55.217Z","user_id":"usr_34","request":{"type":"chat","endpoint":"/v1/images/generations","model":"claude-3-sonnet","options":{"temperature":"0.1","max_tokens":1893,"stream":false}},"usage":{"prompt_tokens":307,"completion_tokens":224,"total_tokens":531}}
209
+ {"request_id":"req_42","timestamp":"2024-10-30T23:13:57.704Z","user_id":"usr_44","request":{"type":"chat","endpoint":"/v1/images/generations","model":"claude-3-opus","options":{"temperature":"0.3","max_tokens":888,"stream":false}},"usage":{"prompt_tokens":212,"completion_tokens":53,"total_tokens":265}}
210
+ {"request_id":"req_43","timestamp":"2024-11-09T02:04:23.904Z","user_id":"usr_28","request":{"type":"chat","endpoint":"/v1/images/generations","model":"claude-3-sonnet","options":{"temperature":"0.3","max_tokens":1574,"stream":true}},"usage":{"prompt_tokens":161,"completion_tokens":190,"total_tokens":351}}
211
+ {"request_id":"req_44","timestamp":"2024-11-20T21:28:23.931Z","user_id":"usr_22","request":{"type":"chat","endpoint":"/v1/completions","model":"claude-3-opus","options":{"temperature":"0.4","max_tokens":578,"stream":true}},"usage":{"prompt_tokens":465,"completion_tokens":294,"total_tokens":759}}
212
+ {"request_id":"req_45","timestamp":"2024-11-15T14:02:11.552Z","user_id":"usr_2","request":{"type":"chat","endpoint":"/v1/completions","model":"claude-3-opus","options":{"temperature":"0.4","max_tokens":502,"stream":true}},"usage":{"prompt_tokens":489,"completion_tokens":288,"total_tokens":777}}
213
+ {"request_id":"req_46","timestamp":"2024-10-22T23:05:29.902Z","user_id":"usr_15","request":{"type":"chat","endpoint":"/v1/chat/completions","model":"gpt-4","options":{"temperature":"0.5","max_tokens":1280,"stream":true}},"usage":{"prompt_tokens":462,"completion_tokens":283,"total_tokens":745}}
214
+ {"request_id":"req_47","timestamp":"2024-10-28T01:16:36.936Z","user_id":"usr_28","request":{"type":"chat","endpoint":"/v1/images/generations","model":"gpt-3.5-turbo","options":{"temperature":"0.4","max_tokens":1513,"stream":true}},"usage":{"prompt_tokens":421,"completion_tokens":204,"total_tokens":625}}
215
+ {"request_id":"req_48","timestamp":"2024-11-20T18:18:38.744Z","user_id":"usr_4","request":{"type":"chat","endpoint":"/v1/chat/completions","model":"gpt-3.5-turbo","options":{"temperature":"0.8","max_tokens":1194,"stream":true}},"usage":{"prompt_tokens":251,"completion_tokens":109,"total_tokens":360}}
216
+ {"request_id":"req_49","timestamp":"2024-10-12T17:21:20.482Z","user_id":"usr_11","request":{"type":"chat","endpoint":"/v1/chat/completions","model":"gpt-3.5-turbo","options":{"temperature":"0.3","max_tokens":621,"stream":false}},"usage":{"prompt_tokens":195,"completion_tokens":253,"total_tokens":448}}
217
+ {"request_id":"req_50","timestamp":"2024-11-27T14:14:23.737Z","user_id":"usr_8","request":{"type":"chat","endpoint":"/v1/chat/completions","model":"gpt-4","options":{"temperature":"0.0","max_tokens":1468,"stream":true}},"usage":{"prompt_tokens":461,"completion_tokens":100,"total_tokens":561}}
218
+ {"request_id":"req_51","timestamp":"2024-10-15T14:58:09.623Z","user_id":"usr_48","request":{"type":"chat","endpoint":"/v1/images/generations","model":"claude-3-opus","options":{"temperature":"1.0","max_tokens":1879,"stream":false}},"usage":{"prompt_tokens":51,"completion_tokens":215,"total_tokens":266}}
219
+ {"request_id":"req_52","timestamp":"2024-11-18T00:55:43.893Z","user_id":"usr_48","request":{"type":"chat","endpoint":"/v1/images/generations","model":"claude-3-sonnet","options":{"temperature":"0.2","max_tokens":549,"stream":false}},"usage":{"prompt_tokens":481,"completion_tokens":142,"total_tokens":623}}
220
+ {"request_id":"req_53","timestamp":"2024-11-01T03:22:01.890Z","user_id":"usr_14","request":{"type":"chat","endpoint":"/v1/completions","model":"gpt-3.5-turbo","options":{"temperature":"0.8","max_tokens":1771,"stream":false}},"usage":{"prompt_tokens":459,"completion_tokens":79,"total_tokens":538}}
221
+ {"request_id":"req_54","timestamp":"2024-10-26T08:57:25.867Z","user_id":"usr_17","request":{"type":"chat","endpoint":"/v1/images/generations","model":"claude-3-opus","options":{"temperature":"0.4","max_tokens":1888,"stream":false}},"usage":{"prompt_tokens":219,"completion_tokens":226,"total_tokens":445}}
222
+ {"request_id":"req_55","timestamp":"2024-11-17T16:12:47.424Z","user_id":"usr_43","request":{"type":"chat","endpoint":"/v1/images/generations","model":"gpt-4","options":{"temperature":"0.0","max_tokens":1281,"stream":false}},"usage":{"prompt_tokens":426,"completion_tokens":244,"total_tokens":670}}
223
+ {"request_id":"req_56","timestamp":"2024-10-10T01:03:29.669Z","user_id":"usr_6","request":{"type":"chat","endpoint":"/v1/completions","model":"gpt-3.5-turbo","options":{"temperature":"0.3","max_tokens":1004,"stream":false}},"usage":{"prompt_tokens":420,"completion_tokens":69,"total_tokens":489}}
224
+ {"request_id":"req_57","timestamp":"2024-10-10T19:18:37.937Z","user_id":"usr_30","request":{"type":"chat","endpoint":"/v1/images/generations","model":"gpt-4","options":{"temperature":"0.7","max_tokens":1209,"stream":true}},"usage":{"prompt_tokens":59,"completion_tokens":66,"total_tokens":125}}
225
+ {"request_id":"req_58","timestamp":"2024-10-07T19:20:21.754Z","user_id":"usr_4","request":{"type":"chat","endpoint":"/v1/completions","model":"gpt-3.5-turbo","options":{"temperature":"0.5","max_tokens":1215,"stream":true}},"usage":{"prompt_tokens":197,"completion_tokens":201,"total_tokens":398}}
226
+ {"request_id":"req_59","timestamp":"2024-10-12T07:54:12.535Z","user_id":"usr_35","request":{"type":"chat","endpoint":"/v1/completions","model":"gpt-4","options":{"temperature":"0.1","max_tokens":1445,"stream":false}},"usage":{"prompt_tokens":318,"completion_tokens":111,"total_tokens":429}}
227
+ {"request_id":"req_60","timestamp":"2024-11-04T00:11:56.488Z","user_id":"usr_3","request":{"type":"chat","endpoint":"/v1/images/generations","model":"gpt-3.5-turbo","options":{"temperature":"1.0","max_tokens":1127,"stream":true}},"usage":{"prompt_tokens":286,"completion_tokens":293,"total_tokens":579}}
228
+ {"request_id":"req_61","timestamp":"2024-10-15T10:29:51.122Z","user_id":"usr_36","request":{"type":"chat","endpoint":"/v1/completions","model":"gpt-3.5-turbo","options":{"temperature":"0.9","max_tokens":1810,"stream":false}},"usage":{"prompt_tokens":308,"completion_tokens":238,"total_tokens":546}}
229
+ {"request_id":"req_62","timestamp":"2024-10-15T07:46:39.995Z","user_id":"usr_36","request":{"type":"chat","endpoint":"/v1/completions","model":"gpt-3.5-turbo","options":{"temperature":"0.8","max_tokens":736,"stream":true}},"usage":{"prompt_tokens":102,"completion_tokens":194,"total_tokens":296}}
230
+ {"request_id":"req_63","timestamp":"2024-10-25T11:39:09.912Z","user_id":"usr_37","request":{"type":"chat","endpoint":"/v1/chat/completions","model":"gpt-3.5-turbo","options":{"temperature":"0.5","max_tokens":1112,"stream":true}},"usage":{"prompt_tokens":211,"completion_tokens":139,"total_tokens":350}}
231
+ {"request_id":"req_64","timestamp":"2024-10-20T16:35:00.170Z","user_id":"usr_32","request":{"type":"chat","endpoint":"/v1/chat/completions","model":"gpt-4","options":{"temperature":"0.2","max_tokens":535,"stream":true}},"usage":{"prompt_tokens":355,"completion_tokens":116,"total_tokens":471}}
232
+ {"request_id":"req_65","timestamp":"2024-11-27T04:31:51.172Z","user_id":"usr_44","request":{"type":"chat","endpoint":"/v1/images/generations","model":"gpt-3.5-turbo","options":{"temperature":"0.3","max_tokens":838,"stream":true}},"usage":{"prompt_tokens":468,"completion_tokens":298,"total_tokens":766}}
233
+ {"request_id":"req_66","timestamp":"2024-10-26T17:30:31.696Z","user_id":"usr_12","request":{"type":"chat","endpoint":"/v1/chat/completions","model":"claude-3-sonnet","options":{"temperature":"0.7","max_tokens":505,"stream":true}},"usage":{"prompt_tokens":190,"completion_tokens":207,"total_tokens":397}}
234
+ {"request_id":"req_67","timestamp":"2024-10-04T02:09:21.280Z","user_id":"usr_31","request":{"type":"chat","endpoint":"/v1/images/generations","model":"gpt-4","options":{"temperature":"1.0","max_tokens":886,"stream":false}},"usage":{"prompt_tokens":264,"completion_tokens":60,"total_tokens":324}}
235
+ {"request_id":"req_68","timestamp":"2024-11-14T09:34:25.291Z","user_id":"usr_17","request":{"type":"chat","endpoint":"/v1/images/generations","model":"gpt-3.5-turbo","options":{"temperature":"0.5","max_tokens":1794,"stream":true}},"usage":{"prompt_tokens":166,"completion_tokens":133,"total_tokens":299}}
236
+ {"request_id":"req_69","timestamp":"2024-10-13T20:06:12.700Z","user_id":"usr_18","request":{"type":"chat","endpoint":"/v1/completions","model":"gpt-3.5-turbo","options":{"temperature":"0.6","max_tokens":555,"stream":false}},"usage":{"prompt_tokens":247,"completion_tokens":149,"total_tokens":396}}
237
+ {"request_id":"req_70","timestamp":"2024-10-05T01:45:08.193Z","user_id":"usr_31","request":{"type":"chat","endpoint":"/v1/completions","model":"claude-3-sonnet","options":{"temperature":"0.9","max_tokens":1428,"stream":true}},"usage":{"prompt_tokens":54,"completion_tokens":132,"total_tokens":186}}
238
+ {"request_id":"req_71","timestamp":"2024-11-15T22:14:55.479Z","user_id":"usr_18","request":{"type":"chat","endpoint":"/v1/images/generations","model":"claude-3-opus","options":{"temperature":"0.5","max_tokens":650,"stream":false}},"usage":{"prompt_tokens":65,"completion_tokens":206,"total_tokens":271}}
239
+ {"request_id":"req_72","timestamp":"2024-11-09T06:44:07.284Z","user_id":"usr_43","request":{"type":"chat","endpoint":"/v1/images/generations","model":"claude-3-sonnet","options":{"temperature":"0.2","max_tokens":1363,"stream":false}},"usage":{"prompt_tokens":443,"completion_tokens":284,"total_tokens":727}}
240
+ {"request_id":"req_73","timestamp":"2024-11-06T01:10:44.917Z","user_id":"usr_24","request":{"type":"chat","endpoint":"/v1/completions","model":"gpt-4","options":{"temperature":"0.4","max_tokens":1927,"stream":true}},"usage":{"prompt_tokens":495,"completion_tokens":216,"total_tokens":711}}
241
+ {"request_id":"req_74","timestamp":"2024-10-19T14:43:49.854Z","user_id":"usr_26","request":{"type":"chat","endpoint":"/v1/chat/completions","model":"gpt-3.5-turbo","options":{"temperature":"0.4","max_tokens":679,"stream":false}},"usage":{"prompt_tokens":221,"completion_tokens":286,"total_tokens":507}}
242
+ {"request_id":"req_75","timestamp":"2024-10-05T06:19:28.703Z","user_id":"usr_25","request":{"type":"chat","endpoint":"/v1/completions","model":"claude-3-opus","options":{"temperature":"0.8","max_tokens":1614,"stream":false}},"usage":{"prompt_tokens":59,"completion_tokens":93,"total_tokens":152}}
243
+ {"request_id":"req_76","timestamp":"2024-11-04T22:29:57.675Z","user_id":"usr_33","request":{"type":"chat","endpoint":"/v1/completions","model":"claude-3-sonnet","options":{"temperature":"0.9","max_tokens":1491,"stream":false}},"usage":{"prompt_tokens":90,"completion_tokens":120,"total_tokens":210}}
244
+ {"request_id":"req_77","timestamp":"2024-11-08T02:17:28.909Z","user_id":"usr_4","request":{"type":"chat","endpoint":"/v1/images/generations","model":"gpt-3.5-turbo","options":{"temperature":"0.0","max_tokens":1324,"stream":true}},"usage":{"prompt_tokens":355,"completion_tokens":102,"total_tokens":457}}
245
+ {"request_id":"req_78","timestamp":"2024-10-24T15:25:14.046Z","user_id":"usr_39","request":{"type":"chat","endpoint":"/v1/images/generations","model":"claude-3-opus","options":{"temperature":"0.8","max_tokens":1529,"stream":false}},"usage":{"prompt_tokens":208,"completion_tokens":141,"total_tokens":349}}
246
+ {"request_id":"req_79","timestamp":"2024-10-27T13:01:16.190Z","user_id":"usr_40","request":{"type":"chat","endpoint":"/v1/chat/completions","model":"claude-3-opus","options":{"temperature":"0.0","max_tokens":1638,"stream":false}},"usage":{"prompt_tokens":243,"completion_tokens":274,"total_tokens":517}}
247
+ {"request_id":"req_80","timestamp":"2024-11-22T13:07:16.699Z","user_id":"usr_24","request":{"type":"chat","endpoint":"/v1/chat/completions","model":"gpt-3.5-turbo","options":{"temperature":"0.4","max_tokens":1397,"stream":true}},"usage":{"prompt_tokens":263,"completion_tokens":185,"total_tokens":448}}
248
+ {"request_id":"req_81","timestamp":"2024-11-29T03:35:54.264Z","user_id":"usr_26","request":{"type":"chat","endpoint":"/v1/chat/completions","model":"gpt-4","options":{"temperature":"0.1","max_tokens":1521,"stream":false}},"usage":{"prompt_tokens":273,"completion_tokens":199,"total_tokens":472}}
249
+ {"request_id":"req_82","timestamp":"2024-11-20T08:27:02.022Z","user_id":"usr_38","request":{"type":"chat","endpoint":"/v1/chat/completions","model":"claude-3-opus","options":{"temperature":"0.6","max_tokens":1463,"stream":true}},"usage":{"prompt_tokens":192,"completion_tokens":120,"total_tokens":312}}
250
+ {"request_id":"req_83","timestamp":"2024-11-09T10:38:51.640Z","user_id":"usr_24","request":{"type":"chat","endpoint":"/v1/images/generations","model":"gpt-3.5-turbo","options":{"temperature":"1.0","max_tokens":1574,"stream":false}},"usage":{"prompt_tokens":406,"completion_tokens":58,"total_tokens":464}}
251
+ {"request_id":"req_84","timestamp":"2024-10-29T18:37:34.187Z","user_id":"usr_16","request":{"type":"chat","endpoint":"/v1/completions","model":"claude-3-opus","options":{"temperature":"0.1","max_tokens":1924,"stream":true}},"usage":{"prompt_tokens":237,"completion_tokens":284,"total_tokens":521}}
252
+ {"request_id":"req_85","timestamp":"2024-10-17T21:47:26.419Z","user_id":"usr_49","request":{"type":"chat","endpoint":"/v1/completions","model":"claude-3-opus","options":{"temperature":"0.7","max_tokens":1714,"stream":false}},"usage":{"prompt_tokens":114,"completion_tokens":194,"total_tokens":308}}
253
+ {"request_id":"req_86","timestamp":"2024-10-13T16:59:19.264Z","user_id":"usr_40","request":{"type":"chat","endpoint":"/v1/completions","model":"gpt-3.5-turbo","options":{"temperature":"0.8","max_tokens":520,"stream":true}},"usage":{"prompt_tokens":227,"completion_tokens":190,"total_tokens":417}}
254
+ {"request_id":"req_87","timestamp":"2024-10-25T06:47:20.855Z","user_id":"usr_48","request":{"type":"chat","endpoint":"/v1/completions","model":"claude-3-sonnet","options":{"temperature":"0.3","max_tokens":1904,"stream":false}},"usage":{"prompt_tokens":476,"completion_tokens":96,"total_tokens":572}}
255
+ {"request_id":"req_88","timestamp":"2024-11-05T07:30:25.734Z","user_id":"usr_4","request":{"type":"chat","endpoint":"/v1/chat/completions","model":"gpt-3.5-turbo","options":{"temperature":"0.9","max_tokens":535,"stream":false}},"usage":{"prompt_tokens":399,"completion_tokens":217,"total_tokens":616}}
256
+ {"request_id":"req_89","timestamp":"2024-11-13T01:46:36.632Z","user_id":"usr_20","request":{"type":"chat","endpoint":"/v1/completions","model":"gpt-4","options":{"temperature":"0.4","max_tokens":1260,"stream":true}},"usage":{"prompt_tokens":189,"completion_tokens":159,"total_tokens":348}}
257
+ {"request_id":"req_90","timestamp":"2024-11-29T06:17:41.475Z","user_id":"usr_7","request":{"type":"chat","endpoint":"/v1/chat/completions","model":"claude-3-sonnet","options":{"temperature":"0.5","max_tokens":1105,"stream":false}},"usage":{"prompt_tokens":164,"completion_tokens":223,"total_tokens":387}}
258
+ {"request_id":"req_91","timestamp":"2024-11-21T21:18:42.618Z","user_id":"usr_31","request":{"type":"chat","endpoint":"/v1/chat/completions","model":"gpt-3.5-turbo","options":{"temperature":"0.1","max_tokens":1899,"stream":false}},"usage":{"prompt_tokens":424,"completion_tokens":121,"total_tokens":545}}
259
+ {"request_id":"req_92","timestamp":"2024-10-13T04:50:06.616Z","user_id":"usr_20","request":{"type":"chat","endpoint":"/v1/completions","model":"gpt-4","options":{"temperature":"0.6","max_tokens":752,"stream":true}},"usage":{"prompt_tokens":400,"completion_tokens":160,"total_tokens":560}}
260
+ {"request_id":"req_93","timestamp":"2024-10-06T12:44:26.578Z","user_id":"usr_18","request":{"type":"chat","endpoint":"/v1/images/generations","model":"gpt-4","options":{"temperature":"0.4","max_tokens":1916,"stream":false}},"usage":{"prompt_tokens":193,"completion_tokens":299,"total_tokens":492}}
261
+ {"request_id":"req_94","timestamp":"2024-11-16T12:40:44.907Z","user_id":"usr_46","request":{"type":"chat","endpoint":"/v1/completions","model":"claude-3-sonnet","options":{"temperature":"0.9","max_tokens":1612,"stream":false}},"usage":{"prompt_tokens":149,"completion_tokens":181,"total_tokens":330}}
262
+ {"request_id":"req_95","timestamp":"2024-10-28T14:02:47.893Z","user_id":"usr_21","request":{"type":"chat","endpoint":"/v1/chat/completions","model":"gpt-3.5-turbo","options":{"temperature":"0.6","max_tokens":1378,"stream":true}},"usage":{"prompt_tokens":363,"completion_tokens":292,"total_tokens":655}}
263
+ {"request_id":"req_96","timestamp":"2024-10-24T10:13:34.492Z","user_id":"usr_9","request":{"type":"chat","endpoint":"/v1/images/generations","model":"claude-3-opus","options":{"temperature":"0.2","max_tokens":1055,"stream":true}},"usage":{"prompt_tokens":463,"completion_tokens":252,"total_tokens":715}}
264
+ {"request_id":"req_97","timestamp":"2024-10-25T05:35:39.972Z","user_id":"usr_32","request":{"type":"chat","endpoint":"/v1/completions","model":"gpt-3.5-turbo","options":{"temperature":"0.1","max_tokens":877,"stream":false}},"usage":{"prompt_tokens":451,"completion_tokens":211,"total_tokens":662}}
265
+ {"request_id":"req_98","timestamp":"2024-10-30T12:55:53.621Z","user_id":"usr_21","request":{"type":"chat","endpoint":"/v1/images/generations","model":"claude-3-sonnet","options":{"temperature":"0.8","max_tokens":1966,"stream":true}},"usage":{"prompt_tokens":497,"completion_tokens":93,"total_tokens":590}}
266
+ {"request_id":"req_99","timestamp":"2024-11-14T11:31:10.758Z","user_id":"usr_11","request":{"type":"chat","endpoint":"/v1/completions","model":"gpt-4","options":{"temperature":"0.4","max_tokens":564,"stream":true}},"usage":{"prompt_tokens":244,"completion_tokens":154,"total_tokens":398}}
267
+ {"request_id":"req_100","timestamp":"2024-11-22T13:43:33.802Z","user_id":"usr_25","request":{"type":"chat","endpoint":"/v1/images/generations","model":"gpt-4","options":{"temperature":"0.9","max_tokens":1096,"stream":true}},"usage":{"prompt_tokens":58,"completion_tokens":274,"total_tokens":332}}
268
+ """
269
+ persist_fixture(f"{ds_name}", ds_content)
270
+ click.echo(FeedbackManager.info(message=f"✓ /fixtures/{ds_name}"))
271
+
272
+ # Create sample endpoint
273
+ pipe_name = "api_token_usage"
274
+ pipe_path = Path(folder) / "endpoints" / f"{pipe_name}.pipe"
275
+ pipe_content = """DESCRIPTION >
276
+ Pipe to generate a token usage timeline.
277
+
278
+ NODE api_token_usage_node_1
279
+ SQL >
280
+ %
281
+ SELECT
282
+ toDate(timestamp) as date,
283
+ user_id,
284
+ sum(usage_total_tokens) as total_tokens
285
+ FROM
286
+ events
287
+ WHERE
288
+ user_id IN (SELECT id FROM users WHERE organization_id = {{String(organization_id)}})
289
+ {%if defined(start_date) and defined(end_date)%}
290
+ AND timestamp BETWEEN {{DateTime(start_date)}} AND {{DateTime(end_date)}}
291
+ {%else%}
292
+ AND timestamp BETWEEN now() - interval 30 day AND now()
293
+ {%end%}
294
+ GROUP BY
295
+ date, user_id
296
+ ORDER BY
297
+ date
298
+
299
+ TYPE Endpoint
300
+
301
+ """
302
+ pipe_path.write_text(pipe_content)
303
+ click.echo(FeedbackManager.info(message=f"✓ /endpoints/{pipe_name}.pipe"))
304
+
305
+ elif data:
67
306
  ds_name = os.path.basename(data.split(".")[0])
68
307
  data_content = Path(data).read_text()
69
308
  datasource_path = Path(folder) / "datasources" / f"{ds_name}.datasource"
@@ -79,7 +318,7 @@ async def create(
79
318
  datasource_content = datasource_path.read_text()
80
319
  has_json_path = "`json:" in datasource_content
81
320
  if has_json_path:
82
- sql = await llm.generate_sql_sample_data(schema=datasource_content, rows=rows)
321
+ sql = await llm.generate_sql_sample_data(schema=datasource_content, rows=rows, context=prompt)
83
322
  result = await tb_client.query(f"{sql} FORMAT JSON")
84
323
  data = result.get("data", [])
85
324
  fixture_name = build_fixture_name(datasource_path.absolute(), datasource_name, datasource_content)
@@ -1,11 +1,14 @@
1
1
  import asyncio
2
+ import json
2
3
  import urllib.parse
3
4
  from copy import deepcopy
4
- from typing import Awaitable, Callable, List
5
+ from typing import Awaitable, Callable, List, Optional
5
6
 
7
+ from openai import OpenAI
6
8
  from pydantic import BaseModel
7
9
 
8
10
  from tinybird.client import TinyB
11
+ from tinybird.prompts import create_test_calls_prompt
9
12
  from tinybird.tb.modules.config import CLIConfig
10
13
 
11
14
 
@@ -19,13 +22,25 @@ class DataProject(BaseModel):
19
22
  pipes: List[DataFile]
20
23
 
21
24
 
25
+ class TestExpectation(BaseModel):
26
+ name: str
27
+ description: str
28
+ parameters: str
29
+ expected_result: str
30
+
31
+
32
+ class TestExpectations(BaseModel):
33
+ tests: List[TestExpectation]
34
+
35
+
22
36
  class LLM:
23
- def __init__(self, client: TinyB):
37
+ def __init__(self, client: TinyB, api_key: Optional[str] = None):
24
38
  self.client = client
25
39
  user_token = CLIConfig.get_project_config().get_user_token()
26
40
  user_client = deepcopy(client)
27
41
  user_client.token = user_token
28
42
  self.user_client = user_client
43
+ self.openai = OpenAI(api_key=api_key) if api_key else None
29
44
 
30
45
  async def _execute(self, action_fn: Callable[[], Awaitable[str]], checker_fn: Callable[[str], bool]):
31
46
  is_valid = False
@@ -46,9 +61,10 @@ class LLM:
46
61
  response = await self.user_client._req(
47
62
  "/v0/llm/create",
48
63
  method="POST",
49
- data=f'{{"prompt": "{prompt}"}}',
64
+ data=f'{{"prompt": {json.dumps(prompt)}}}',
50
65
  headers={"Content-Type": "application/json"},
51
66
  )
67
+
52
68
  return DataProject.model_validate(response.get("result", {}))
53
69
  except Exception:
54
70
  return DataProject(datasources=[], pipes=[])
@@ -61,3 +77,19 @@ class LLM:
61
77
  headers={"Content-Type": "application/json"},
62
78
  )
63
79
  return response.get("result", "")
80
+
81
+ async def create_test_commands(
82
+ self, pipe_content: str, pipe_params: set[str], context: str = ""
83
+ ) -> TestExpectations:
84
+ if not self.openai:
85
+ raise ValueError("OpenAI API key is not set")
86
+
87
+ completion = self.openai.beta.chat.completions.parse(
88
+ model="gpt-4o",
89
+ messages=[
90
+ {"role": "system", "content": create_test_calls_prompt.format(context=context)},
91
+ {"role": "user", "content": f"Pipe content: {pipe_content}\nPipe params: {pipe_params}"},
92
+ ],
93
+ response_format=TestExpectations,
94
+ )
95
+ return completion.choices[0].message.parsed or TestExpectations(tests=[])
@@ -1,25 +1,14 @@
1
- import hashlib
2
1
  import os
3
2
  import time
4
- from typing import Optional
5
3
 
6
4
  import click
7
- import requests
8
5
 
9
6
  import docker
10
7
  from tinybird.feedback_manager import FeedbackManager
11
8
  from tinybird.tb.modules.cli import cli
12
- from tinybird.tb.modules.common import (
13
- coro,
14
- )
15
- from tinybird.tb.modules.config import CLIConfig
9
+ from tinybird.tb.modules.common import coro
16
10
  from tinybird.tb.modules.exceptions import CLIException
17
-
18
- # TODO: Use the official Tinybird image once it's available 'tinybirdco/tinybird-local:latest'
19
- TB_IMAGE_NAME = "registry.gitlab.com/tinybird/analytics/tinybird-local-jammy-3.11:latest"
20
- TB_CONTAINER_NAME = "tinybird-local"
21
- TB_LOCAL_PORT = int(os.getenv("TB_LOCAL_PORT", 80))
22
- TB_LOCAL_HOST = f"http://localhost:{TB_LOCAL_PORT}"
11
+ from tinybird.tb.modules.local_common import TB_CONTAINER_NAME, TB_IMAGE_NAME, TB_LOCAL_PORT
23
12
 
24
13
 
25
14
  def start_tinybird_local(
@@ -113,39 +102,6 @@ def remove_tinybird_local(docker_client):
113
102
  pass
114
103
 
115
104
 
116
- async def get_tinybird_local_client(path: Optional[str] = None):
117
- """Get a Tinybird client connected to the local environment."""
118
- config = CLIConfig.get_project_config()
119
- try:
120
- # ruff: noqa: ASYNC210
121
- tokens = requests.get(f"{TB_LOCAL_HOST}/tokens").json()
122
- except Exception:
123
- raise CLIException("Tinybird local is not running. Please run `tb local start` first.")
124
-
125
- user_token = tokens["user_token"]
126
- token = tokens["workspace_admin_token"]
127
- # Create a new workspace if path is provided. This is used to isolate the build in a different workspace.
128
- if path:
129
- folder_hash = hashlib.sha256(path.encode()).hexdigest()
130
- user_client = config.get_client(host=TB_LOCAL_HOST, token=user_token)
131
-
132
- ws_name = f"Tinybird_Local_Build_{folder_hash}"
133
-
134
- user_workspaces = await user_client.user_workspaces()
135
- ws = next((ws for ws in user_workspaces["workspaces"] if ws["name"] == ws_name), None)
136
- if not ws:
137
- await user_client.create_workspace(ws_name, template=None)
138
- user_workspaces = await user_client.user_workspaces()
139
- ws = next((ws for ws in user_workspaces["workspaces"] if ws["name"] == ws_name), None)
140
-
141
- token = ws["token"]
142
-
143
- config.set_token(token)
144
- config.set_host(TB_LOCAL_HOST)
145
- config.set_user_token(user_token)
146
- return config.get_client(host=TB_LOCAL_HOST, token=token)
147
-
148
-
149
105
  @cli.command()
150
106
  def upgrade():
151
107
  """Upgrade Tinybird CLI to the latest version"""
@@ -0,0 +1,54 @@
1
+ import hashlib
2
+ import os
3
+ from typing import Optional
4
+
5
+ import requests
6
+
7
+ from tinybird.client import TinyB
8
+ from tinybird.tb.modules.config import CLIConfig
9
+ from tinybird.tb.modules.exceptions import CLIException
10
+
11
+ # TODO: Use the official Tinybird image once it's available 'tinybirdco/tinybird-local:latest'
12
+ TB_IMAGE_NAME = "registry.gitlab.com/tinybird/analytics/tinybird-local-jammy-3.11:latest"
13
+ TB_CONTAINER_NAME = "tinybird-local"
14
+ TB_LOCAL_PORT = int(os.getenv("TB_LOCAL_PORT", 80))
15
+ TB_LOCAL_HOST = f"http://localhost:{TB_LOCAL_PORT}"
16
+
17
+
18
+ async def get_tinybird_local_client(path: Optional[str] = None) -> TinyB:
19
+ """Get a Tinybird client connected to the local environment."""
20
+ config = CLIConfig.get_project_config()
21
+ try:
22
+ # ruff: noqa: ASYNC210
23
+ tokens = requests.get(f"{TB_LOCAL_HOST}/tokens").json()
24
+ except Exception:
25
+ raise CLIException("Tinybird local is not running. Please run `tb local start` first.")
26
+
27
+ user_token = tokens["user_token"]
28
+ default_token = tokens["workspace_admin_token"]
29
+ # Create a new workspace if path is provided. This is used to isolate the build in a different workspace.
30
+ path = path or os.getcwd()
31
+ if path:
32
+ folder_hash = hashlib.sha256(path.encode()).hexdigest()
33
+ user_client = config.get_client(host=TB_LOCAL_HOST, token=user_token)
34
+
35
+ ws_name = f"Tinybird_Local_Build_{folder_hash}"
36
+
37
+ user_workspaces = await user_client.user_workspaces()
38
+ ws = next((ws for ws in user_workspaces["workspaces"] if ws["name"] == ws_name), None)
39
+ if not ws:
40
+ await user_client.create_workspace(ws_name, template=None)
41
+ user_workspaces = await user_client.user_workspaces()
42
+ ws = next((ws for ws in user_workspaces["workspaces"] if ws["name"] == ws_name), None)
43
+
44
+ ws_token = ws["token"]
45
+
46
+ config.set_token(ws_token)
47
+ config.set_token_for_host(TB_LOCAL_HOST, ws_token)
48
+ config.set_host(TB_LOCAL_HOST)
49
+ else:
50
+ config.set_token(default_token)
51
+ config.set_token_for_host(TB_LOCAL_HOST, default_token)
52
+
53
+ config.set_user_token(user_token)
54
+ return config.get_client(host=TB_LOCAL_HOST)
@@ -9,7 +9,7 @@ from tinybird.tb.modules.common import CLIException, coro
9
9
  from tinybird.tb.modules.config import CLIConfig
10
10
  from tinybird.tb.modules.datafile.fixture import build_fixture_name, persist_fixture
11
11
  from tinybird.tb.modules.llm import LLM
12
- from tinybird.tb.modules.local import get_tinybird_local_client
12
+ from tinybird.tb.modules.local_common import get_tinybird_local_client
13
13
 
14
14
 
15
15
  @cli.command()
@@ -3,8 +3,8 @@
3
3
  # - If it makes sense and only when strictly necessary, you can create utility functions in this file.
4
4
  # - But please, **do not** interleave utility functions and command definitions.
5
5
 
6
+ import difflib
6
7
  import glob
7
- import json
8
8
  import os
9
9
  from pathlib import Path
10
10
  from typing import Iterable, List, Optional, Tuple
@@ -18,7 +18,7 @@ from tinybird.tb.modules.common import coro
18
18
  from tinybird.tb.modules.config import CLIConfig
19
19
  from tinybird.tb.modules.exceptions import CLIException
20
20
  from tinybird.tb.modules.llm import LLM, TestExpectation
21
- from tinybird.tb.modules.local import get_tinybird_local_client
21
+ from tinybird.tb.modules.local_common import get_tinybird_local_client
22
22
 
23
23
 
24
24
  @cli.group()
@@ -70,22 +70,26 @@ async def test_create(ctx: click.Context, pipe: str, prompt: Optional[str], fold
70
70
  pipe_path = Path(folder) / pipe_path
71
71
  pipe_content = pipe_path.read_text()
72
72
 
73
- llm_config = CLIConfig.get_llm_config()
74
- llm = LLM(key=llm_config["api_key"])
75
-
76
73
  client = await get_tinybird_local_client(os.path.abspath(folder))
74
+ pipe_nodes = await client._req(f"/v0/pipes/{pipe_name}")
75
+ pipe_params = set([param["name"] for node in pipe_nodes["nodes"] for param in node["params"]])
76
+
77
+ llm_config = CLIConfig.get_llm_config()
78
+ llm = LLM(client=client, api_key=llm_config["api_key"])
77
79
 
78
- test_expectations = await llm.create_test_commands(pipe_content=pipe_content, context=prompt)
80
+ test_expectations = await llm.create_test_commands(
81
+ pipe_content=pipe_content, pipe_params=pipe_params, context=prompt
82
+ )
79
83
  valid_test_expectations = []
80
84
  for test in test_expectations.tests:
85
+ test_params = test.parameters if test.parameters.startswith("?") else f"?{test.parameters}"
81
86
  try:
82
- response = await client._req(f"/v0/pipes/{pipe_name}.ndjson?{test.parameters}")
87
+ response = await client._req(f"/v0/pipes/{pipe_name}.ndjson{test_params}")
83
88
  except Exception:
84
89
  continue
85
90
 
86
- test.expected_result = json.dumps(response)
91
+ test.expected_result = response
87
92
  valid_test_expectations.append(test.model_dump())
88
-
89
93
  if valid_test_expectations:
90
94
  generate_test_file(pipe_name, valid_test_expectations)
91
95
  click.echo(FeedbackManager.info(message=f"✓ /tests/{pipe_name}.yaml"))
@@ -117,11 +121,18 @@ async def test_run(ctx: click.Context, file: Tuple[str, ...], folder: Optional[s
117
121
  for test in test_file_content:
118
122
  try:
119
123
  response = await client._req(f"/v0/pipes/{test_file_path.stem}.ndjson?{test['parameters']}")
120
- if test["expected_result"] != json.dumps(response):
121
- raise Exception("Test failed")
124
+ if test["expected_result"] != response:
125
+ diff = difflib.ndiff(
126
+ test["expected_result"].splitlines(keepends=True), response.splitlines(keepends=True)
127
+ )
128
+ printable_diff = "".join(diff)
129
+ raise Exception(
130
+ f"\nExpected: \n{test['expected_result']}\nGot: \n{response}\nDiff: \n{printable_diff}"
131
+ )
122
132
  click.echo(FeedbackManager.success(message=f"✓ {test_file_path.name} - {test['name']}"))
123
- except Exception:
133
+ except Exception as e:
124
134
  click.echo(FeedbackManager.error(message=f"✗ {test_file_path.name} - {test['name']}"))
135
+ click.echo(FeedbackManager.error(message=f"Output and expected output are different: \n{e}"))
125
136
 
126
137
  for test_file in file_list:
127
138
  await run_test(test_file)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tinybird
3
- Version: 0.0.1.dev15
3
+ Version: 0.0.1.dev16
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -1,11 +1,12 @@
1
1
  tinybird/__cli__.py,sha256=pgYsVLcqL16wtSn6KtKweNZYoYJdEksTgSvQAW7hH64,250
2
- tinybird/client.py,sha256=nd97gD2-8Ap8yDonBcVwk9eXDAL43hmIYdo-Pse43RE,50738
2
+ tinybird/client.py,sha256=_voHMgKOwOhmncSaPH6DL94X6YPqKJJzcY-EKp8xLug,50810
3
3
  tinybird/config.py,sha256=Z-BX9FrjgsLw1YwcCdF0IztLB97Zpc70VVPplO_pDSY,6089
4
4
  tinybird/connectors.py,sha256=lkpVSUmSuViEZBa4QjTK7YmPHUop0a5UFoTrSmlVq6k,15244
5
5
  tinybird/context.py,sha256=kutUQ0kCwparowI74_YLXx6wtTzGLRouJ6oGHVBPzBo,1291
6
6
  tinybird/datatypes.py,sha256=IHyhZ86ib54Vnd1pbod9y2aS8DDvDKZm1HJGlThdbuQ,10460
7
- tinybird/feedback_manager.py,sha256=-nDkg13DoiNk40AztzSJdbldbKhfuTsCZcSOviK9sik,67790
7
+ tinybird/feedback_manager.py,sha256=U_VbVl3RBCPV-qc3g8vz124Mg9XeMmKk055eqiIaBqY,68338
8
8
  tinybird/git_settings.py,sha256=XUL9ZUj59-ZVQJDYmMEq4UpnuuOuQOHGlNcX3JgQHjQ,3954
9
+ tinybird/prompts.py,sha256=pCqN1JAHqdA-PgxBnzjIYpCXjpko89Xu4EHl6ol0iNs,6417
9
10
  tinybird/sql.py,sha256=gfRKjdqEygcE1WOTeQ1QV2Jal8Jzl4RSX8fftu1KSEs,45825
10
11
  tinybird/sql_template.py,sha256=IqYRfUxDYBCoOYjqqvn--_8QXLv9FSRnJ0bInx7q1Xs,93051
11
12
  tinybird/sql_template_fmt.py,sha256=1z-PuqSZXtzso8Z_mPqUc-NxIxUrNUcVIPezNieZk-M,10196
@@ -14,31 +15,32 @@ tinybird/syncasync.py,sha256=fAvq0qkRgqXqXMKwbY2iJNYqLT_r6mDsh1MRpGKrdRU,27763
14
15
  tinybird/tornado_template.py,sha256=o2HguxrL1Evnt8o3IvrsI8Zm6JtRQ3zhLJKf1XyR3SQ,41965
15
16
  tinybird/ch_utils/constants.py,sha256=aYvg2C_WxYWsnqPdZB1ZFoIr8ZY-XjUXYyHKE9Ansj0,3890
16
17
  tinybird/ch_utils/engine.py,sha256=OXkBhlzGjZotjD0vaT-rFIbSGV4tpiHxE8qO_ip0SyQ,40454
17
- tinybird/tb/cli.py,sha256=NipQJ0V-uqJDQSb4xkTw04ALzgqSfe1twmjkBkQJWT4,784
18
+ tinybird/tb/cli.py,sha256=VS9cy6FfiQkpc1-ps7snwuw7bB_F8Z6FEmucEo3jrzY,849
18
19
  tinybird/tb/modules/auth.py,sha256=hynZ-Temot8YBsySUWKSFzZlYadtFPxG3o6lCSu1n6E,9018
19
20
  tinybird/tb/modules/branch.py,sha256=R1tTUBGyI0p_dt2IAWbuyNOvemhjCIPwYxEmOxL3zOg,38468
20
- tinybird/tb/modules/build.py,sha256=_EvBtX5FO7fnpHP74qlrBJqm7Uk1Bx9NarJ8P-M8vvg,12901
21
+ tinybird/tb/modules/build.py,sha256=c9fC9qBSw7__MJreJZIpHGvKD6CgyL0zMtparmJCUgw,8001
22
+ tinybird/tb/modules/build_shell.py,sha256=oqIMZYaQ8cpaE1C09shAsiFUlWb-zwawuf6FLqRotBU,5209
21
23
  tinybird/tb/modules/cicd.py,sha256=KCFfywFfvGRh24GZwqrhICiTK_arHelPs_X4EB-pXIw,7331
22
- tinybird/tb/modules/cli.py,sha256=pF7bxobeLJP-aN7RwJmLaYOKQZHlBnneZpYbiAbTmaM,55511
24
+ tinybird/tb/modules/cli.py,sha256=UAVZfdQhw-zKokKlEL23Uup55adT1JVwcK9HPDv6TJQ,55759
23
25
  tinybird/tb/modules/common.py,sha256=Vubc2AIR8BfEupnT5e1Y8OYGEyvNoIcjo8th-SaUflw,80111
24
26
  tinybird/tb/modules/config.py,sha256=ppWvACHrSLkb5hOoQLYNby2w8jR76-8Kx2NBCst7ntQ,11760
25
27
  tinybird/tb/modules/connection.py,sha256=ZSqBGoRiJedjHKEyB_fr1ybucOHtaad8d7uqGa2Q92M,28668
26
- tinybird/tb/modules/create.py,sha256=WL2GzJBgH3z1m6oU58uZXyUQRzVB2rH4ig4BIdOJkLw,6307
28
+ tinybird/tb/modules/create.py,sha256=BiqeuRtDM9dfIB3wwFrO5SNqdafr1hWDwma4oNHYris,42432
27
29
  tinybird/tb/modules/datasource.py,sha256=tjcf5o-HYIdTkb_c1ErGUFIE-W6G992vsvCuDGcxb9Q,35818
28
30
  tinybird/tb/modules/exceptions.py,sha256=4A2sSjCEqKUMqpP3WI00zouCWW4uLaghXXLZBSw04mY,3363
29
31
  tinybird/tb/modules/fmt.py,sha256=UszEQO15fdzQ49QEj7Unhu68IKwSuKPsOrKhk2p2TAg,3547
30
32
  tinybird/tb/modules/job.py,sha256=eoBVyA24lYIPonU88Jn7FF9hBKz1kScy9_w_oWreuc4,2952
31
- tinybird/tb/modules/llm.py,sha256=j8AZl-4BaCrtpM3_ozGt2Pr0BPfPFG5qYAaFgjJabGk,2008
32
- tinybird/tb/modules/local.py,sha256=ajOrQMttYLXNs-4uDW9njw7-eIAHENvlbrNbURhr6gI,6926
33
+ tinybird/tb/modules/llm.py,sha256=duwf3r1caLaKzuOJC_stI_Hx54ncr-5uWaWeZ3HN65o,3099
34
+ tinybird/tb/modules/local.py,sha256=hV2fvHPaVHVzKwVoVDFAIbJZslOX1_COx96DZrR-dW8,5151
35
+ tinybird/tb/modules/local_common.py,sha256=Z2JYIE5HIJVNuzdIv1_NFxSEs8DOsLoCzbmT1iy0-HU,2172
33
36
  tinybird/tb/modules/login.py,sha256=vIeysdttfGDBMkf_i3cqAVNR5s0X0D6exATcRsDdWiA,5849
34
- tinybird/tb/modules/mock.py,sha256=HorUdULzxCCCGsFlPvL4RJeMJvVNnKnhbxxSog2Cn1Y,2817
37
+ tinybird/tb/modules/mock.py,sha256=5xhR_djr1-_JwJPI-Oy1exI3ndz7srmmvlei0DMQCAk,2824
35
38
  tinybird/tb/modules/pipe.py,sha256=9wnfKbp2FkmLiJgVk3qbra76ktwsUTXghu6j9cCEahQ,31058
36
- tinybird/tb/modules/prompts.py,sha256=KIDoAC0FvBSSsTft_mhpY6vxliNRBYOCsqikr_Hx0HA,9655
37
39
  tinybird/tb/modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
38
40
  tinybird/tb/modules/table.py,sha256=hG-PRDVuFp2uph41WpoLRV1yjp3RI2fi_iGGiI0rdxU,7695
39
41
  tinybird/tb/modules/tag.py,sha256=1qQWyk1p3Btv3LzM8VbJG-k7x2-pFuAlYCg3QL6QewI,3480
40
42
  tinybird/tb/modules/telemetry.py,sha256=iEGnMuCuNhvF6ln__j6X9MSTwL_0Hm-GgFHHHvhfknk,10466
41
- tinybird/tb/modules/test.py,sha256=UR7Fz8AHhIptZfxofidzNzTnfAgwhCSWaGyjKbD2TAQ,4605
43
+ tinybird/tb/modules/test.py,sha256=wWOg9FFyAiJkF7qjYapJUne5SVyXwmmhStfpCAZwZ80,5383
42
44
  tinybird/tb/modules/token.py,sha256=r0oeG1RpOOzHtqbUaHBiOmhE55HfNIvReAAWyKl9fJg,12695
43
45
  tinybird/tb/modules/workspace.py,sha256=FVlh-kbiZp5Gvp6dGFxi0UD8ail77rMamXLhqdVwrZ0,10916
44
46
  tinybird/tb/modules/workspace_members.py,sha256=08W0onEYkKLEC5TkAI07cxN9XSquEm7HnL7OkHAVDjo,8715
@@ -65,8 +67,8 @@ tinybird/tb_cli_modules/config.py,sha256=6NTgIdwf0X132A1j6G_YrdPep87ymZ9b5pABabK
65
67
  tinybird/tb_cli_modules/exceptions.py,sha256=pmucP4kTF4irIt7dXiG-FcnI-o3mvDusPmch1L8RCWk,3367
66
68
  tinybird/tb_cli_modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
67
69
  tinybird/tb_cli_modules/telemetry.py,sha256=iEGnMuCuNhvF6ln__j6X9MSTwL_0Hm-GgFHHHvhfknk,10466
68
- tinybird-0.0.1.dev15.dist-info/METADATA,sha256=D7NcYU4DyAQdBn5hevIYUm3gugIyzdVfXYT7L5DxTYQ,2405
69
- tinybird-0.0.1.dev15.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
70
- tinybird-0.0.1.dev15.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
71
- tinybird-0.0.1.dev15.dist-info/top_level.txt,sha256=pgw6AzERHBcW3YTi2PW4arjxLkulk2msOz_SomfOEuc,45
72
- tinybird-0.0.1.dev15.dist-info/RECORD,,
70
+ tinybird-0.0.1.dev16.dist-info/METADATA,sha256=Mo5LrkfYVPxIL2oqfKIjc2uUG41aYEzeVyTvWYI_abY,2405
71
+ tinybird-0.0.1.dev16.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
72
+ tinybird-0.0.1.dev16.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
73
+ tinybird-0.0.1.dev16.dist-info/top_level.txt,sha256=pgw6AzERHBcW3YTi2PW4arjxLkulk2msOz_SomfOEuc,45
74
+ tinybird-0.0.1.dev16.dist-info/RECORD,,