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.
- tinybird/config.py +1 -1
- tinybird/datatypes.py +46 -57
- tinybird/git_settings.py +4 -4
- tinybird/prompts.py +644 -0
- tinybird/sql.py +9 -0
- tinybird/sql_toolset.py +17 -3
- tinybird/syncasync.py +1 -1
- tinybird/tb/__cli__.py +2 -2
- tinybird/tb/cli.py +2 -0
- tinybird/tb/modules/build.py +47 -19
- tinybird/tb/modules/build_server.py +75 -0
- tinybird/tb/modules/cli.py +22 -0
- tinybird/tb/modules/common.py +2 -2
- tinybird/tb/modules/config.py +13 -14
- tinybird/tb/modules/create.py +125 -120
- tinybird/tb/modules/datafile/build.py +28 -0
- tinybird/tb/modules/datafile/common.py +1 -0
- tinybird/tb/modules/datafile/fixture.py +10 -6
- tinybird/tb/modules/datafile/parse_pipe.py +2 -0
- tinybird/tb/modules/datasource.py +1 -1
- tinybird/tb/modules/deploy.py +160 -0
- tinybird/tb/modules/llm.py +32 -16
- tinybird/tb/modules/llm_utils.py +24 -0
- tinybird/tb/modules/local.py +2 -2
- tinybird/tb/modules/login.py +8 -6
- tinybird/tb/modules/mock.py +13 -9
- tinybird/tb/modules/test.py +69 -47
- tinybird/tb/modules/watch.py +2 -2
- tinybird/tb_cli_modules/common.py +2 -2
- tinybird/tb_cli_modules/config.py +5 -5
- tinybird/tornado_template.py +1 -3
- {tinybird-0.0.1.dev25.dist-info → tinybird-0.0.1.dev27.dist-info}/METADATA +1 -1
- {tinybird-0.0.1.dev25.dist-info → tinybird-0.0.1.dev27.dist-info}/RECORD +36 -33
- {tinybird-0.0.1.dev25.dist-info → tinybird-0.0.1.dev27.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev25.dist-info → tinybird-0.0.1.dev27.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev25.dist-info → tinybird-0.0.1.dev27.dist-info}/top_level.txt +0 -0
tinybird/tb/modules/create.py
CHANGED
|
@@ -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=
|
|
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
|
-
|
|
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
|
-
|
|
88
|
+
if validate_fixtures(folder):
|
|
89
|
+
click.echo(FeedbackManager.highlight(message="\n» Generating fixtures..."))
|
|
84
90
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
147
|
+
async def create_resources(
|
|
176
148
|
local_client: TinyB,
|
|
177
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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,
|
|
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
|
|
222
|
-
content = pipe
|
|
223
|
-
generate_pipe_file(pipe
|
|
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(
|
|
49
|
-
|
|
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[
|
|
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)
|
tinybird/tb/modules/llm.py
CHANGED
|
@@ -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
|
|
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__(
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
times = 0
|
|
44
|
+
Args:
|
|
45
|
+
prompt (str): The user prompt to send to the model.
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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:
|