tinybird 0.0.1.dev25__py3-none-any.whl → 0.0.1.dev27__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.

Files changed (36) hide show
  1. tinybird/config.py +1 -1
  2. tinybird/datatypes.py +46 -57
  3. tinybird/git_settings.py +4 -4
  4. tinybird/prompts.py +644 -0
  5. tinybird/sql.py +9 -0
  6. tinybird/sql_toolset.py +17 -3
  7. tinybird/syncasync.py +1 -1
  8. tinybird/tb/__cli__.py +2 -2
  9. tinybird/tb/cli.py +2 -0
  10. tinybird/tb/modules/build.py +47 -19
  11. tinybird/tb/modules/build_server.py +75 -0
  12. tinybird/tb/modules/cli.py +22 -0
  13. tinybird/tb/modules/common.py +2 -2
  14. tinybird/tb/modules/config.py +13 -14
  15. tinybird/tb/modules/create.py +125 -120
  16. tinybird/tb/modules/datafile/build.py +28 -0
  17. tinybird/tb/modules/datafile/common.py +1 -0
  18. tinybird/tb/modules/datafile/fixture.py +10 -6
  19. tinybird/tb/modules/datafile/parse_pipe.py +2 -0
  20. tinybird/tb/modules/datasource.py +1 -1
  21. tinybird/tb/modules/deploy.py +160 -0
  22. tinybird/tb/modules/llm.py +32 -16
  23. tinybird/tb/modules/llm_utils.py +24 -0
  24. tinybird/tb/modules/local.py +2 -2
  25. tinybird/tb/modules/login.py +8 -6
  26. tinybird/tb/modules/mock.py +13 -9
  27. tinybird/tb/modules/test.py +69 -47
  28. tinybird/tb/modules/watch.py +2 -2
  29. tinybird/tb_cli_modules/common.py +2 -2
  30. tinybird/tb_cli_modules/config.py +5 -5
  31. tinybird/tornado_template.py +1 -3
  32. {tinybird-0.0.1.dev25.dist-info → tinybird-0.0.1.dev27.dist-info}/METADATA +1 -1
  33. {tinybird-0.0.1.dev25.dist-info → tinybird-0.0.1.dev27.dist-info}/RECORD +36 -33
  34. {tinybird-0.0.1.dev25.dist-info → tinybird-0.0.1.dev27.dist-info}/WHEEL +0 -0
  35. {tinybird-0.0.1.dev25.dist-info → tinybird-0.0.1.dev27.dist-info}/entry_points.txt +0 -0
  36. {tinybird-0.0.1.dev25.dist-info → tinybird-0.0.1.dev27.dist-info}/top_level.txt +0 -0
@@ -4,9 +4,9 @@ from pathlib import Path
4
4
  from typing import Optional
5
5
 
6
6
  import click
7
- import requests
8
7
 
9
8
  from tinybird.client import TinyB
9
+ from tinybird.prompts import create_prompt, mock_prompt
10
10
  from tinybird.tb.modules.cicd import init_cicd
11
11
  from tinybird.tb.modules.cli import cli
12
12
  from tinybird.tb.modules.common import _generate_datafile, check_user_token_with_client, coro, generate_datafile
@@ -15,15 +15,11 @@ from tinybird.tb.modules.datafile.fixture import build_fixture_name, persist_fix
15
15
  from tinybird.tb.modules.exceptions import CLIException
16
16
  from tinybird.tb.modules.feedback_manager import FeedbackManager
17
17
  from tinybird.tb.modules.llm import LLM
18
+ from tinybird.tb.modules.llm_utils import extract_xml, parse_xml
18
19
  from tinybird.tb.modules.local_common import get_tinybird_local_client
19
20
 
20
21
 
21
22
  @cli.command()
22
- @click.option(
23
- "--demo",
24
- is_flag=True,
25
- help="Demo data and files to get started",
26
- )
27
23
  @click.option(
28
24
  "--data",
29
25
  type=click.Path(exists=True),
@@ -39,13 +35,12 @@ from tinybird.tb.modules.local_common import get_tinybird_local_client
39
35
  @click.option(
40
36
  "--folder",
41
37
  default=".",
42
- type=click.Path(exists=True, file_okay=False),
38
+ type=click.Path(exists=False, file_okay=False),
43
39
  help="Folder where datafiles will be placed",
44
40
  )
45
41
  @click.option("--rows", type=int, default=10, help="Number of events to send")
46
42
  @coro
47
43
  async def create(
48
- demo: bool,
49
44
  data: Optional[str],
50
45
  prompt: Optional[str],
51
46
  folder: Optional[str],
@@ -53,6 +48,10 @@ async def create(
53
48
  ) -> None:
54
49
  """Initialize a new project."""
55
50
  folder = folder or getcwd()
51
+ folder_path = Path(folder)
52
+ if not folder_path.exists():
53
+ folder_path.mkdir()
54
+
56
55
  try:
57
56
  config = CLIConfig.get_project_config(folder)
58
57
  tb_client = config.get_client()
@@ -71,127 +70,92 @@ async def create(
71
70
  )
72
71
  return
73
72
  local_client = await get_tinybird_local_client(folder)
74
- click.echo(FeedbackManager.gray(message="Creating new project structure..."))
75
- await project_create(local_client, tb_client, user_token, data, prompt, folder)
76
- click.echo(FeedbackManager.success(message="✓ Scaffolding completed!\n"))
77
73
 
78
- click.echo(FeedbackManager.gray(message="\nCreating CI/CD files for GitHub and GitLab..."))
74
+ if not validate_project_structure(folder):
75
+ click.echo(FeedbackManager.highlight(message="\n» Creating new project structure..."))
76
+ create_project_structure(folder)
77
+ click.echo(FeedbackManager.success(message="✓ Scaffolding completed!\n"))
78
+
79
+ click.echo(FeedbackManager.highlight(message="\n» Creating resources..."))
80
+ await create_resources(local_client, tb_client, user_token, data, prompt, folder)
81
+ click.echo(FeedbackManager.success(message="✓ Done!\n"))
82
+
83
+ click.echo(FeedbackManager.highlight(message="\n» Creating CI/CD files for GitHub and GitLab..."))
79
84
  init_git(folder)
80
85
  await init_cicd(data_project_dir=os.path.relpath(folder))
81
86
  click.echo(FeedbackManager.success(message="✓ Done!\n"))
82
87
 
83
- click.echo(FeedbackManager.gray(message="Building fixtures..."))
88
+ if validate_fixtures(folder):
89
+ click.echo(FeedbackManager.highlight(message="\n» Generating fixtures..."))
84
90
 
85
- if demo:
86
- # Users datasource
87
- ds_name = "users"
88
- datasource_path = Path(folder) / "datasources" / f"{ds_name}.datasource"
89
- datasource_content = fetch_gist_content(
90
- "https://gist.githubusercontent.com/gnzjgo/b48fb9c92825ed27c04e3104b9e871e1/raw/1f33c20eefbabc4903f38e234329e028d8ef9def/users.datasource"
91
- )
92
- datasource_path.write_text(datasource_content)
93
- click.echo(FeedbackManager.info(message=f"✓ /datasources/{ds_name}.datasource"))
91
+ if data:
92
+ ds_name = os.path.basename(data.split(".")[0])
93
+ data_content = Path(data).read_text()
94
+ datasource_path = Path(folder) / "datasources" / f"{ds_name}.datasource"
95
+ fixture_name = build_fixture_name(
96
+ datasource_path.absolute().as_posix(), ds_name, datasource_path.read_text()
97
+ )
98
+ click.echo(FeedbackManager.info(message=f"✓ /fixtures/{ds_name}"))
99
+ persist_fixture(fixture_name, data_content, folder)
100
+ elif prompt and user_token:
101
+ datasource_files = [f for f in os.listdir(Path(folder) / "datasources") if f.endswith(".datasource")]
102
+ for datasource_file in datasource_files:
103
+ datasource_path = Path(folder) / "datasources" / datasource_file
104
+ llm = LLM(user_token=user_token, client=tb_client)
105
+ datasource_name = datasource_path.stem
106
+ datasource_content = datasource_path.read_text()
107
+ has_json_path = "`json:" in datasource_content
108
+ if has_json_path:
109
+ response = await llm.ask(prompt, system_prompt=mock_prompt(rows))
110
+ sql = extract_xml(response, "sql")
111
+ result = await local_client.query(f"{sql} FORMAT JSON")
112
+ data = result.get("data", [])
113
+ fixture_name = build_fixture_name(
114
+ datasource_path.absolute().as_posix(), datasource_name, datasource_content
115
+ )
116
+ if data:
117
+ persist_fixture(fixture_name, data, folder)
118
+ click.echo(FeedbackManager.info(message=f"✓ /fixtures/{datasource_name}"))
94
119
 
95
- # Users fixtures
96
- fixture_content = fetch_gist_content(
97
- "https://gist.githubusercontent.com/gnzjgo/8e8f66a39d7576ce3a2529bf773334a8/raw/9cab636767990e97d44a141867e5f226e992de8c/users.ndjson"
98
- )
99
- fixture_name = build_fixture_name(
100
- datasource_path.absolute().as_posix(), ds_name, datasource_path.read_text()
101
- )
102
- persist_fixture(fixture_name, fixture_content)
103
- click.echo(FeedbackManager.info(message=f"✓ /fixtures/{ds_name}"))
120
+ click.echo(FeedbackManager.success(message="✓ Done!\n"))
121
+ except Exception as e:
122
+ click.echo(FeedbackManager.error(message=f"Error: {str(e)}"))
104
123
 
105
- # Events datasource
106
- ds_name = "events"
107
- datasource_path = Path(folder) / "datasources" / f"{ds_name}.datasource"
108
- datasource_content = fetch_gist_content(
109
- "https://gist.githubusercontent.com/gnzjgo/f8ca37b5b1f6707c75206b618de26bc9/raw/cd625da0dcd1ba8de29f12bc1c8600b9ff7c809c/events.datasource"
110
- )
111
- datasource_path.write_text(datasource_content)
112
- click.echo(FeedbackManager.info(message=f"✓ /datasources/{ds_name}.datasource"))
113
124
 
114
- # Events fixtures
115
- fixture_content = fetch_gist_content(
116
- "https://gist.githubusercontent.com/gnzjgo/859ab9439c17e77241d0c14a5a532809/raw/251f2f3f00a968f8759ec4068cebde915256b054/events.ndjson"
117
- )
118
- fixture_name = build_fixture_name(
119
- datasource_path.absolute().as_posix(), ds_name, datasource_path.read_text()
120
- )
121
- persist_fixture(fixture_name, fixture_content)
122
- click.echo(FeedbackManager.info(message=f"✓ /fixtures/{ds_name}"))
125
+ PROJECT_PATHS = ("datasources", "endpoints", "materializations", "copies", "sinks", "fixtures", "tests")
123
126
 
124
- # Create sample endpoint
125
- pipe_name = "api_token_usage"
126
- pipe_path = Path(folder) / "endpoints" / f"{pipe_name}.pipe"
127
- pipe_content = fetch_gist_content(
128
- "https://gist.githubusercontent.com/gnzjgo/68ecc47472c2b754b0ae0c1187022963/raw/52cc3aa3afdf939e58d43355bfe4ddc739989ddd/api_token_usage.pipe"
129
- )
130
- pipe_path.write_text(pipe_content)
131
- click.echo(FeedbackManager.info(message=f"✓ /endpoints/{pipe_name}.pipe"))
132
127
 
133
- # Create sample test
134
- test_name = "api_token_usage"
135
- test_path = Path(folder) / "tests" / f"{test_name}.yaml"
136
- test_content = fetch_gist_content(
137
- "https://gist.githubusercontent.com/gnzjgo/e58620bbb977d6f42f1d0c2a7b46ac8f/raw/a3a1cd0ce3a90bcd2f6dfce00da51e6051443612/api_token_usage.yaml"
138
- )
139
- test_path.write_text(test_content)
140
- click.echo(FeedbackManager.info(message=f"✓ /tests/{test_name}.yaml"))
128
+ def validate_project_structure(folder: str) -> bool:
129
+ return all((Path(folder) / path).exists() for path in PROJECT_PATHS)
141
130
 
142
- elif data:
143
- ds_name = os.path.basename(data.split(".")[0])
144
- data_content = Path(data).read_text()
145
- datasource_path = Path(folder) / "datasources" / f"{ds_name}.datasource"
146
- fixture_name = build_fixture_name(
147
- datasource_path.absolute().as_posix(), ds_name, datasource_path.read_text()
148
- )
149
- click.echo(FeedbackManager.info(message=f"✓ /fixtures/{ds_name}"))
150
- persist_fixture(fixture_name, data_content)
151
- elif prompt and user_token:
152
- datasource_files = [f for f in os.listdir(Path(folder) / "datasources") if f.endswith(".datasource")]
153
- for datasource_file in datasource_files:
154
- datasource_path = Path(folder) / "datasources" / datasource_file
155
- llm = LLM(user_token=user_token, client=tb_client)
156
- datasource_name = datasource_path.stem
157
- datasource_content = datasource_path.read_text()
158
- has_json_path = "`json:" in datasource_content
159
- if has_json_path:
160
- sql = await llm.generate_sql_sample_data(schema=datasource_content, rows=rows, prompt=prompt)
161
- result = await local_client.query(f"{sql} FORMAT JSON")
162
- data = result.get("data", [])
163
- fixture_name = build_fixture_name(
164
- datasource_path.absolute().as_posix(), datasource_name, datasource_content
165
- )
166
- if data:
167
- persist_fixture(fixture_name, data)
168
- click.echo(FeedbackManager.info(message=f"✓ /fixtures/{datasource_name}"))
169
131
 
170
- click.echo(FeedbackManager.success(message="✓ Done!\n"))
171
- except Exception as e:
172
- click.echo(FeedbackManager.error(message=f"Error: {str(e)}"))
132
+ def validate_fixtures(folder: str) -> bool:
133
+ return (Path(folder) / "datasources").exists()
134
+
135
+
136
+ def create_project_structure(folder: str):
137
+ folder_path = Path(folder)
138
+ for x in PROJECT_PATHS:
139
+ try:
140
+ f = folder_path / x
141
+ f.mkdir()
142
+ except FileExistsError:
143
+ pass
144
+ click.echo(FeedbackManager.info_path_created(path=x))
173
145
 
174
146
 
175
- async def project_create(
147
+ async def create_resources(
176
148
  local_client: TinyB,
177
- client: TinyB,
149
+ tb_client: TinyB,
178
150
  user_token: Optional[str],
179
151
  data: Optional[str],
180
152
  prompt: Optional[str],
181
153
  folder: str,
182
154
  ):
183
- project_paths = ["datasources", "endpoints", "materializations", "copies", "sinks", "fixtures", "tests"]
184
155
  force = True
185
- for x in project_paths:
186
- try:
187
- f = Path(folder) / x
188
- f.mkdir()
189
- except FileExistsError:
190
- pass
191
- click.echo(FeedbackManager.info_path_created(path=x))
192
-
156
+ folder_path = Path(folder)
193
157
  if data:
194
- path = Path(folder) / data
158
+ path = folder_path / data
195
159
  format = path.suffix.lstrip(".")
196
160
  try:
197
161
  await _generate_datafile(str(path), local_client, format=format, force=force)
@@ -210,17 +174,64 @@ TYPE ENDPOINT
210
174
  )
211
175
  elif prompt and user_token:
212
176
  try:
213
- llm = LLM(user_token=user_token, client=client)
214
- result = await llm.create_project(prompt)
215
- for ds in result.datasources:
216
- content = ds.content.replace("```", "")
177
+ datasource_paths = [
178
+ Path(folder) / "datasources" / f
179
+ for f in os.listdir(Path(folder) / "datasources")
180
+ if f.endswith(".datasource")
181
+ ]
182
+ pipes_paths = [
183
+ Path(folder) / "endpoints" / f for f in os.listdir(Path(folder) / "endpoints") if f.endswith(".pipe")
184
+ ]
185
+ resources_xml = "\n".join(
186
+ [
187
+ f"<resource><type>{resource_type}</type><name>{resource_name}</name><content>{resource_content}</content></resource>"
188
+ for resource_type, resource_name, resource_content in [
189
+ ("datasource", ds.stem, ds.read_text()) for ds in datasource_paths
190
+ ]
191
+ + [
192
+ (
193
+ "pipe",
194
+ pipe.stem,
195
+ pipe.read_text(),
196
+ )
197
+ for pipe in pipes_paths
198
+ ]
199
+ ]
200
+ )
201
+ llm = LLM(user_token=user_token, client=tb_client)
202
+ result = await llm.ask(prompt, system_prompt=create_prompt(resources_xml))
203
+ result = extract_xml(result, "response")
204
+ resources = parse_xml(result, "resource")
205
+ datasources = []
206
+ pipes = []
207
+ for resource_xml in resources:
208
+ resource_type = extract_xml(resource_xml, "type")
209
+ name = extract_xml(resource_xml, "name")
210
+ content = extract_xml(resource_xml, "content")
211
+ resource = {
212
+ "name": name,
213
+ "content": content,
214
+ }
215
+ if resource_type.lower() == "datasource":
216
+ datasources.append(resource)
217
+ elif resource_type.lower() == "pipe":
218
+ pipes.append(resource)
219
+
220
+ for ds in datasources:
221
+ content = ds["content"].replace("```", "")
222
+ filename = f"{ds['name']}.datasource"
217
223
  generate_datafile(
218
- content, filename=f"{ds.name}.datasource", data=None, _format="ndjson", force=force, folder=folder
224
+ content,
225
+ filename=filename,
226
+ data=None,
227
+ _format="ndjson",
228
+ force=force,
229
+ folder=folder,
219
230
  )
220
231
 
221
- for pipe in result.pipes:
222
- content = pipe.content.replace("```", "")
223
- generate_pipe_file(pipe.name, content, folder)
232
+ for pipe in pipes:
233
+ content = pipe["content"].replace("```", "")
234
+ generate_pipe_file(pipe["name"], content, folder)
224
235
  except Exception as e:
225
236
  click.echo(FeedbackManager.error(message=f"Error: {str(e)}"))
226
237
 
@@ -250,9 +261,3 @@ def generate_pipe_file(name: str, content: str, folder: str):
250
261
  with open(f"{f}", "w") as file:
251
262
  file.write(content)
252
263
  click.echo(FeedbackManager.info_file_created(file=f.relative_to(folder)))
253
-
254
-
255
- def fetch_gist_content(url: str) -> str: # TODO: replace this with a function that fetches the content from a repo
256
- response = requests.get(url)
257
- response.raise_for_status()
258
- return response.text
@@ -722,6 +722,34 @@ async def process(
722
722
  except Exception as e:
723
723
  raise click.ClickException(str(e))
724
724
 
725
+ # datasource
726
+ # {
727
+ # "resource": "datasources",
728
+ # "resource_name": name,
729
+ # "version": doc.version,
730
+ # "params": params,
731
+ # "filename": filename,
732
+ # "deps": deps,
733
+ # "tokens": doc.tokens,
734
+ # "shared_with": doc.shared_with,
735
+ # "filtering_tags": doc.filtering_tags,
736
+ # }
737
+ # pipe
738
+ # {
739
+ # "resource": "pipes",
740
+ # "resource_name": name,
741
+ # "version": doc.version,
742
+ # "filename": filename,
743
+ # "name": name + version,
744
+ # "nodes": nodes,
745
+ # "deps": [x for x in set(deps)],
746
+ # "tokens": doc.tokens,
747
+ # "description": description,
748
+ # "warnings": doc.warnings,
749
+ # "filtering_tags": doc.filtering_tags,
750
+ # }
751
+
752
+ # r is essentially a Datasource or a Pipe in dict shape, like in the comment above
725
753
  for r in res:
726
754
  resource_name = r["resource_name"]
727
755
  warnings = r.get("warnings", [])
@@ -1340,6 +1340,7 @@ def parse(
1340
1340
  "export_compression": assign_var("export_compression"),
1341
1341
  "export_write_strategy": assign_var("export_write_strategy"),
1342
1342
  "export_kafka_topic": assign_var("export_kafka_topic"),
1343
+ "forward_query": sql("forward_query"),
1343
1344
  }
1344
1345
 
1345
1346
  engine_vars = set()
@@ -31,22 +31,26 @@ def build_fixture_name(filename: str, datasource_name: str, datasource_content:
31
31
  return f"{datasource_name}_{hash_str}"
32
32
 
33
33
 
34
- def get_fixture_dir() -> Path:
35
- fixture_dir = Path("fixtures")
34
+ def get_fixture_dir(folder: str) -> Path:
35
+ fixture_dir = Path(folder) / "fixtures"
36
36
  if not fixture_dir.exists():
37
37
  fixture_dir.mkdir()
38
38
  return fixture_dir
39
39
 
40
40
 
41
- def persist_fixture(fixture_name: str, data: Union[List[Dict[str, Any]], str], format="ndjson") -> Path:
42
- fixture_dir = get_fixture_dir()
41
+ def persist_fixture(fixture_name: str, data: Union[List[Dict[str, Any]], str], folder: str, format="ndjson") -> Path:
42
+ fixture_dir = get_fixture_dir(folder)
43
43
  fixture_file = fixture_dir / f"{fixture_name}.{format}"
44
44
  fixture_file.write_text(data if isinstance(data, str) else format_data_to_ndjson(data))
45
45
  return fixture_file
46
46
 
47
47
 
48
- def load_fixture(fixture_name: str, format="ndjson") -> Union[Path, None]:
49
- fixture_dir = get_fixture_dir()
48
+ def load_fixture(
49
+ fixture_name: str,
50
+ folder: str,
51
+ format="ndjson",
52
+ ) -> Union[Path, None]:
53
+ fixture_dir = get_fixture_dir(folder)
50
54
  fixture_file = fixture_dir / f"{fixture_name}.{format}"
51
55
  if not fixture_file.exists():
52
56
  return None
@@ -45,6 +45,8 @@ def parse_pipe(
45
45
  for node in doc.nodes:
46
46
  sql = node.get("sql", "")
47
47
  if sql.strip()[0] == "%":
48
+ # Note(eclbg): not sure what test_mode is for. I think it does something like using placeholder values
49
+ # for the variables in the template.
48
50
  sql, _, variable_warnings = render_sql_template(sql[1:], test_mode=True, name=node["name"])
49
51
  doc.warnings = variable_warnings
50
52
  # it'll fail with a ModuleNotFoundError when the toolset is not available but it returns the parsed doc
@@ -453,7 +453,7 @@ async def generate_datasource(ctx: Context, connector: str, filenames, force: bo
453
453
  """Generate a data source file based on a sample CSV file from local disk or url"""
454
454
  client: TinyB = ctx.ensure_object(dict)["client"]
455
455
 
456
- _connector: Optional["Connector"] = None
456
+ _connector: Optional[Connector] = None
457
457
  if connector:
458
458
  load_connector_config(ctx, connector, False, check_uninstalled=False)
459
459
  if connector not in ctx.ensure_object(dict):
@@ -0,0 +1,160 @@
1
+ import glob
2
+ import json
3
+ import logging
4
+ import time
5
+ from pathlib import Path
6
+ from typing import List
7
+
8
+ import click
9
+ import requests
10
+
11
+ from tinybird.tb.modules.cli import cli
12
+ from tinybird.tb.modules.config import CLIConfig
13
+ from tinybird.tb.modules.feedback_manager import FeedbackManager
14
+
15
+
16
+ def project_files(project_path: Path) -> List[str]:
17
+ project_file_extensions = ("datasource", "pipe")
18
+ project_files = []
19
+ for extension in project_file_extensions:
20
+ for project_file in glob.glob(f"{project_path}/**/*.{extension}", recursive=True):
21
+ logging.debug(f"Found project file: {project_file}")
22
+ project_files.append(project_file)
23
+ return project_files
24
+
25
+
26
+ def promote_deployment(host: str, headers: dict) -> None:
27
+ TINYBIRD_API_URL = host + "/v1/deployments"
28
+ r = requests.get(TINYBIRD_API_URL, headers=headers)
29
+ result = r.json()
30
+ logging.debug(json.dumps(result, indent=2))
31
+
32
+ deployments = result.get("deployments")
33
+ if not deployments:
34
+ click.echo(FeedbackManager.error(message="No deployments found"))
35
+ return
36
+
37
+ last_deployment, candidate_deployment = deployments[0], deployments[1]
38
+
39
+ if candidate_deployment.get("status") != "data_ready":
40
+ click.echo(FeedbackManager.error(message="Current deployment is not ready"))
41
+ return
42
+
43
+ if candidate_deployment.get("live"):
44
+ click.echo(FeedbackManager.error(message="Candidate deployment is already live"))
45
+ else:
46
+ click.echo(FeedbackManager.success(message="Promoting deployment"))
47
+
48
+ TINYBIRD_API_URL = host + f"/v1/deployments/{candidate_deployment.get('id')}/set-live"
49
+ r = requests.post(TINYBIRD_API_URL, headers=headers)
50
+ result = r.json()
51
+ logging.debug(json.dumps(result, indent=2))
52
+
53
+ click.echo(FeedbackManager.success(message="Removing old deployment"))
54
+
55
+ TINYBIRD_API_URL = host + f"/v1/deployments/{last_deployment.get('id')}"
56
+ r = requests.delete(TINYBIRD_API_URL, headers=headers)
57
+ result = r.json()
58
+ logging.debug(json.dumps(result, indent=2))
59
+
60
+ click.echo(FeedbackManager.success(message="Deployment promoted successfully"))
61
+
62
+
63
+ @cli.command()
64
+ @click.argument("project_path", type=click.Path(exists=True), default=Path.cwd())
65
+ @click.option(
66
+ "--wait/--no-wait",
67
+ is_flag=True,
68
+ default=False,
69
+ help="Wait for deploy to finish. Disabled by default.",
70
+ )
71
+ @click.option(
72
+ "--auto/--no-auto",
73
+ is_flag=True,
74
+ default=False,
75
+ help="Auto-promote the deployment. Only works if --wait is enabled. Disabled by default.",
76
+ )
77
+ def deploy(project_path: Path, wait: bool, auto: bool) -> None:
78
+ """
79
+ Validate and deploy the project server side.
80
+ """
81
+ # TODO: This code is duplicated in build_server.py
82
+ # Should be refactored to be shared
83
+ MULTIPART_BOUNDARY_DATA_PROJECT = "data_project://"
84
+ DATAFILE_TYPE_TO_CONTENT_TYPE = {
85
+ ".datasource": "text/plain",
86
+ ".pipe": "text/plain",
87
+ }
88
+
89
+ config = CLIConfig.get_project_config(str(project_path))
90
+ TINYBIRD_API_URL = (config.get_host() or "") + "/v1/deploy"
91
+ TINYBIRD_API_KEY = config.get_token()
92
+
93
+ files = [
94
+ ("context://", ("cli-version", "1.0.0", "text/plain")),
95
+ ]
96
+ fds = []
97
+ for file_path in project_files(project_path):
98
+ relative_path = str(Path(file_path).relative_to(project_path))
99
+ fd = open(file_path, "rb")
100
+ fds.append(fd)
101
+ content_type = DATAFILE_TYPE_TO_CONTENT_TYPE.get(Path(file_path).suffix, "application/unknown")
102
+ files.append((MULTIPART_BOUNDARY_DATA_PROJECT, (relative_path, fd.read().decode("utf-8"), content_type)))
103
+
104
+ deployment = None
105
+ try:
106
+ HEADERS = {"Authorization": f"Bearer {TINYBIRD_API_KEY}"}
107
+
108
+ r = requests.post(TINYBIRD_API_URL, files=files, headers=HEADERS)
109
+ result = r.json()
110
+ logging.debug(json.dumps(result, indent=2))
111
+
112
+ deploy_result = result.get("result")
113
+ if deploy_result == "success":
114
+ click.echo(FeedbackManager.success(message="Deploy submitted successfully"))
115
+ deployment = result.get("deployment")
116
+ elif deploy_result == "failed":
117
+ click.echo(FeedbackManager.error(message="Deploy failed"))
118
+ deploy_errors = result.get("errors")
119
+ for deploy_error in deploy_errors:
120
+ if deploy_error.get("filename", None):
121
+ click.echo(
122
+ FeedbackManager.error(message=f"{deploy_error.get('filename')}\n\n{deploy_error.get('error')}")
123
+ )
124
+ else:
125
+ click.echo(FeedbackManager.error(message=f"{deploy_error.get('error')}"))
126
+ else:
127
+ click.echo(FeedbackManager.error(message=f"Unknown build result {deploy_result}"))
128
+ finally:
129
+ for fd in fds:
130
+ fd.close()
131
+
132
+ if deployment and wait:
133
+ while deployment.get("status") != "data_ready":
134
+ time.sleep(5)
135
+ TINYBIRD_API_URL = (config.get_host() or "") + f"/v1/deployments/{deployment.get('id')}"
136
+ r = requests.get(TINYBIRD_API_URL, headers=HEADERS)
137
+ result = r.json()
138
+ deployment = result.get("deployment")
139
+ if deployment.get("status") == "failed":
140
+ click.echo(FeedbackManager.error(message="Deployment failed"))
141
+ return
142
+
143
+ click.echo(FeedbackManager.success(message="Deployment is ready"))
144
+
145
+ if auto:
146
+ promote_deployment((config.get_host() or ""), HEADERS)
147
+
148
+
149
+ @cli.command(name="release")
150
+ @click.argument("project_path", type=click.Path(exists=True), default=Path.cwd())
151
+ def deploy_promote(project_path: Path) -> None:
152
+ """
153
+ Promote last deploy to ready and remove old one.
154
+ """
155
+ config = CLIConfig.get_project_config(str(project_path))
156
+
157
+ TINYBIRD_API_KEY = config.get_token()
158
+ HEADERS = {"Authorization": f"Bearer {TINYBIRD_API_KEY}"}
159
+
160
+ promote_deployment((config.get_host() or ""), HEADERS)
@@ -1,10 +1,8 @@
1
- import asyncio
2
1
  import json
3
2
  import urllib.parse
4
3
  from copy import deepcopy
5
- from typing import Awaitable, Callable, List, Optional
4
+ from typing import List
6
5
 
7
- from openai import OpenAI
8
6
  from pydantic import BaseModel
9
7
 
10
8
  from tinybird.client import TinyB
@@ -31,25 +29,43 @@ class TestExpectations(BaseModel):
31
29
 
32
30
 
33
31
  class LLM:
34
- def __init__(self, user_token: str, client: TinyB, api_key: Optional[str] = None):
32
+ def __init__(
33
+ self,
34
+ user_token: str,
35
+ client: TinyB,
36
+ ):
35
37
  self.user_client = deepcopy(client)
36
38
  self.user_client.token = user_token
37
39
 
38
- self.openai = OpenAI(api_key=api_key) if api_key else None
40
+ async def ask(self, prompt: str, system_prompt: str = "", model: str = "o1-mini") -> str:
41
+ """
42
+ Calls the model with the given prompt and returns the response.
39
43
 
40
- async def _execute(self, action_fn: Callable[[], Awaitable[str]], checker_fn: Callable[[str], bool]):
41
- is_valid = False
42
- times = 0
44
+ Args:
45
+ prompt (str): The user prompt to send to the model.
43
46
 
44
- while not is_valid and times < 5:
45
- result = await action_fn()
46
- if asyncio.iscoroutinefunction(checker_fn):
47
- is_valid = await checker_fn(result)
48
- else:
49
- is_valid = checker_fn(result)
50
- times += 1
47
+ Returns:
48
+ str: The response from the language model.
49
+ """
50
+ messages = []
51
51
 
52
- return result
52
+ if system_prompt:
53
+ messages.append({"role": "user", "content": system_prompt})
54
+
55
+ if prompt:
56
+ messages.append({"role": "user", "content": prompt})
57
+
58
+ data = {
59
+ "model": model,
60
+ "messages": messages,
61
+ }
62
+ response = await self.user_client._req(
63
+ "/v0/llm",
64
+ method="POST",
65
+ data=json.dumps(data),
66
+ headers={"Content-Type": "application/json"},
67
+ )
68
+ return response.get("result", "")
53
69
 
54
70
  async def create_project(self, prompt: str) -> DataProject:
55
71
  try: