tinybird 0.0.1.dev15__py3-none-any.whl → 0.0.1.dev17__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.

@@ -36,7 +36,6 @@ from tinybird.tb.modules.common import (
36
36
  coro,
37
37
  create_tb_client,
38
38
  echo_safe_format_table,
39
- folder_init,
40
39
  get_current_main_workspace,
41
40
  getenv_bool,
42
41
  is_major_semver,
@@ -63,6 +62,7 @@ from tinybird.tb.modules.datafile.exceptions import (
63
62
  from tinybird.tb.modules.datafile.parse_datasource import parse_datasource
64
63
  from tinybird.tb.modules.datafile.parse_pipe import parse_pipe
65
64
  from tinybird.tb.modules.datafile.pull import folder_pull
65
+ from tinybird.tb.modules.local_common import get_tinybird_local_client
66
66
  from tinybird.tb.modules.telemetry import add_telemetry_event
67
67
 
68
68
  __old_click_echo = click.echo
@@ -80,7 +80,6 @@ DEFAULT_PATTERNS: List[Tuple[str, Union[str, Callable[[str], str]]]] = [
80
80
  )
81
81
  @click.option("--token", help="Use auth token, defaults to TB_TOKEN envvar, then to the .tinyb file")
82
82
  @click.option("--host", help="Use custom host, defaults to TB_HOST envvar, then to https://api.tinybird.co")
83
- @click.option("--semver", help="Semver of a Release to run the command. Example: 1.0.0", hidden=True)
84
83
  @click.option("--gcp-project-id", help="The Google Cloud project ID", hidden=True)
85
84
  @click.option(
86
85
  "--gcs-bucket", help="The Google Cloud Storage bucket to write temp files when using the connectors", hidden=True
@@ -108,6 +107,7 @@ DEFAULT_PATTERNS: List[Tuple[str, Union[str, Callable[[str], str]]]] = [
108
107
  "--with-headers", help="Flag to enable connector to export with headers", is_flag=True, default=False, hidden=True
109
108
  )
110
109
  @click.option("--show-tokens", is_flag=True, default=False, help="Enable the output of tokens")
110
+ @click.option("--local", is_flag=True, default=False, help="Run in local mode")
111
111
  @click.version_option(version=VERSION)
112
112
  @click.pass_context
113
113
  @coro
@@ -116,7 +116,6 @@ async def cli(
116
116
  debug: bool,
117
117
  token: str,
118
118
  host: str,
119
- semver: str,
120
119
  gcp_project_id: str,
121
120
  gcs_bucket: str,
122
121
  google_application_credentials: str,
@@ -131,13 +130,14 @@ async def cli(
131
130
  sf_stage,
132
131
  with_headers: bool,
133
132
  show_tokens: bool,
133
+ local: bool,
134
134
  ) -> None:
135
135
  """
136
136
  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.
137
137
  """
138
138
 
139
139
  # We need to unpatch for our tests not to break
140
- if show_tokens:
140
+ if show_tokens or local or ctx.invoked_subcommand == "build":
141
141
  __unpatch_click_output()
142
142
  else:
143
143
  __patch_click_output()
@@ -153,9 +153,7 @@ async def cli(
153
153
  config_temp.set_token(token)
154
154
  if host:
155
155
  config_temp.set_host(host)
156
- if semver:
157
- config_temp.set_semver(semver)
158
- if token or host or semver:
156
+ if token or host:
159
157
  await try_update_config_with_remote(config_temp, auto_persist=False, raise_on_errors=False)
160
158
 
161
159
  # Overwrite token and host with env vars manually, without resorting to click.
@@ -167,10 +165,8 @@ async def cli(
167
165
  token = os.environ.get("TB_TOKEN", "")
168
166
  if not host and "TB_HOST" in os.environ:
169
167
  host = os.environ.get("TB_HOST", "")
170
- if not semver and "TB_SEMVER" in os.environ:
171
- semver = os.environ.get("TB_SEMVER", "")
172
168
 
173
- config = await get_config(host, token, semver)
169
+ config = await get_config(host, token)
174
170
  client = _get_tb_client(config.get("token", None), config["host"])
175
171
 
176
172
  # If they have passed a token or host as paramter and it's different that record in .tinyb, refresh the workspace id
@@ -228,85 +224,14 @@ async def cli(
228
224
 
229
225
  logging.debug("debug enabled")
230
226
 
231
- ctx.ensure_object(dict)["client"] = _get_tb_client(config.get("token", None), config["host"], semver)
227
+ ctx.ensure_object(dict)["client"] = (
228
+ await get_tinybird_local_client() if local else _get_tb_client(config.get("token", None), config["host"])
229
+ )
232
230
 
233
231
  for connector in SUPPORTED_CONNECTORS:
234
232
  load_connector_config(ctx, connector, debug, check_uninstalled=True)
235
233
 
236
234
 
237
- @cli.command()
238
- @click.option(
239
- "--generate-datasources",
240
- is_flag=True,
241
- default=False,
242
- help="Generate datasources based on CSV, NDJSON and Parquet files in this folder",
243
- )
244
- @click.option(
245
- "--folder",
246
- default=None,
247
- type=click.Path(exists=True, file_okay=False),
248
- help="Folder where datafiles will be placed",
249
- )
250
- @click.option("-f", "--force", is_flag=True, default=False, help="Overrides existing files")
251
- @click.option(
252
- "-ir",
253
- "--ignore-remote",
254
- is_flag=True,
255
- default=False,
256
- help="Ignores remote files not present in the local data project on git init",
257
- )
258
- @click.option(
259
- "--git",
260
- is_flag=True,
261
- default=False,
262
- help="Init workspace with git releases. Generates CI/CD files for your git provider",
263
- )
264
- @click.option(
265
- "--override-commit",
266
- default=None,
267
- help="Use this option to manually override the reference commit of your workspace. This is useful if a commit is not recognized in your git log, such as after a force push (git push -f).",
268
- )
269
- @click.option(
270
- "--cicd", is_flag=True, default=False, help="Generates only CI/CD files for your git provider", hidden=True
271
- )
272
- @click.pass_context
273
- @coro
274
- async def init(
275
- ctx: Context,
276
- generate_datasources: bool,
277
- folder: Optional[str],
278
- force: bool,
279
- ignore_remote: bool,
280
- git: bool,
281
- override_commit: Optional[str],
282
- cicd: Optional[bool],
283
- ) -> None:
284
- """Initialize folder layout."""
285
- client: TinyB = ctx.ensure_object(dict)["client"]
286
- config = CLIConfig.get_project_config()
287
- if config.get("token") is None:
288
- raise AuthNoTokenException
289
- folder = folder if folder else getcwd()
290
-
291
- workspaces: List[Dict[str, Any]] = (await client.user_workspaces_and_branches()).get("workspaces", [])
292
- current_ws: Dict[str, Any] = next(
293
- (workspace for workspace in workspaces if config and workspace.get("id", ".") == config.get("id", "..")), {}
294
- )
295
-
296
- if current_ws.get("is_branch"):
297
- raise CLIException(FeedbackManager.error_not_allowed_in_branch())
298
-
299
- await folder_init(client, folder, generate_datasources, generate_releases=True, force=force)
300
-
301
- error = False
302
- final_response = None
303
-
304
- if final_response:
305
- if error:
306
- raise CLIException(final_response)
307
- click.echo(final_response)
308
-
309
-
310
235
  @cli.command()
311
236
  @click.argument("filenames", type=click.Path(exists=True), nargs=-1, default=None)
312
237
  @click.option("--debug", is_flag=True, default=False, help="Print internal representation")
@@ -1340,9 +1265,6 @@ async def deploy(
1340
1265
  check_backfill_required = getenv_bool("TB_CHECK_BACKFILL_REQUIRED", True)
1341
1266
  try:
1342
1267
  tb_client = create_tb_client(ctx)
1343
- if dry_run:
1344
- config.set_semver(None)
1345
- tb_client.semver = None
1346
1268
  await folder_push(
1347
1269
  tb_client=tb_client,
1348
1270
  dry_run=dry_run,
@@ -218,19 +218,6 @@ async def get_current_environment(client, config):
218
218
  return next((workspace for workspace in workspaces if workspace["id"] == config["id"]), None)
219
219
 
220
220
 
221
- async def get_current_workspace_branches(config: CLIConfig) -> List[Dict[str, Any]]:
222
- current_main_workspace: Optional[Dict[str, Any]] = await get_current_main_workspace(config)
223
- if not current_main_workspace:
224
- raise CLIException(FeedbackManager.error_unable_to_identify_main_workspace())
225
-
226
- client = config.get_client()
227
- user_branches: List[Dict[str, Any]] = (await client.user_workspace_branches()).get("workspaces", [])
228
- all_branches: List[Dict[str, Any]] = (await client.branches()).get("environments", [])
229
- branches = user_branches + [branch for branch in all_branches if branch not in user_branches]
230
-
231
- return [branch for branch in branches if branch.get("main") == current_main_workspace["id"]]
232
-
233
-
234
221
  class AliasedGroup(click.Group):
235
222
  def get_command(self, ctx, cmd_name):
236
223
  # Step one: built-in commands as normal
@@ -320,16 +307,15 @@ def getenv_bool(key: str, default: bool) -> bool:
320
307
  return v.lower() == "true" or v == "1"
321
308
 
322
309
 
323
- def _get_tb_client(token: str, host: str, semver: Optional[str] = None) -> TinyB:
310
+ def _get_tb_client(token: str, host: str) -> TinyB:
324
311
  disable_ssl: bool = getenv_bool("TB_DISABLE_SSL_CHECKS", False)
325
- return TinyB(token, host, version=VERSION, disable_ssl_checks=disable_ssl, send_telemetry=True, semver=semver)
312
+ return TinyB(token, host, version=VERSION, disable_ssl_checks=disable_ssl, send_telemetry=True)
326
313
 
327
314
 
328
315
  def create_tb_client(ctx: Context) -> TinyB:
329
316
  token = ctx.ensure_object(dict)["config"].get("token", "")
330
317
  host = ctx.ensure_object(dict)["config"].get("host", DEFAULT_API_HOST)
331
- semver = ctx.ensure_object(dict)["config"].get("semver", "")
332
- return _get_tb_client(token, host, semver=semver)
318
+ return _get_tb_client(token, host)
333
319
 
334
320
 
335
321
  async def _analyze(filename: str, client: TinyB, format: str, connector: Optional["Connector"] = None):
@@ -776,77 +762,6 @@ async def create_workspace_interactive(
776
762
  await create_workspace_non_interactive(ctx, workspace_name, starterkit, user_token, fork) # type: ignore
777
763
 
778
764
 
779
- async def create_workspace_branch(
780
- branch_name: Optional[str],
781
- last_partition: bool,
782
- all: bool,
783
- ignore_datasources: Optional[List[str]],
784
- wait: Optional[bool],
785
- ) -> None:
786
- """
787
- Creates a workspace branch
788
- """
789
- config = CLIConfig.get_project_config()
790
- _ = await try_update_config_with_remote(config)
791
-
792
- try:
793
- workspace = await get_current_workspace(config)
794
- if not workspace:
795
- raise CLIWorkspaceException(FeedbackManager.error_workspace())
796
-
797
- if not branch_name:
798
- click.echo(FeedbackManager.info_workspace_branch_create_greeting())
799
- default_name = f"{workspace['name']}_{uuid.uuid4().hex[0:4]}"
800
- branch_name = click.prompt("\Branch name", default=default_name, err=True, type=str)
801
- assert isinstance(branch_name, str)
802
-
803
- response = await config.get_client().create_workspace_branch(
804
- branch_name,
805
- last_partition,
806
- all,
807
- ignore_datasources,
808
- )
809
- assert isinstance(response, dict)
810
-
811
- is_job: bool = "job" in response
812
- is_summary: bool = "partitions" in response
813
-
814
- if not is_job and not is_summary:
815
- raise CLIException(str(response))
816
-
817
- if all and not is_job:
818
- raise CLIException(str(response))
819
-
820
- click.echo(
821
- FeedbackManager.success_workspace_branch_created(workspace_name=workspace["name"], branch_name=branch_name)
822
- )
823
-
824
- job_id: Optional[str] = None
825
-
826
- if is_job:
827
- job_id = response["job"]["job_id"]
828
- job_url = response["job"]["job_url"]
829
- click.echo(FeedbackManager.info_data_branch_job_url(url=job_url))
830
-
831
- if wait and is_job:
832
- assert isinstance(job_id, str)
833
-
834
- # Await the job to finish and get the result dict
835
- job_response = await wait_job(config.get_client(), job_id, job_url, "Branch creation")
836
- if job_response is None:
837
- raise CLIException(f"Empty job API response (job_id: {job_id}, job_url: {job_url})")
838
- else:
839
- response = job_response.get("result", {})
840
- is_summary = "partitions" in response
841
-
842
- await switch_workspace(config, branch_name, only_environments=True)
843
- if is_summary and (bool(last_partition) or bool(all)):
844
- await print_data_branch_summary(config.get_client(), None, response)
845
-
846
- except Exception as e:
847
- raise CLIException(FeedbackManager.error_exception(error=str(e)))
848
-
849
-
850
765
  async def print_data_branch_summary(client, job_id, response=None):
851
766
  response = await client.job(job_id) if job_id else response or {"partitions": []}
852
767
  columns = ["Data Source", "Partition", "Status", "Error"]
@@ -1327,13 +1242,10 @@ def _get_setting_value(connection, setting, sensitive_settings):
1327
1242
  return connection.get(setting, "")
1328
1243
 
1329
1244
 
1330
- async def switch_workspace(config: CLIConfig, workspace_name_or_id: str, only_environments: bool = False) -> None:
1245
+ async def switch_workspace(config: CLIConfig, workspace_name_or_id: str) -> None:
1331
1246
  try:
1332
- if only_environments:
1333
- workspaces = await get_current_workspace_branches(config)
1334
- else:
1335
- response = await config.get_client().user_workspaces()
1336
- workspaces = response["workspaces"]
1247
+ response = await config.get_client().user_workspaces()
1248
+ workspaces = response["workspaces"]
1337
1249
 
1338
1250
  workspace = next(
1339
1251
  (
@@ -1345,10 +1257,7 @@ async def switch_workspace(config: CLIConfig, workspace_name_or_id: str, only_en
1345
1257
  )
1346
1258
 
1347
1259
  if not workspace:
1348
- if only_environments:
1349
- raise CLIException(FeedbackManager.error_branch(branch=workspace_name_or_id))
1350
- else:
1351
- raise CLIException(FeedbackManager.error_workspace(workspace=workspace_name_or_id))
1260
+ raise CLIException(FeedbackManager.error_workspace(workspace=workspace_name_or_id))
1352
1261
 
1353
1262
  config.set_token(workspace["token"])
1354
1263
  config.set_token_for_host(workspace["token"], config.get_host())
@@ -72,7 +72,6 @@ class CLIConfig:
72
72
  "token": "TB_TOKEN",
73
73
  "user_token": "TB_USER_TOKEN",
74
74
  "host": "TB_HOST",
75
- "semver": "TB_SEMVER",
76
75
  }
77
76
 
78
77
  DEFAULTS: Dict[str, str] = {"host": DEFAULT_API_HOST if not FeatureFlags.is_localhost() else DEFAULT_LOCALHOST}
@@ -178,15 +177,6 @@ class CLIConfig:
178
177
  except KeyError:
179
178
  return None
180
179
 
181
- def set_semver(self, semver: Optional[str]) -> None:
182
- self["semver"] = semver
183
-
184
- def get_semver(self) -> Optional[str]:
185
- try:
186
- return self["semver"]
187
- except KeyError:
188
- return None
189
-
190
180
  def set_token_for_host(self, token: Optional[str], host: Optional[str]) -> None:
191
181
  """Sets the token for the specified host.
192
182
 
@@ -5,13 +5,13 @@ from pathlib import Path
5
5
  from typing import Optional
6
6
 
7
7
  import click
8
- from click import Context
8
+ import requests
9
9
 
10
10
  from tinybird.client import TinyB
11
11
  from tinybird.feedback_manager import FeedbackManager
12
12
  from tinybird.tb.modules.cicd import init_cicd
13
13
  from tinybird.tb.modules.cli import cli
14
- from tinybird.tb.modules.common import _generate_datafile, coro, generate_datafile
14
+ from tinybird.tb.modules.common import _generate_datafile, check_user_token, coro, generate_datafile
15
15
  from tinybird.tb.modules.config import CLIConfig
16
16
  from tinybird.tb.modules.datafile.fixture import build_fixture_name, persist_fixture
17
17
  from tinybird.tb.modules.exceptions import CLIException
@@ -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),
@@ -41,7 +46,8 @@ from tinybird.tb.modules.llm import LLM
41
46
  @click.pass_context
42
47
  @coro
43
48
  async def create(
44
- ctx: Context,
49
+ ctx: click.Context,
50
+ demo: bool,
45
51
  data: Optional[str],
46
52
  prompt: Optional[str],
47
53
  folder: Optional[str],
@@ -50,7 +56,16 @@ async def create(
50
56
  """Initialize a new project."""
51
57
  folder = folder or getcwd()
52
58
  try:
53
- config = CLIConfig.get_project_config()
59
+ config = CLIConfig.get_project_config(folder)
60
+
61
+ if prompt:
62
+ user_token = config.get_user_token()
63
+ try:
64
+ await check_user_token(ctx, token=user_token)
65
+ except Exception:
66
+ click.echo(FeedbackManager.error(message="This action requires authentication. Run 'tb login' first."))
67
+ return
68
+
54
69
  tb_client = config.get_client()
55
70
  click.echo(FeedbackManager.gray(message="Creating new project structure..."))
56
71
  await project_create(tb_client, data, prompt, folder)
@@ -63,7 +78,60 @@ async def create(
63
78
 
64
79
  click.echo(FeedbackManager.gray(message="Building fixtures..."))
65
80
 
66
- if data:
81
+ if demo:
82
+ # Users datasource
83
+ ds_name = "users"
84
+ datasource_path = Path(folder) / "datasources" / f"{ds_name}.datasource"
85
+ datasource_content = fetch_gist_content(
86
+ "https://gist.githubusercontent.com/gnzjgo/b48fb9c92825ed27c04e3104b9e871e1/raw/1f33c20eefbabc4903f38e234329e028d8ef9def/users.datasource"
87
+ )
88
+ datasource_path.write_text(datasource_content)
89
+ click.echo(FeedbackManager.info(message=f"✓ /datasources/{ds_name}.datasource"))
90
+
91
+ # Users fixtures
92
+ fixture_content = fetch_gist_content(
93
+ "https://gist.githubusercontent.com/gnzjgo/8e8f66a39d7576ce3a2529bf773334a8/raw/9cab636767990e97d44a141867e5f226e992de8c/users.ndjson"
94
+ )
95
+ fixture_name = build_fixture_name(datasource_path.absolute(), ds_name, datasource_path.read_text())
96
+ persist_fixture(fixture_name, fixture_content)
97
+ click.echo(FeedbackManager.info(message=f"✓ /fixtures/{ds_name}"))
98
+
99
+ # Events datasource
100
+ ds_name = "events"
101
+ datasource_path = Path(folder) / "datasources" / f"{ds_name}.datasource"
102
+ datasource_content = fetch_gist_content(
103
+ "https://gist.githubusercontent.com/gnzjgo/f8ca37b5b1f6707c75206b618de26bc9/raw/cd625da0dcd1ba8de29f12bc1c8600b9ff7c809c/events.datasource"
104
+ )
105
+ datasource_path.write_text(datasource_content)
106
+ click.echo(FeedbackManager.info(message=f"✓ /datasources/{ds_name}.datasource"))
107
+
108
+ # Events fixtures
109
+ fixture_content = fetch_gist_content(
110
+ "https://gist.githubusercontent.com/gnzjgo/859ab9439c17e77241d0c14a5a532809/raw/251f2f3f00a968f8759ec4068cebde915256b054/events.ndjson"
111
+ )
112
+ fixture_name = build_fixture_name(datasource_path.absolute(), ds_name, datasource_path.read_text())
113
+ persist_fixture(fixture_name, fixture_content)
114
+ click.echo(FeedbackManager.info(message=f"✓ /fixtures/{ds_name}"))
115
+
116
+ # Create sample endpoint
117
+ pipe_name = "api_token_usage"
118
+ pipe_path = Path(folder) / "endpoints" / f"{pipe_name}.pipe"
119
+ pipe_content = fetch_gist_content(
120
+ "https://gist.githubusercontent.com/gnzjgo/68ecc47472c2b754b0ae0c1187022963/raw/52cc3aa3afdf939e58d43355bfe4ddc739989ddd/api_token_usage.pipe"
121
+ )
122
+ pipe_path.write_text(pipe_content)
123
+ click.echo(FeedbackManager.info(message=f"✓ /endpoints/{pipe_name}.pipe"))
124
+
125
+ # Create sample test
126
+ test_name = "api_token_usage"
127
+ test_path = Path(folder) / "tests" / f"{test_name}.yaml"
128
+ test_content = fetch_gist_content(
129
+ "https://gist.githubusercontent.com/gnzjgo/e58620bbb977d6f42f1d0c2a7b46ac8f/raw/a5f61d5019111f937484f941111829dfce69f648/api_token_usage.yaml"
130
+ )
131
+ test_path.write_text(test_content)
132
+ click.echo(FeedbackManager.info(message=f"✓ /tests/{test_name}.yaml"))
133
+
134
+ elif data:
67
135
  ds_name = os.path.basename(data.split(".")[0])
68
136
  data_content = Path(data).read_text()
69
137
  datasource_path = Path(folder) / "datasources" / f"{ds_name}.datasource"
@@ -79,7 +147,7 @@ async def create(
79
147
  datasource_content = datasource_path.read_text()
80
148
  has_json_path = "`json:" in datasource_content
81
149
  if has_json_path:
82
- sql = await llm.generate_sql_sample_data(schema=datasource_content, rows=rows)
150
+ sql = await llm.generate_sql_sample_data(schema=datasource_content, rows=rows, context=prompt)
83
151
  result = await tb_client.query(f"{sql} FORMAT JSON")
84
152
  data = result.get("data", [])
85
153
  fixture_name = build_fixture_name(datasource_path.absolute(), datasource_name, datasource_content)
@@ -97,7 +165,7 @@ async def project_create(
97
165
  prompt: Optional[str],
98
166
  folder: str,
99
167
  ):
100
- project_paths = ["datasources", "endpoints", "materializations", "copies", "sinks", "fixtures"]
168
+ project_paths = ["datasources", "endpoints", "materializations", "copies", "sinks", "fixtures", "tests"]
101
169
  force = True
102
170
  for x in project_paths:
103
171
  try:
@@ -113,7 +181,7 @@ async def project_create(
113
181
  try:
114
182
  await _generate_datafile(str(path), client, format=format, force=force)
115
183
  except Exception as e:
116
- click.echo(FeedbackManager.error(message=f"Ersssssror: {str(e)}"))
184
+ click.echo(FeedbackManager.error(message=f"Error: {str(e)}"))
117
185
  name = data.split(".")[0]
118
186
  generate_pipe_file(
119
187
  f"{name}_endpoint",
@@ -170,3 +238,9 @@ def generate_pipe_file(name: str, content: str, folder: str):
170
238
  with open(f"{f}", "w") as file:
171
239
  file.write(content)
172
240
  click.echo(FeedbackManager.info_file_created(file=f.relative_to(folder)))
241
+
242
+
243
+ def fetch_gist_content(url: str) -> str: # TODO: replace this with a function that fetches the content from a repo
244
+ response = requests.get(url)
245
+ response.raise_for_status()
246
+ return response.text
@@ -266,7 +266,7 @@ async def new_pipe(
266
266
 
267
267
  if data.get("type") == "endpoint":
268
268
  token = tb_client.token
269
- print(f"""** => Test endpoint with:\n** $ curl {host}/v0/pipes/{p["name"]}.json?token={token}""") # noqa: T201
269
+ click.echo(f"""** => Test endpoint with:\n** $ curl {host}/v0/pipes/{p["name"]}.json?token={token}""")
270
270
 
271
271
 
272
272
  async def get_token_from_main_branch(branch_tb_client: TinyB) -> Optional[str]:
@@ -21,7 +21,6 @@ if TYPE_CHECKING:
21
21
  from tinybird.connectors import Connector
22
22
 
23
23
  from tinybird.feedback_manager import FeedbackManager
24
- from tinybird.tb.modules.branch import warn_if_in_live
25
24
  from tinybird.tb.modules.cli import cli
26
25
  from tinybird.tb.modules.common import (
27
26
  _analyze,
@@ -358,9 +357,6 @@ async def datasource_delete(ctx: Context, datasource_name: str, yes: bool):
358
357
  FeedbackManager.error_datasource_can_not_be_deleted(datasource=datasource_name, error=e)
359
358
  )
360
359
 
361
- semver: str = ctx.ensure_object(dict)["config"]["semver"]
362
- await warn_if_in_live(semver)
363
-
364
360
  if yes or click.confirm(
365
361
  FeedbackManager.warning_confirm_delete_datasource(
366
362
  warning_message=warning_message, dependencies_information=dependencies_information
@@ -391,9 +387,6 @@ async def datasource_delete(ctx: Context, datasource_name: str, yes: bool):
391
387
  async def datasource_truncate(ctx, datasource_name, yes, cascade):
392
388
  """Truncate a data source"""
393
389
 
394
- semver: str = ctx.ensure_object(dict)["config"]["semver"]
395
- await warn_if_in_live(semver)
396
-
397
390
  client = ctx.obj["client"]
398
391
  if yes or click.confirm(FeedbackManager.warning_confirm_truncate_datasource(datasource=datasource_name)):
399
392
  try:
@@ -453,9 +446,6 @@ async def datasource_delete_rows(ctx, datasource_name, sql_condition, yes, wait,
453
446
  - Delete rows with SQL condition and wait for the job to finish: `tb datasource delete [datasource_name] --sql-condition "country='ES'" --wait`
454
447
  """
455
448
 
456
- semver: str = ctx.ensure_object(dict)["config"]["semver"]
457
- await warn_if_in_live(semver)
458
-
459
449
  client: TinyB = ctx.ensure_object(dict)["client"]
460
450
  if (
461
451
  dry_run
@@ -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=[])