tinybird 0.0.1.dev29__py3-none-any.whl → 0.0.1.dev30__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 +1 -1
- tinybird/prompts.py +218 -325
- tinybird/tb/__cli__.py +2 -2
- tinybird/tb/cli.py +2 -1
- tinybird/tb/modules/build.py +61 -201
- tinybird/tb/modules/build_client.py +219 -0
- tinybird/tb/modules/cli.py +52 -16
- tinybird/tb/modules/common.py +1 -26
- tinybird/tb/modules/config.py +0 -8
- tinybird/tb/modules/create.py +20 -1
- tinybird/tb/modules/datafile/build.py +2 -2
- tinybird/tb/modules/datafile/build_pipe.py +13 -1
- tinybird/tb/modules/datasource.py +1 -1
- tinybird/tb/modules/llm.py +19 -4
- tinybird/tb/modules/local.py +1 -1
- tinybird/tb/modules/login.py +7 -2
- tinybird/tb/modules/shell.py +2 -2
- tinybird/tb/modules/test.py +41 -22
- tinybird/tb/modules/update.py +182 -0
- {tinybird-0.0.1.dev29.dist-info → tinybird-0.0.1.dev30.dist-info}/METADATA +2 -1
- {tinybird-0.0.1.dev29.dist-info → tinybird-0.0.1.dev30.dist-info}/RECORD +24 -23
- tinybird/tb/modules/build_server.py +0 -75
- {tinybird-0.0.1.dev29.dist-info → tinybird-0.0.1.dev30.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev29.dist-info → tinybird-0.0.1.dev30.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev29.dist-info → tinybird-0.0.1.dev30.dist-info}/top_level.txt +0 -0
tinybird/tb/modules/common.py
CHANGED
|
@@ -175,7 +175,7 @@ def generate_datafile(
|
|
|
175
175
|
if not f.exists() or force:
|
|
176
176
|
with open(f"{f}", "w") as ds_file:
|
|
177
177
|
ds_file.write(datafile)
|
|
178
|
-
click.echo(FeedbackManager.info_file_created(file=f.relative_to(folder)))
|
|
178
|
+
click.echo(FeedbackManager.info_file_created(file=f.relative_to(folder or ".")))
|
|
179
179
|
|
|
180
180
|
if data and (base / "fixtures").exists():
|
|
181
181
|
# Generating a fixture for Parquet files is not so trivial, since Parquet format
|
|
@@ -1177,31 +1177,6 @@ async def print_current_workspace(config: CLIConfig) -> None:
|
|
|
1177
1177
|
echo_safe_humanfriendly_tables_format_smart_table(table, column_names=columns)
|
|
1178
1178
|
|
|
1179
1179
|
|
|
1180
|
-
async def print_current_branch(config: CLIConfig) -> None:
|
|
1181
|
-
_ = await try_update_config_with_remote(config, only_if_needed=True)
|
|
1182
|
-
|
|
1183
|
-
response = await config.get_client().user_workspaces_and_branches()
|
|
1184
|
-
|
|
1185
|
-
columns = ["name", "id", "workspace"]
|
|
1186
|
-
table = []
|
|
1187
|
-
|
|
1188
|
-
for workspace in response["workspaces"]:
|
|
1189
|
-
if config["id"] == workspace["id"]:
|
|
1190
|
-
click.echo(FeedbackManager.info_current_branch())
|
|
1191
|
-
if workspace.get("is_branch"):
|
|
1192
|
-
name = workspace["name"]
|
|
1193
|
-
main_workspace = await get_current_main_workspace(config)
|
|
1194
|
-
assert isinstance(main_workspace, dict)
|
|
1195
|
-
main_name = main_workspace["name"]
|
|
1196
|
-
else:
|
|
1197
|
-
name = MAIN_BRANCH
|
|
1198
|
-
main_name = workspace["name"]
|
|
1199
|
-
table.append([name, workspace["id"], main_name])
|
|
1200
|
-
break
|
|
1201
|
-
|
|
1202
|
-
echo_safe_humanfriendly_tables_format_smart_table(table, column_names=columns)
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
1180
|
class ConnectionReplacements:
|
|
1206
1181
|
_PARAMS_REPLACEMENTS: Dict[str, Dict[str, str]] = {
|
|
1207
1182
|
"s3": {
|
tinybird/tb/modules/config.py
CHANGED
|
@@ -323,14 +323,6 @@ class CLIConfig:
|
|
|
323
323
|
path: str = os.path.join(working_dir, ".tinyb")
|
|
324
324
|
return CLIConfig(path, parent=CLIConfig.get_global_config())
|
|
325
325
|
|
|
326
|
-
@staticmethod
|
|
327
|
-
def get_llm_config(working_dir: Optional[str] = None) -> Dict[str, Any]:
|
|
328
|
-
return (
|
|
329
|
-
CLIConfig.get_project_config(working_dir)
|
|
330
|
-
.get("llms", {})
|
|
331
|
-
.get("openai", {"model": "gpt-4o-mini", "api_key": None})
|
|
332
|
-
)
|
|
333
|
-
|
|
334
326
|
@staticmethod
|
|
335
327
|
def reset() -> None:
|
|
336
328
|
CLIConfig._global = None
|
tinybird/tb/modules/create.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import re
|
|
2
3
|
from os import getcwd
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
from typing import Optional
|
|
@@ -260,7 +261,25 @@ def init_git(folder: str):
|
|
|
260
261
|
|
|
261
262
|
|
|
262
263
|
def generate_pipe_file(name: str, content: str, folder: str):
|
|
263
|
-
|
|
264
|
+
def is_copy(content: str) -> bool:
|
|
265
|
+
return re.search(r"TYPE copy", content, re.IGNORECASE) is not None
|
|
266
|
+
|
|
267
|
+
def is_materialization(content: str) -> bool:
|
|
268
|
+
return re.search(r"TYPE materialized", content, re.IGNORECASE) is not None
|
|
269
|
+
|
|
270
|
+
def is_sink(content: str) -> bool:
|
|
271
|
+
return re.search(r"TYPE sink", content, re.IGNORECASE) is not None
|
|
272
|
+
|
|
273
|
+
if is_copy(content):
|
|
274
|
+
pathname = "copies"
|
|
275
|
+
elif is_materialization(content):
|
|
276
|
+
pathname = "materializations"
|
|
277
|
+
elif is_sink(content):
|
|
278
|
+
pathname = "sinks"
|
|
279
|
+
else:
|
|
280
|
+
pathname = "endpoints"
|
|
281
|
+
|
|
282
|
+
base = Path(folder) / pathname
|
|
264
283
|
if not base.exists():
|
|
265
284
|
base = Path()
|
|
266
285
|
f = base / (f"{name}.pipe")
|
|
@@ -768,13 +768,13 @@ async def process(
|
|
|
768
768
|
raise click.ClickException(FeedbackManager.error_forkdownstream_pipes_with_engine(pipe=resource_name))
|
|
769
769
|
|
|
770
770
|
to_run[resource_name] = r
|
|
771
|
-
file_deps = r.get("deps", [])
|
|
771
|
+
file_deps: List[str] = r.get("deps", [])
|
|
772
772
|
deps += file_deps
|
|
773
773
|
# calculate and look for deps
|
|
774
774
|
dep_list = []
|
|
775
775
|
for x in file_deps:
|
|
776
776
|
if x not in INTERNAL_TABLES or is_internal:
|
|
777
|
-
f, ds = find_file_by_name(dir_path, x, verbose, vendor_paths=vendor_paths, resource=r)
|
|
777
|
+
f, ds = find_file_by_name(dir_path or ".", x, verbose, vendor_paths=vendor_paths, resource=r)
|
|
778
778
|
if f:
|
|
779
779
|
dep_list.append(f.rsplit(".", 1)[0])
|
|
780
780
|
if ds:
|
|
@@ -266,7 +266,19 @@ async def new_pipe(
|
|
|
266
266
|
|
|
267
267
|
if data.get("type") == "endpoint":
|
|
268
268
|
token = tb_client.token
|
|
269
|
-
|
|
269
|
+
try:
|
|
270
|
+
example_params = {
|
|
271
|
+
"format": "json",
|
|
272
|
+
"pipe": p["name"],
|
|
273
|
+
"q": "",
|
|
274
|
+
"token": token,
|
|
275
|
+
}
|
|
276
|
+
endpoint_url = await tb_client._req(f"/examples/query.http?{urlencode(example_params)}")
|
|
277
|
+
if endpoint_url:
|
|
278
|
+
endpoint_url = endpoint_url.replace("http://localhost:8001", host)
|
|
279
|
+
click.echo(f"""** => Test endpoint with:\n** $ curl {endpoint_url}""")
|
|
280
|
+
except Exception:
|
|
281
|
+
pass
|
|
270
282
|
|
|
271
283
|
|
|
272
284
|
async def get_token_from_main_branch(branch_tb_client: TinyB) -> Optional[str]:
|
|
@@ -542,7 +542,7 @@ async def datasource_share(ctx: Context, datasource_name: str, workspace_name_or
|
|
|
542
542
|
"""Share a datasource"""
|
|
543
543
|
|
|
544
544
|
config = CLIConfig.get_project_config()
|
|
545
|
-
client =
|
|
545
|
+
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
546
546
|
host = config.get_host() or CLIConfig.DEFAULTS["host"]
|
|
547
547
|
ui_host = get_display_host(host)
|
|
548
548
|
|
tinybird/tb/modules/llm.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import os
|
|
2
3
|
import urllib.parse
|
|
3
4
|
from copy import deepcopy
|
|
4
|
-
from typing import List
|
|
5
|
+
from typing import Any, List
|
|
5
6
|
|
|
7
|
+
from anthropic import AnthropicVertex
|
|
6
8
|
from pydantic import BaseModel
|
|
7
9
|
|
|
8
10
|
from tinybird.client import TinyB
|
|
@@ -37,7 +39,7 @@ class LLM:
|
|
|
37
39
|
self.user_client = deepcopy(client)
|
|
38
40
|
self.user_client.token = user_token
|
|
39
41
|
|
|
40
|
-
async def ask(self, prompt: str, system_prompt: str = ""
|
|
42
|
+
async def ask(self, prompt: str, system_prompt: str = "") -> str:
|
|
41
43
|
"""
|
|
42
44
|
Calls the model with the given prompt and returns the response.
|
|
43
45
|
|
|
@@ -47,7 +49,7 @@ class LLM:
|
|
|
47
49
|
Returns:
|
|
48
50
|
str: The response from the language model.
|
|
49
51
|
"""
|
|
50
|
-
messages = []
|
|
52
|
+
messages: List[Any] = []
|
|
51
53
|
|
|
52
54
|
if system_prompt:
|
|
53
55
|
messages.append({"role": "user", "content": system_prompt})
|
|
@@ -55,8 +57,21 @@ class LLM:
|
|
|
55
57
|
if prompt:
|
|
56
58
|
messages.append({"role": "user", "content": prompt})
|
|
57
59
|
|
|
60
|
+
if gcloud_access_token := os.getenv("GCLOUD_ACCESS_TOKEN"):
|
|
61
|
+
client = AnthropicVertex(
|
|
62
|
+
region="europe-west1",
|
|
63
|
+
project_id="gen-lang-client-0705305160",
|
|
64
|
+
access_token=gcloud_access_token,
|
|
65
|
+
)
|
|
66
|
+
message = client.messages.create(
|
|
67
|
+
max_tokens=8000,
|
|
68
|
+
messages=messages,
|
|
69
|
+
model="claude-3-5-sonnet-v2@20241022",
|
|
70
|
+
)
|
|
71
|
+
return message.content[0].text or "" # type: ignore
|
|
72
|
+
|
|
58
73
|
data = {
|
|
59
|
-
"model":
|
|
74
|
+
"model": "o1-mini",
|
|
60
75
|
"messages": messages,
|
|
61
76
|
}
|
|
62
77
|
response = await self.user_client._req(
|
tinybird/tb/modules/local.py
CHANGED
|
@@ -82,7 +82,7 @@ def get_docker_client():
|
|
|
82
82
|
client.ping()
|
|
83
83
|
return client
|
|
84
84
|
except Exception:
|
|
85
|
-
raise CLIException("
|
|
85
|
+
raise CLIException("No container runtime is running. Make sure a Docker-compatible runtime is installed and running.")
|
|
86
86
|
|
|
87
87
|
|
|
88
88
|
def stop_tinybird_local(docker_client):
|
tinybird/tb/modules/login.py
CHANGED
|
@@ -135,7 +135,12 @@ async def login(host: str, workspace: str):
|
|
|
135
135
|
server_thread.start()
|
|
136
136
|
|
|
137
137
|
# Open the browser to the auth page
|
|
138
|
-
|
|
138
|
+
if "wadus" in host:
|
|
139
|
+
client_id = "Rpl7Uy9aSjqoPCSvHgGl3zNQuZcSOXBe"
|
|
140
|
+
base_auth_url = "https://auth.wadus1.tinybird.co"
|
|
141
|
+
else:
|
|
142
|
+
client_id = "T6excMo8IKguvUw4vFNYfqlt9pe6msCU"
|
|
143
|
+
base_auth_url = "https://auth.tinybird.co"
|
|
139
144
|
callback_url = f"http://localhost:{AUTH_SERVER_PORT}"
|
|
140
145
|
params = {
|
|
141
146
|
"client_id": client_id,
|
|
@@ -143,7 +148,7 @@ async def login(host: str, workspace: str):
|
|
|
143
148
|
"response_type": "token",
|
|
144
149
|
"scope": "openid profile email",
|
|
145
150
|
}
|
|
146
|
-
auth_url = f"
|
|
151
|
+
auth_url = f"{base_auth_url}/authorize?{urlencode(params)}"
|
|
147
152
|
webbrowser.open(auth_url)
|
|
148
153
|
|
|
149
154
|
# Wait for the authentication to complete or timeout
|
tinybird/tb/modules/shell.py
CHANGED
|
@@ -274,7 +274,7 @@ class Shell:
|
|
|
274
274
|
elif arg.startswith("mock"):
|
|
275
275
|
self.handle_mock(arg)
|
|
276
276
|
else:
|
|
277
|
-
subprocess.run(f"tb
|
|
277
|
+
subprocess.run(f"tb {arg}", shell=True, text=True)
|
|
278
278
|
|
|
279
279
|
def default(self, argline):
|
|
280
280
|
click.echo("")
|
|
@@ -286,7 +286,7 @@ class Shell:
|
|
|
286
286
|
elif len(arg.split()) == 1 and arg in self.endpoints + self.pipes + self.datasources + self.shared_datasources:
|
|
287
287
|
self.run_sql(f"select * from {arg}")
|
|
288
288
|
else:
|
|
289
|
-
subprocess.run(f"tb
|
|
289
|
+
subprocess.run(f"tb {arg}", shell=True, text=True)
|
|
290
290
|
|
|
291
291
|
def run_sql(self, query, rows_limit=20):
|
|
292
292
|
try:
|
tinybird/tb/modules/test.py
CHANGED
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
import difflib
|
|
7
7
|
import glob
|
|
8
8
|
import os
|
|
9
|
+
import urllib.parse
|
|
9
10
|
from pathlib import Path
|
|
10
|
-
from typing import Any, Dict,
|
|
11
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
11
12
|
|
|
12
13
|
import click
|
|
13
14
|
import yaml
|
|
15
|
+
from requests import Response
|
|
14
16
|
|
|
15
17
|
from tinybird.prompts import test_create_prompt
|
|
16
18
|
from tinybird.tb.modules.cli import cli
|
|
@@ -66,9 +68,9 @@ def test(ctx: click.Context) -> None:
|
|
|
66
68
|
type=click.Path(exists=True, file_okay=False),
|
|
67
69
|
help="Folder where datafiles will be placed",
|
|
68
70
|
)
|
|
69
|
-
@click.option("--prompt", type=str, default=
|
|
71
|
+
@click.option("--prompt", type=str, default="", help="Prompt to be used to create the test")
|
|
70
72
|
@coro
|
|
71
|
-
async def test_create(name_or_filename: str, prompt:
|
|
73
|
+
async def test_create(name_or_filename: str, prompt: str, folder: str) -> None:
|
|
72
74
|
"""
|
|
73
75
|
Create a test for an existing pipe
|
|
74
76
|
"""
|
|
@@ -80,15 +82,13 @@ async def test_create(name_or_filename: str, prompt: Optional[str], folder: str)
|
|
|
80
82
|
raise CLIException(FeedbackManager.error(message=f"Pipe {name_or_filename} not found"))
|
|
81
83
|
else:
|
|
82
84
|
pipe_folders = ("endpoints", "copies", "materializations", "sinks", "pipes")
|
|
83
|
-
|
|
84
|
-
(
|
|
85
|
+
try:
|
|
86
|
+
pipe_path = next(
|
|
85
87
|
root_path / folder / f"{name_or_filename}.pipe"
|
|
86
88
|
for folder in pipe_folders
|
|
87
89
|
if (root_path / folder / f"{name_or_filename}.pipe").exists()
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
)
|
|
91
|
-
if not pipe_path:
|
|
90
|
+
)
|
|
91
|
+
except Exception:
|
|
92
92
|
raise CLIException(FeedbackManager.error(message=f"Pipe {name_or_filename} not found"))
|
|
93
93
|
|
|
94
94
|
pipe_name = pipe_path.stem
|
|
@@ -97,8 +97,8 @@ async def test_create(name_or_filename: str, prompt: Optional[str], folder: str)
|
|
|
97
97
|
pipe_content = pipe_path.read_text()
|
|
98
98
|
|
|
99
99
|
client = await get_tinybird_local_client(os.path.abspath(folder))
|
|
100
|
-
|
|
101
|
-
parameters = set([param["name"] for node in
|
|
100
|
+
pipe = await client._req(f"/v0/pipes/{pipe_name}")
|
|
101
|
+
parameters = set([param["name"] for node in pipe["nodes"] for param in node["params"]])
|
|
102
102
|
|
|
103
103
|
system_prompt = test_create_prompt.format(
|
|
104
104
|
name=pipe_name,
|
|
@@ -107,24 +107,26 @@ async def test_create(name_or_filename: str, prompt: Optional[str], folder: str)
|
|
|
107
107
|
)
|
|
108
108
|
config = CLIConfig.get_project_config(folder)
|
|
109
109
|
user_token = config.get_user_token()
|
|
110
|
+
if not user_token:
|
|
111
|
+
raise CLIException(FeedbackManager.error(message="No user token found"))
|
|
110
112
|
llm = LLM(user_token=user_token, client=config.get_client())
|
|
111
113
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
tests_content = parse_xml(
|
|
114
|
+
response_llm = await llm.ask(prompt, system_prompt=system_prompt)
|
|
115
|
+
response_xml = extract_xml(response_llm, "response")
|
|
116
|
+
tests_content = parse_xml(response_xml, "test")
|
|
115
117
|
|
|
116
118
|
tests: List[Dict[str, Any]] = []
|
|
117
119
|
for test_content in tests_content:
|
|
118
|
-
test = {}
|
|
120
|
+
test: Dict[str, Any] = {}
|
|
119
121
|
test["name"] = extract_xml(test_content, "name")
|
|
120
122
|
test["description"] = extract_xml(test_content, "description")
|
|
121
|
-
|
|
122
|
-
test["parameters"] =
|
|
123
|
+
parameters_api = extract_xml(test_content, "parameters")
|
|
124
|
+
test["parameters"] = parameters_api.split("?")[1] if "?" in parameters_api else parameters_api
|
|
123
125
|
test["expected_result"] = ""
|
|
124
126
|
|
|
125
127
|
response = None
|
|
126
128
|
try:
|
|
127
|
-
response = await client
|
|
129
|
+
response = await get_pipe_data(client, pipe_name=pipe_name, test_params=test["parameters"])
|
|
128
130
|
except Exception:
|
|
129
131
|
pass
|
|
130
132
|
|
|
@@ -181,7 +183,7 @@ async def test_update(pipe: str, folder: str) -> None:
|
|
|
181
183
|
test_params = test["parameters"].split("?")[1] if "?" in test["parameters"] else test["parameters"]
|
|
182
184
|
response = None
|
|
183
185
|
try:
|
|
184
|
-
response = await client
|
|
186
|
+
response = await get_pipe_data(client, pipe_name=pipe_name, test_params=test_params)
|
|
185
187
|
except Exception:
|
|
186
188
|
continue
|
|
187
189
|
|
|
@@ -221,20 +223,21 @@ async def run_tests(name: Tuple[str, ...], folder: str) -> None:
|
|
|
221
223
|
client = await get_tinybird_local_client(os.path.abspath(folder))
|
|
222
224
|
paths = [Path(n) for n in name]
|
|
223
225
|
endpoints = [f"./tests/{p.stem}.yaml" for p in paths]
|
|
224
|
-
test_files:
|
|
226
|
+
test_files: List[str] = endpoints if len(endpoints) > 0 else glob.glob("./tests/**/*.y*ml", recursive=True)
|
|
225
227
|
|
|
226
228
|
async def run_test(test_file):
|
|
227
229
|
test_file_path = Path(test_file)
|
|
228
230
|
click.echo(FeedbackManager.info(message=f"\n* {test_file_path.stem}{test_file_path.suffix}"))
|
|
229
231
|
test_file_content = yaml.safe_load(test_file_path.read_text())
|
|
232
|
+
|
|
230
233
|
for test in test_file_content:
|
|
231
234
|
try:
|
|
232
235
|
test_params = test["parameters"].split("?")[1] if "?" in test["parameters"] else test["parameters"]
|
|
233
236
|
response = None
|
|
234
237
|
try:
|
|
235
|
-
response = await client
|
|
238
|
+
response = await get_pipe_data(client, pipe_name=test_file_path.stem, test_params=test_params)
|
|
236
239
|
except Exception:
|
|
237
|
-
|
|
240
|
+
continue
|
|
238
241
|
|
|
239
242
|
expected_result = response.text
|
|
240
243
|
if response.status_code >= 400:
|
|
@@ -271,3 +274,19 @@ async def run_tests(name: Tuple[str, ...], folder: str) -> None:
|
|
|
271
274
|
exit(1)
|
|
272
275
|
else:
|
|
273
276
|
click.echo(FeedbackManager.success(message=f"\n✓ {test_count}/{test_count} passed"))
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
async def get_pipe_data(client, pipe_name: str, test_params: str) -> Response:
|
|
280
|
+
pipe = await client._req(f"/v0/pipes/{pipe_name}")
|
|
281
|
+
output_node = next(
|
|
282
|
+
(node for node in pipe["nodes"] if node["node_type"] != "default" and node["node_type"] != "standard"),
|
|
283
|
+
{"name": "not_found"},
|
|
284
|
+
)
|
|
285
|
+
if output_node["node_type"] == "endpoint":
|
|
286
|
+
return await client._req_raw(f"/v0/pipes/{pipe_name}.ndjson?{test_params}")
|
|
287
|
+
|
|
288
|
+
params = {
|
|
289
|
+
"q": output_node["sql"],
|
|
290
|
+
"pipeline": pipe_name,
|
|
291
|
+
}
|
|
292
|
+
return await client._req_raw(f"""/v0/sql?{urllib.parse.urlencode(params)}&{test_params}""")
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
from os import getcwd
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from tinybird.client import TinyB
|
|
10
|
+
from tinybird.prompts import mock_prompt, update_prompt
|
|
11
|
+
from tinybird.tb.modules.cli import cli
|
|
12
|
+
from tinybird.tb.modules.common import check_user_token_with_client, coro, generate_datafile
|
|
13
|
+
from tinybird.tb.modules.config import CLIConfig
|
|
14
|
+
from tinybird.tb.modules.datafile.fixture import build_fixture_name, persist_fixture
|
|
15
|
+
from tinybird.tb.modules.exceptions import CLIException
|
|
16
|
+
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
17
|
+
from tinybird.tb.modules.llm import LLM
|
|
18
|
+
from tinybird.tb.modules.llm_utils import extract_xml, parse_xml
|
|
19
|
+
from tinybird.tb.modules.local_common import get_tinybird_local_client
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@cli.command()
|
|
23
|
+
@click.argument("prompt")
|
|
24
|
+
@click.option(
|
|
25
|
+
"--folder",
|
|
26
|
+
default=".",
|
|
27
|
+
type=click.Path(exists=False, file_okay=False),
|
|
28
|
+
help="Folder where project files will be placed",
|
|
29
|
+
)
|
|
30
|
+
@coro
|
|
31
|
+
async def update(
|
|
32
|
+
prompt: str,
|
|
33
|
+
folder: str,
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Update resources in the project."""
|
|
36
|
+
folder = folder or getcwd()
|
|
37
|
+
folder_path = Path(folder)
|
|
38
|
+
if not folder_path.exists():
|
|
39
|
+
folder_path.mkdir()
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
config = CLIConfig.get_project_config(folder)
|
|
43
|
+
tb_client = config.get_client()
|
|
44
|
+
user_token: Optional[str] = None
|
|
45
|
+
try:
|
|
46
|
+
user_token = config.get_user_token()
|
|
47
|
+
if not user_token:
|
|
48
|
+
raise CLIException("No user token found")
|
|
49
|
+
await check_user_token_with_client(tb_client, token=user_token)
|
|
50
|
+
except Exception as e:
|
|
51
|
+
click.echo(
|
|
52
|
+
FeedbackManager.error(message=f"This action requires authentication. Run 'tb login' first. Error: {e}")
|
|
53
|
+
)
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
local_client = await get_tinybird_local_client(folder)
|
|
57
|
+
|
|
58
|
+
click.echo(FeedbackManager.highlight(message="\n» Updating resources..."))
|
|
59
|
+
datasources_updated = await update_resources(tb_client, user_token, prompt, folder)
|
|
60
|
+
click.echo(FeedbackManager.success(message="✓ Done!\n"))
|
|
61
|
+
|
|
62
|
+
if datasources_updated and user_token:
|
|
63
|
+
click.echo(FeedbackManager.highlight(message="\n» Generating fixtures..."))
|
|
64
|
+
|
|
65
|
+
datasource_files = [f for f in os.listdir(Path(folder) / "datasources") if f.endswith(".datasource")]
|
|
66
|
+
for datasource_file in datasource_files:
|
|
67
|
+
datasource_path = Path(folder) / "datasources" / datasource_file
|
|
68
|
+
llm = LLM(user_token=user_token, client=tb_client)
|
|
69
|
+
datasource_name = datasource_path.stem
|
|
70
|
+
datasource_content = datasource_path.read_text()
|
|
71
|
+
has_json_path = "`json:" in datasource_content
|
|
72
|
+
if has_json_path:
|
|
73
|
+
prompt = f"<datasource_schema>{datasource_content}</datasource_schema>\n<user_input>{prompt}</user_input>"
|
|
74
|
+
response = await llm.ask(prompt, system_prompt=mock_prompt(rows=20))
|
|
75
|
+
sql = extract_xml(response, "sql")
|
|
76
|
+
sql = sql.split("FORMAT")[0]
|
|
77
|
+
result = await local_client.query(f"{sql} FORMAT JSON")
|
|
78
|
+
data = result.get("data", [])
|
|
79
|
+
fixture_name = build_fixture_name(
|
|
80
|
+
datasource_path.absolute().as_posix(), datasource_name, datasource_content
|
|
81
|
+
)
|
|
82
|
+
if data:
|
|
83
|
+
persist_fixture(fixture_name, data, folder)
|
|
84
|
+
click.echo(FeedbackManager.info(message=f"✓ /fixtures/{datasource_name}"))
|
|
85
|
+
|
|
86
|
+
click.echo(FeedbackManager.success(message="✓ Done!\n"))
|
|
87
|
+
except Exception as e:
|
|
88
|
+
click.echo(FeedbackManager.error(message=f"Error: {str(e)}"))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def update_resources(
|
|
92
|
+
tb_client: TinyB,
|
|
93
|
+
user_token: str,
|
|
94
|
+
prompt: str,
|
|
95
|
+
folder: str,
|
|
96
|
+
):
|
|
97
|
+
datasource_paths = [
|
|
98
|
+
Path(folder) / "datasources" / f for f in os.listdir(Path(folder) / "datasources") if f.endswith(".datasource")
|
|
99
|
+
]
|
|
100
|
+
pipes_paths = [
|
|
101
|
+
Path(folder) / "endpoints" / f for f in os.listdir(Path(folder) / "endpoints") if f.endswith(".pipe")
|
|
102
|
+
]
|
|
103
|
+
resources_xml = "\n".join(
|
|
104
|
+
[
|
|
105
|
+
f"<resource><type>{resource_type}</type><name>{resource_name}</name><content>{resource_content}</content></resource>"
|
|
106
|
+
for resource_type, resource_name, resource_content in [
|
|
107
|
+
("datasource", ds.stem, ds.read_text()) for ds in datasource_paths
|
|
108
|
+
]
|
|
109
|
+
+ [
|
|
110
|
+
(
|
|
111
|
+
"pipe",
|
|
112
|
+
pipe.stem,
|
|
113
|
+
pipe.read_text(),
|
|
114
|
+
)
|
|
115
|
+
for pipe in pipes_paths
|
|
116
|
+
]
|
|
117
|
+
]
|
|
118
|
+
)
|
|
119
|
+
llm = LLM(user_token=user_token, client=tb_client)
|
|
120
|
+
result = await llm.ask(prompt, system_prompt=update_prompt(resources_xml))
|
|
121
|
+
result = extract_xml(result, "response")
|
|
122
|
+
resources = parse_xml(result, "resource")
|
|
123
|
+
datasources = []
|
|
124
|
+
pipes = []
|
|
125
|
+
for resource_xml in resources:
|
|
126
|
+
resource_type = extract_xml(resource_xml, "type")
|
|
127
|
+
name = extract_xml(resource_xml, "name")
|
|
128
|
+
content = extract_xml(resource_xml, "content")
|
|
129
|
+
resource = {
|
|
130
|
+
"name": name,
|
|
131
|
+
"content": content,
|
|
132
|
+
}
|
|
133
|
+
if resource_type.lower() == "datasource":
|
|
134
|
+
datasources.append(resource)
|
|
135
|
+
elif resource_type.lower() == "pipe":
|
|
136
|
+
pipes.append(resource)
|
|
137
|
+
|
|
138
|
+
for ds in datasources:
|
|
139
|
+
content = ds["content"].replace("```", "")
|
|
140
|
+
filename = f"{ds['name']}.datasource"
|
|
141
|
+
generate_datafile(
|
|
142
|
+
content,
|
|
143
|
+
filename=filename,
|
|
144
|
+
data=None,
|
|
145
|
+
_format="ndjson",
|
|
146
|
+
force=True,
|
|
147
|
+
folder=folder,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
for pipe in pipes:
|
|
151
|
+
content = pipe["content"].replace("```", "")
|
|
152
|
+
generate_pipe_file(pipe["name"], content, folder)
|
|
153
|
+
|
|
154
|
+
return len(datasources) > 0
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def generate_pipe_file(name: str, content: str, folder: str):
|
|
158
|
+
def is_copy(content: str) -> bool:
|
|
159
|
+
return re.search(r"TYPE copy", content, re.IGNORECASE) is not None
|
|
160
|
+
|
|
161
|
+
def is_materialization(content: str) -> bool:
|
|
162
|
+
return re.search(r"TYPE materialized", content, re.IGNORECASE) is not None
|
|
163
|
+
|
|
164
|
+
def is_sink(content: str) -> bool:
|
|
165
|
+
return re.search(r"TYPE sink", content, re.IGNORECASE) is not None
|
|
166
|
+
|
|
167
|
+
if is_copy(content):
|
|
168
|
+
pathname = "copies"
|
|
169
|
+
elif is_materialization(content):
|
|
170
|
+
pathname = "materializations"
|
|
171
|
+
elif is_sink(content):
|
|
172
|
+
pathname = "sinks"
|
|
173
|
+
else:
|
|
174
|
+
pathname = "endpoints"
|
|
175
|
+
|
|
176
|
+
base = Path(folder) / pathname
|
|
177
|
+
if not base.exists():
|
|
178
|
+
base = Path()
|
|
179
|
+
f = base / (f"{name}.pipe")
|
|
180
|
+
with open(f"{f}", "w") as file:
|
|
181
|
+
file.write(content)
|
|
182
|
+
click.echo(FeedbackManager.info_file_created(file=f.relative_to(folder)))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: tinybird
|
|
3
|
-
Version: 0.0.1.
|
|
3
|
+
Version: 0.0.1.dev30
|
|
4
4
|
Summary: Tinybird Command Line Tool
|
|
5
5
|
Home-page: https://www.tinybird.co/docs/cli/introduction.html
|
|
6
6
|
Author: Tinybird
|
|
@@ -8,6 +8,7 @@ Author-email: support@tinybird.co
|
|
|
8
8
|
Requires-Python: >=3.9, <3.13
|
|
9
9
|
Description-Content-Type: text/x-rst
|
|
10
10
|
Requires-Dist: aiofiles (==24.1.0)
|
|
11
|
+
Requires-Dist: anthropic (==0.42.0)
|
|
11
12
|
Requires-Dist: click (<8.2,>=8.1.6)
|
|
12
13
|
Requires-Dist: clickhouse-toolset (==0.33.dev0)
|
|
13
14
|
Requires-Dist: colorama (==0.4.6)
|