tinybird 0.0.1.dev26__py3-none-any.whl → 0.0.1.dev28__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 +647 -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 +44 -16
- 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 +145 -134
- 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 +254 -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 +11 -6
- tinybird/tb/modules/test.py +69 -47
- tinybird/tb/modules/watch.py +1 -1
- 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.dev26.dist-info → tinybird-0.0.1.dev28.dist-info}/METADATA +1 -1
- {tinybird-0.0.1.dev26.dist-info → tinybird-0.0.1.dev28.dist-info}/RECORD +36 -33
- {tinybird-0.0.1.dev26.dist-info → tinybird-0.0.1.dev28.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev26.dist-info → tinybird-0.0.1.dev28.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev26.dist-info → tinybird-0.0.1.dev28.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,254 @@
|
|
|
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.common import echo_safe_humanfriendly_tables_format_smart_table
|
|
13
|
+
from tinybird.tb.modules.config import CLIConfig
|
|
14
|
+
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def project_files(project_path: Path) -> List[str]:
|
|
18
|
+
project_file_extensions = ("datasource", "pipe")
|
|
19
|
+
project_files = []
|
|
20
|
+
for extension in project_file_extensions:
|
|
21
|
+
for project_file in glob.glob(f"{project_path}/**/*.{extension}", recursive=True):
|
|
22
|
+
logging.debug(f"Found project file: {project_file}")
|
|
23
|
+
project_files.append(project_file)
|
|
24
|
+
return project_files
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def promote_deployment(host: str, headers: dict) -> None:
|
|
28
|
+
TINYBIRD_API_URL = host + "/v1/deployments"
|
|
29
|
+
r = requests.get(TINYBIRD_API_URL, headers=headers)
|
|
30
|
+
result = r.json()
|
|
31
|
+
logging.debug(json.dumps(result, indent=2))
|
|
32
|
+
|
|
33
|
+
deployments = result.get("deployments")
|
|
34
|
+
if not deployments:
|
|
35
|
+
click.echo(FeedbackManager.error(message="No deployments found"))
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
if len(deployments) < 2:
|
|
39
|
+
click.echo(FeedbackManager.error(message="Only one deployment found"))
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
last_deployment, candidate_deployment = deployments[0], deployments[1]
|
|
43
|
+
|
|
44
|
+
if candidate_deployment.get("status") != "data_ready":
|
|
45
|
+
click.echo(FeedbackManager.error(message="Current deployment is not ready"))
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
if candidate_deployment.get("live"):
|
|
49
|
+
click.echo(FeedbackManager.error(message="Candidate deployment is already live"))
|
|
50
|
+
else:
|
|
51
|
+
click.echo(FeedbackManager.success(message="Promoting deployment"))
|
|
52
|
+
|
|
53
|
+
TINYBIRD_API_URL = host + f"/v1/deployments/{candidate_deployment.get('id')}/set-live"
|
|
54
|
+
r = requests.post(TINYBIRD_API_URL, headers=headers)
|
|
55
|
+
result = r.json()
|
|
56
|
+
logging.debug(json.dumps(result, indent=2))
|
|
57
|
+
|
|
58
|
+
click.echo(FeedbackManager.success(message="Removing old deployment"))
|
|
59
|
+
|
|
60
|
+
TINYBIRD_API_URL = host + f"/v1/deployments/{last_deployment.get('id')}"
|
|
61
|
+
r = requests.delete(TINYBIRD_API_URL, headers=headers)
|
|
62
|
+
result = r.json()
|
|
63
|
+
logging.debug(json.dumps(result, indent=2))
|
|
64
|
+
|
|
65
|
+
click.echo(FeedbackManager.success(message="Deployment promoted successfully"))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def rollback_deployment(host: str, headers: dict) -> None:
|
|
69
|
+
TINYBIRD_API_URL = host + "/v1/deployments"
|
|
70
|
+
r = requests.get(TINYBIRD_API_URL, headers=headers)
|
|
71
|
+
result = r.json()
|
|
72
|
+
logging.debug(json.dumps(result, indent=2))
|
|
73
|
+
|
|
74
|
+
deployments = result.get("deployments")
|
|
75
|
+
if not deployments:
|
|
76
|
+
click.echo(FeedbackManager.error(message="No deployments found"))
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
if len(deployments) < 2:
|
|
80
|
+
click.echo(FeedbackManager.error(message="Only one deployment found"))
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
previous_deployment, current_deployment = deployments[0], deployments[1]
|
|
84
|
+
|
|
85
|
+
if previous_deployment.get("status") != "data_ready":
|
|
86
|
+
click.echo(FeedbackManager.error(message="Previous deployment is not ready"))
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
if previous_deployment.get("live"):
|
|
90
|
+
click.echo(FeedbackManager.error(message="Previous deployment is already live"))
|
|
91
|
+
else:
|
|
92
|
+
click.echo(FeedbackManager.success(message="Promoting previous deployment"))
|
|
93
|
+
|
|
94
|
+
TINYBIRD_API_URL = host + f"/v1/deployments/{previous_deployment.get('id')}/set-live"
|
|
95
|
+
r = requests.post(TINYBIRD_API_URL, headers=headers)
|
|
96
|
+
result = r.json()
|
|
97
|
+
logging.debug(json.dumps(result, indent=2))
|
|
98
|
+
|
|
99
|
+
click.echo(FeedbackManager.success(message="Removing current deployment"))
|
|
100
|
+
|
|
101
|
+
TINYBIRD_API_URL = host + f"/v1/deployments/{current_deployment.get('id')}"
|
|
102
|
+
r = requests.delete(TINYBIRD_API_URL, headers=headers)
|
|
103
|
+
result = r.json()
|
|
104
|
+
logging.debug(json.dumps(result, indent=2))
|
|
105
|
+
|
|
106
|
+
click.echo(FeedbackManager.success(message="Deployment rolled back successfully"))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@cli.command()
|
|
110
|
+
@click.argument("project_path", type=click.Path(exists=True), default=Path.cwd())
|
|
111
|
+
@click.option(
|
|
112
|
+
"--wait/--no-wait",
|
|
113
|
+
is_flag=True,
|
|
114
|
+
default=False,
|
|
115
|
+
help="Wait for deploy to finish. Disabled by default.",
|
|
116
|
+
)
|
|
117
|
+
@click.option(
|
|
118
|
+
"--auto/--no-auto",
|
|
119
|
+
is_flag=True,
|
|
120
|
+
default=False,
|
|
121
|
+
help="Auto-promote the deployment. Only works if --wait is enabled. Disabled by default.",
|
|
122
|
+
)
|
|
123
|
+
def deploy(project_path: Path, wait: bool, auto: bool) -> None:
|
|
124
|
+
"""
|
|
125
|
+
Validate and deploy the project server side.
|
|
126
|
+
"""
|
|
127
|
+
# TODO: This code is duplicated in build_server.py
|
|
128
|
+
# Should be refactored to be shared
|
|
129
|
+
MULTIPART_BOUNDARY_DATA_PROJECT = "data_project://"
|
|
130
|
+
DATAFILE_TYPE_TO_CONTENT_TYPE = {
|
|
131
|
+
".datasource": "text/plain",
|
|
132
|
+
".pipe": "text/plain",
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
config = CLIConfig.get_project_config(str(project_path))
|
|
136
|
+
TINYBIRD_API_URL = (config.get_host() or "") + "/v1/deploy"
|
|
137
|
+
TINYBIRD_API_KEY = config.get_token()
|
|
138
|
+
|
|
139
|
+
files = [
|
|
140
|
+
("context://", ("cli-version", "1.0.0", "text/plain")),
|
|
141
|
+
]
|
|
142
|
+
fds = []
|
|
143
|
+
for file_path in project_files(project_path):
|
|
144
|
+
relative_path = str(Path(file_path).relative_to(project_path))
|
|
145
|
+
fd = open(file_path, "rb")
|
|
146
|
+
fds.append(fd)
|
|
147
|
+
content_type = DATAFILE_TYPE_TO_CONTENT_TYPE.get(Path(file_path).suffix, "application/unknown")
|
|
148
|
+
files.append((MULTIPART_BOUNDARY_DATA_PROJECT, (relative_path, fd.read().decode("utf-8"), content_type)))
|
|
149
|
+
|
|
150
|
+
deployment = None
|
|
151
|
+
try:
|
|
152
|
+
HEADERS = {"Authorization": f"Bearer {TINYBIRD_API_KEY}"}
|
|
153
|
+
|
|
154
|
+
r = requests.post(TINYBIRD_API_URL, files=files, headers=HEADERS)
|
|
155
|
+
result = r.json()
|
|
156
|
+
logging.debug(json.dumps(result, indent=2))
|
|
157
|
+
|
|
158
|
+
deploy_result = result.get("result")
|
|
159
|
+
if deploy_result == "success":
|
|
160
|
+
click.echo(FeedbackManager.success(message="Deploy submitted successfully"))
|
|
161
|
+
deployment = result.get("deployment")
|
|
162
|
+
elif deploy_result == "failed":
|
|
163
|
+
click.echo(FeedbackManager.error(message="Deploy failed"))
|
|
164
|
+
deploy_errors = result.get("errors")
|
|
165
|
+
for deploy_error in deploy_errors:
|
|
166
|
+
if deploy_error.get("filename", None):
|
|
167
|
+
click.echo(
|
|
168
|
+
FeedbackManager.error(message=f"{deploy_error.get('filename')}\n\n{deploy_error.get('error')}")
|
|
169
|
+
)
|
|
170
|
+
else:
|
|
171
|
+
click.echo(FeedbackManager.error(message=f"{deploy_error.get('error')}"))
|
|
172
|
+
else:
|
|
173
|
+
click.echo(FeedbackManager.error(message=f"Unknown build result {deploy_result}"))
|
|
174
|
+
finally:
|
|
175
|
+
for fd in fds:
|
|
176
|
+
fd.close()
|
|
177
|
+
|
|
178
|
+
if deployment and wait:
|
|
179
|
+
while deployment.get("status") != "data_ready":
|
|
180
|
+
time.sleep(5)
|
|
181
|
+
TINYBIRD_API_URL = (config.get_host() or "") + f"/v1/deployments/{deployment.get('id')}"
|
|
182
|
+
r = requests.get(TINYBIRD_API_URL, headers=HEADERS)
|
|
183
|
+
result = r.json()
|
|
184
|
+
deployment = result.get("deployment")
|
|
185
|
+
if deployment.get("status") == "failed":
|
|
186
|
+
click.echo(FeedbackManager.error(message="Deployment failed"))
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
click.echo(FeedbackManager.success(message="Deployment is ready"))
|
|
190
|
+
|
|
191
|
+
if auto:
|
|
192
|
+
promote_deployment((config.get_host() or ""), HEADERS)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@cli.group(name="releases")
|
|
196
|
+
def releases_group() -> None:
|
|
197
|
+
"""
|
|
198
|
+
Release commands.
|
|
199
|
+
"""
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@releases_group.command(name="list")
|
|
204
|
+
@click.argument("project_path", type=click.Path(exists=True), default=Path.cwd())
|
|
205
|
+
def release_list(project_path: Path) -> None:
|
|
206
|
+
"""
|
|
207
|
+
List all the releases you have in the project.
|
|
208
|
+
"""
|
|
209
|
+
config = CLIConfig.get_project_config(str(project_path))
|
|
210
|
+
|
|
211
|
+
TINYBIRD_API_KEY = config.get_token()
|
|
212
|
+
HEADERS = {"Authorization": f"Bearer {TINYBIRD_API_KEY}"}
|
|
213
|
+
TINYBIRD_API_URL = (config.get_host() or "") + "/v1/deployments"
|
|
214
|
+
|
|
215
|
+
r = requests.get(TINYBIRD_API_URL, headers=HEADERS)
|
|
216
|
+
result = r.json()
|
|
217
|
+
logging.debug(json.dumps(result, indent=2))
|
|
218
|
+
|
|
219
|
+
columns = ["id", "status", "created_at", "live"]
|
|
220
|
+
table = []
|
|
221
|
+
for deployment in result.get("deployments"):
|
|
222
|
+
table.append(
|
|
223
|
+
[deployment.get("id"), deployment.get("status"), deployment.get("created_at"), deployment.get("live")]
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
echo_safe_humanfriendly_tables_format_smart_table(table, column_names=columns)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@releases_group.command(name="promote")
|
|
230
|
+
@click.argument("project_path", type=click.Path(exists=True), default=Path.cwd())
|
|
231
|
+
def release_promote(project_path: Path) -> None:
|
|
232
|
+
"""
|
|
233
|
+
Promote last deploy to ready and remove old one.
|
|
234
|
+
"""
|
|
235
|
+
config = CLIConfig.get_project_config(str(project_path))
|
|
236
|
+
|
|
237
|
+
TINYBIRD_API_KEY = config.get_token()
|
|
238
|
+
HEADERS = {"Authorization": f"Bearer {TINYBIRD_API_KEY}"}
|
|
239
|
+
|
|
240
|
+
promote_deployment((config.get_host() or ""), HEADERS)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@releases_group.command(name="rollback")
|
|
244
|
+
@click.argument("project_path", type=click.Path(exists=True), default=Path.cwd())
|
|
245
|
+
def release_rollback(project_path: Path) -> None:
|
|
246
|
+
"""
|
|
247
|
+
Rollback to the previous release.
|
|
248
|
+
"""
|
|
249
|
+
config = CLIConfig.get_project_config(str(project_path))
|
|
250
|
+
|
|
251
|
+
TINYBIRD_API_KEY = config.get_token()
|
|
252
|
+
HEADERS = {"Authorization": f"Bearer {TINYBIRD_API_KEY}"}
|
|
253
|
+
|
|
254
|
+
rollback_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:
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def extract_xml(text: str, tag: str) -> str:
|
|
6
|
+
"""
|
|
7
|
+
Extracts the content of the specified XML tag from the given text. Used for parsing structured responses
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
text (str): The text containing the XML.
|
|
11
|
+
tag (str): The XML tag to extract content from.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
str: The content of the specified XML tag, or an empty string if the tag is not found.
|
|
15
|
+
"""
|
|
16
|
+
match = re.search(f"<{tag}>(.*?)</{tag}>", text, re.DOTALL)
|
|
17
|
+
return match.group(1) if match else ""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def parse_xml(text: str, tag: str) -> List[str]:
|
|
21
|
+
"""
|
|
22
|
+
Parses the text for the specified XML tag and returns a list of the contents of each tag.
|
|
23
|
+
"""
|
|
24
|
+
return re.findall(f"<{tag}.*?>(.*?)</{tag}>", text, re.DOTALL)
|
tinybird/tb/modules/local.py
CHANGED
|
@@ -65,7 +65,7 @@ def start_tinybird_local(
|
|
|
65
65
|
if health == "healthy":
|
|
66
66
|
break
|
|
67
67
|
if health == "unhealthy":
|
|
68
|
-
raise CLIException("Tinybird Local is unhealthy.
|
|
68
|
+
raise CLIException("Tinybird Local is unhealthy. Try running `tb local restart` in a few seconds.")
|
|
69
69
|
|
|
70
70
|
time.sleep(5)
|
|
71
71
|
|
|
@@ -82,7 +82,7 @@ def get_docker_client():
|
|
|
82
82
|
client.ping()
|
|
83
83
|
return client
|
|
84
84
|
except Exception:
|
|
85
|
-
raise CLIException("Docker is not running or installed.
|
|
85
|
+
raise CLIException("Docker is not running or installed. Make sure Docker is installed and running.")
|
|
86
86
|
|
|
87
87
|
|
|
88
88
|
def stop_tinybird_local(docker_client):
|
tinybird/tb/modules/login.py
CHANGED
|
@@ -110,15 +110,15 @@ def start_server(auth_callback):
|
|
|
110
110
|
@cli.command()
|
|
111
111
|
@click.option(
|
|
112
112
|
"--host",
|
|
113
|
-
help="Set custom host if it's different than https://api.tinybird.co.
|
|
113
|
+
help="Set custom host if it's different than https://api.tinybird.co. See https://www.tinybird.co/docs/api-reference/overview#regions-and-endpoints for the available list of regions.",
|
|
114
114
|
)
|
|
115
115
|
@click.option(
|
|
116
116
|
"--workspace",
|
|
117
|
-
help="Set the workspace to authenticate to. If
|
|
117
|
+
help="Set the workspace to authenticate to. If unset, the default workspace will be used.",
|
|
118
118
|
)
|
|
119
119
|
@coro
|
|
120
120
|
async def login(host: str, workspace: str):
|
|
121
|
-
"""Authenticate
|
|
121
|
+
"""Authenticate using the browser."""
|
|
122
122
|
auth_event = threading.Event()
|
|
123
123
|
auth_code = [None] # Using a list to store the code, as it's mutable
|
|
124
124
|
host = host or "https://api.tinybird.co"
|
|
@@ -127,7 +127,7 @@ async def login(host: str, workspace: str):
|
|
|
127
127
|
auth_code[0] = code
|
|
128
128
|
auth_event.set()
|
|
129
129
|
|
|
130
|
-
click.echo("Opening browser for authentication...")
|
|
130
|
+
click.echo(FeedbackManager.highlight(message="» Opening browser for authentication..."))
|
|
131
131
|
|
|
132
132
|
# Start the local server in a separate thread
|
|
133
133
|
server_thread = threading.Thread(target=start_server, args=(auth_callback,))
|
|
@@ -169,7 +169,9 @@ async def login(host: str, workspace: str):
|
|
|
169
169
|
cli_config[k] = ws[k]
|
|
170
170
|
|
|
171
171
|
cli_config.persist_to_file()
|
|
172
|
-
|
|
173
|
-
click.echo(FeedbackManager.
|
|
172
|
+
click.echo(FeedbackManager.info(message="\nWorkspace: %s" % ws["name"]))
|
|
173
|
+
click.echo(FeedbackManager.info(message="User: %s" % ws["user_email"]))
|
|
174
|
+
click.echo(FeedbackManager.info(message="Host: %s" % host))
|
|
175
|
+
click.echo(FeedbackManager.success(message="\n✓ Authentication successful!"))
|
|
174
176
|
else:
|
|
175
177
|
click.echo(FeedbackManager.error(message="Authentication failed or timed out."))
|
tinybird/tb/modules/mock.py
CHANGED
|
@@ -4,12 +4,14 @@ from pathlib import Path
|
|
|
4
4
|
|
|
5
5
|
import click
|
|
6
6
|
|
|
7
|
+
from tinybird.prompts import mock_prompt
|
|
7
8
|
from tinybird.tb.modules.cli import cli
|
|
8
9
|
from tinybird.tb.modules.common import CLIException, check_user_token_with_client, coro
|
|
9
10
|
from tinybird.tb.modules.config import CLIConfig
|
|
10
11
|
from tinybird.tb.modules.datafile.fixture import build_fixture_name, persist_fixture
|
|
11
12
|
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
12
13
|
from tinybird.tb.modules.llm import LLM
|
|
14
|
+
from tinybird.tb.modules.llm_utils import extract_xml
|
|
13
15
|
from tinybird.tb.modules.local_common import get_tinybird_local_client
|
|
14
16
|
|
|
15
17
|
|
|
@@ -18,14 +20,15 @@ from tinybird.tb.modules.local_common import get_tinybird_local_client
|
|
|
18
20
|
@click.option("--rows", type=int, default=10, help="Number of events to send")
|
|
19
21
|
@click.option("--prompt", type=str, default="", help="Extra context to use for data generation")
|
|
20
22
|
@click.option("--folder", type=str, default=".", help="Folder where datafiles will be placed")
|
|
21
|
-
@click.pass_context
|
|
22
23
|
@coro
|
|
23
|
-
async def mock(
|
|
24
|
+
async def mock(datasource: str, rows: int, prompt: str, folder: str) -> None:
|
|
24
25
|
"""Load sample data into a Data Source.
|
|
25
26
|
|
|
26
27
|
Args:
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
datasource: Path to the datasource file to load sample data into
|
|
29
|
+
rows: Number of events to send
|
|
30
|
+
prompt: Extra context to use for data generation
|
|
31
|
+
folder: Folder where datafiles will be placed
|
|
29
32
|
"""
|
|
30
33
|
|
|
31
34
|
try:
|
|
@@ -62,13 +65,15 @@ async def mock(ctx: click.Context, datasource: str, rows: int, prompt: str, fold
|
|
|
62
65
|
user_client.token = user_token
|
|
63
66
|
llm = LLM(user_token=user_token, client=user_client)
|
|
64
67
|
tb_client = await get_tinybird_local_client(os.path.abspath(folder))
|
|
65
|
-
|
|
68
|
+
prompt = f"<datasource_schema>{datasource_content}</datasource_schema>\n<user_input>{prompt}</user_input>"
|
|
69
|
+
response = await llm.ask(prompt, system_prompt=mock_prompt(rows))
|
|
70
|
+
sql = extract_xml(response, "sql")
|
|
66
71
|
if os.environ.get("TB_DEBUG", "") != "":
|
|
67
72
|
logging.debug(sql)
|
|
68
73
|
result = await tb_client.query(f"{sql} FORMAT JSON")
|
|
69
74
|
data = result.get("data", [])[:rows]
|
|
70
75
|
fixture_name = build_fixture_name(datasource_path.absolute().as_posix(), datasource_name, datasource_content)
|
|
71
|
-
persist_fixture(fixture_name, data)
|
|
76
|
+
persist_fixture(fixture_name, data, folder)
|
|
72
77
|
click.echo(FeedbackManager.success(message=f"✓ /fixtures/{fixture_name}.ndjson created with {rows} rows"))
|
|
73
78
|
|
|
74
79
|
except Exception as e:
|
tinybird/tb/modules/test.py
CHANGED
|
@@ -12,12 +12,14 @@ from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
|
12
12
|
import click
|
|
13
13
|
import yaml
|
|
14
14
|
|
|
15
|
+
from tinybird.prompts import test_create_prompt
|
|
15
16
|
from tinybird.tb.modules.cli import cli
|
|
16
17
|
from tinybird.tb.modules.common import coro
|
|
17
18
|
from tinybird.tb.modules.config import CLIConfig
|
|
18
19
|
from tinybird.tb.modules.exceptions import CLIException
|
|
19
20
|
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
20
21
|
from tinybird.tb.modules.llm import LLM
|
|
22
|
+
from tinybird.tb.modules.llm_utils import extract_xml, parse_xml
|
|
21
23
|
from tinybird.tb.modules.local_common import get_tinybird_local_client
|
|
22
24
|
|
|
23
25
|
yaml.SafeDumper.org_represent_str = yaml.SafeDumper.represent_str # type: ignore[attr-defined]
|
|
@@ -55,9 +57,9 @@ def test(ctx: click.Context) -> None:
|
|
|
55
57
|
|
|
56
58
|
@test.command(
|
|
57
59
|
name="create",
|
|
58
|
-
help="Create a test for an existing
|
|
60
|
+
help="Create a test for an existing pipe",
|
|
59
61
|
)
|
|
60
|
-
@click.argument("
|
|
62
|
+
@click.argument("name_or_filename", type=str)
|
|
61
63
|
@click.option(
|
|
62
64
|
"--folder",
|
|
63
65
|
default=".",
|
|
@@ -66,64 +68,84 @@ def test(ctx: click.Context) -> None:
|
|
|
66
68
|
)
|
|
67
69
|
@click.option("--prompt", type=str, default=None, help="Prompt to be used to create the test")
|
|
68
70
|
@coro
|
|
69
|
-
async def test_create(
|
|
71
|
+
async def test_create(name_or_filename: str, prompt: Optional[str], folder: str) -> None:
|
|
70
72
|
"""
|
|
71
|
-
Create a test for an existing
|
|
73
|
+
Create a test for an existing pipe
|
|
72
74
|
"""
|
|
73
|
-
|
|
75
|
+
root_path = Path(folder)
|
|
74
76
|
try:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if pipe_path.suffix == ".pipe":
|
|
78
|
-
pipe_name = pipe_path.stem
|
|
79
|
-
else:
|
|
80
|
-
pipe_path = Path("endpoints", f"{pipe}.pipe")
|
|
77
|
+
if ".pipe" in name_or_filename:
|
|
78
|
+
pipe_path = Path(name_or_filename)
|
|
81
79
|
if not pipe_path.exists():
|
|
82
|
-
|
|
80
|
+
raise CLIException(FeedbackManager.error(message=f"Pipe {name_or_filename} not found"))
|
|
81
|
+
else:
|
|
82
|
+
pipe_folders = ("endpoints", "copies", "materializations", "sinks", "pipes")
|
|
83
|
+
pipe_path = next(
|
|
84
|
+
(
|
|
85
|
+
root_path / folder / f"{name_or_filename}.pipe"
|
|
86
|
+
for folder in pipe_folders
|
|
87
|
+
if (root_path / folder / f"{name_or_filename}.pipe").exists()
|
|
88
|
+
),
|
|
89
|
+
None,
|
|
90
|
+
)
|
|
91
|
+
if not pipe_path:
|
|
92
|
+
raise CLIException(FeedbackManager.error(message=f"Pipe {name_or_filename} not found"))
|
|
83
93
|
|
|
94
|
+
pipe_name = pipe_path.stem
|
|
84
95
|
click.echo(FeedbackManager.highlight(message=f"\n» Creating tests for {pipe_name} endpoint..."))
|
|
85
|
-
pipe_path =
|
|
96
|
+
pipe_path = root_path / pipe_path
|
|
86
97
|
pipe_content = pipe_path.read_text()
|
|
87
98
|
|
|
88
99
|
client = await get_tinybird_local_client(os.path.abspath(folder))
|
|
89
100
|
pipe_nodes = await client._req(f"/v0/pipes/{pipe_name}")
|
|
90
|
-
|
|
101
|
+
parameters = set([param["name"] for node in pipe_nodes["nodes"] for param in node["params"]])
|
|
91
102
|
|
|
103
|
+
system_prompt = test_create_prompt.format(
|
|
104
|
+
name=pipe_name,
|
|
105
|
+
content=pipe_content,
|
|
106
|
+
parameters=parameters or "No parameters",
|
|
107
|
+
)
|
|
92
108
|
config = CLIConfig.get_project_config(folder)
|
|
93
109
|
user_token = config.get_user_token()
|
|
94
110
|
llm = LLM(user_token=user_token, client=config.get_client())
|
|
95
111
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
)
|
|
112
|
+
response = await llm.ask(prompt, system_prompt=system_prompt)
|
|
113
|
+
response = extract_xml(response, "response")
|
|
114
|
+
tests_content = parse_xml(response, "test")
|
|
115
|
+
|
|
116
|
+
tests: List[Dict[str, Any]] = []
|
|
117
|
+
for test_content in tests_content:
|
|
118
|
+
test = {}
|
|
119
|
+
test["name"] = extract_xml(test_content, "name")
|
|
120
|
+
test["description"] = extract_xml(test_content, "description")
|
|
121
|
+
parameters = extract_xml(test_content, "parameters")
|
|
122
|
+
test["parameters"] = parameters.split("?")[1] if "?" in parameters else parameters
|
|
123
|
+
test["expected_result"] = ""
|
|
105
124
|
|
|
106
125
|
response = None
|
|
107
126
|
try:
|
|
108
|
-
response = await client._req_raw(f"/v0/pipes/{pipe_name}.ndjson{
|
|
127
|
+
response = await client._req_raw(f"/v0/pipes/{pipe_name}.ndjson?{parameters}")
|
|
109
128
|
except Exception:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if response.status_code >= 400:
|
|
113
|
-
valid_test["expected_http_status"] = response.status_code
|
|
114
|
-
valid_test["expected_result"] = response.json()["error"]
|
|
115
|
-
else:
|
|
116
|
-
if "expected_http_status" in valid_test:
|
|
117
|
-
del valid_test["expected_http_status"]
|
|
118
|
-
valid_test["expected_result"] = response.text or ""
|
|
119
|
-
|
|
120
|
-
valid_test_expectations.append(valid_test)
|
|
129
|
+
pass
|
|
121
130
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
131
|
+
if response:
|
|
132
|
+
if response.status_code >= 400:
|
|
133
|
+
test["expected_http_status"] = response.status_code
|
|
134
|
+
test["expected_result"] = response.json()["error"]
|
|
135
|
+
else:
|
|
136
|
+
if "expected_http_status" in test:
|
|
137
|
+
del test["expected_http_status"]
|
|
138
|
+
test["expected_result"] = response.text or ""
|
|
139
|
+
|
|
140
|
+
tests.append(test)
|
|
141
|
+
|
|
142
|
+
if len(tests) > 0:
|
|
143
|
+
generate_test_file(pipe_name, tests, folder, mode="a")
|
|
144
|
+
for test in tests:
|
|
125
145
|
test_name = test["name"]
|
|
126
146
|
click.echo(FeedbackManager.info(message=f"✓ {test_name} created"))
|
|
147
|
+
else:
|
|
148
|
+
click.echo(FeedbackManager.info(message="* No tests created"))
|
|
127
149
|
|
|
128
150
|
click.echo(FeedbackManager.success(message="✓ Done!\n"))
|
|
129
151
|
except Exception as e:
|
|
@@ -156,10 +178,10 @@ async def test_update(pipe: str, folder: str) -> None:
|
|
|
156
178
|
pipe_tests_path = Path(folder) / pipe_tests_path
|
|
157
179
|
pipe_tests_content = yaml.safe_load(pipe_tests_path.read_text())
|
|
158
180
|
for test in pipe_tests_content:
|
|
159
|
-
test_params = test["parameters"] if test["parameters"]
|
|
181
|
+
test_params = test["parameters"].split("?")[1] if "?" in test["parameters"] else test["parameters"]
|
|
160
182
|
response = None
|
|
161
183
|
try:
|
|
162
|
-
response = await client._req_raw(f"/v0/pipes/{pipe_name}.ndjson{test_params}")
|
|
184
|
+
response = await client._req_raw(f"/v0/pipes/{pipe_name}.ndjson?{test_params}")
|
|
163
185
|
except Exception:
|
|
164
186
|
continue
|
|
165
187
|
|
|
@@ -194,12 +216,12 @@ async def test_update(pipe: str, folder: str) -> None:
|
|
|
194
216
|
help="Folder where tests will be placed",
|
|
195
217
|
)
|
|
196
218
|
@coro
|
|
197
|
-
async def
|
|
219
|
+
async def run_tests(name: Tuple[str, ...], folder: str) -> None:
|
|
198
220
|
click.echo(FeedbackManager.highlight(message="\n» Running tests"))
|
|
199
221
|
client = await get_tinybird_local_client(os.path.abspath(folder))
|
|
200
222
|
paths = [Path(n) for n in name]
|
|
201
223
|
endpoints = [f"./tests/{p.stem}.yaml" for p in paths]
|
|
202
|
-
|
|
224
|
+
test_files: Iterable[str] = endpoints if len(endpoints) > 0 else glob.glob("./tests/**/*.y*ml", recursive=True)
|
|
203
225
|
|
|
204
226
|
async def run_test(test_file):
|
|
205
227
|
test_file_path = Path(test_file)
|
|
@@ -207,11 +229,10 @@ async def test_run(name: Tuple[str, ...], folder: str) -> None:
|
|
|
207
229
|
test_file_content = yaml.safe_load(test_file_path.read_text())
|
|
208
230
|
for test in test_file_content:
|
|
209
231
|
try:
|
|
210
|
-
test_params = test["parameters"] if test["parameters"]
|
|
211
|
-
|
|
232
|
+
test_params = test["parameters"].split("?")[1] if "?" in test["parameters"] else test["parameters"]
|
|
212
233
|
response = None
|
|
213
234
|
try:
|
|
214
|
-
response = await client._req_raw(f"/v0/pipes/{test_file_path.stem}.ndjson{test_params}")
|
|
235
|
+
response = await client._req_raw(f"/v0/pipes/{test_file_path.stem}.ndjson?{test_params}")
|
|
215
236
|
except Exception:
|
|
216
237
|
raise Exception("Expected to not fail but got an error")
|
|
217
238
|
|
|
@@ -239,8 +260,9 @@ async def test_run(name: Tuple[str, ...], folder: str) -> None:
|
|
|
239
260
|
return True
|
|
240
261
|
|
|
241
262
|
failed_tests_count = 0
|
|
242
|
-
test_count = len(
|
|
243
|
-
|
|
263
|
+
test_count = len(test_files)
|
|
264
|
+
|
|
265
|
+
for test_file in test_files:
|
|
244
266
|
if not await run_test(test_file):
|
|
245
267
|
failed_tests_count += 1
|
|
246
268
|
|
tinybird/tb/modules/watch.py
CHANGED