tinybird 0.0.1.dev133__py3-none-any.whl → 0.0.1.dev134__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/tb/__cli__.py +2 -2
- tinybird/tb/config.py +3 -3
- tinybird/tb/modules/build.py +10 -14
- tinybird/tb/modules/common.py +27 -4
- tinybird/tb/modules/create.py +6 -14
- tinybird/tb/modules/deployment.py +44 -31
- tinybird/tb/modules/exceptions.py +51 -1
- tinybird/tb/modules/local.py +10 -4
- tinybird/tb/modules/local_common.py +5 -2
- tinybird/tb/modules/login.py +72 -67
- tinybird/tb/modules/mock.py +7 -10
- tinybird/tb/modules/telemetry.py +1 -2
- tinybird/tb/modules/test.py +98 -93
- {tinybird-0.0.1.dev133.dist-info → tinybird-0.0.1.dev134.dist-info}/METADATA +1 -1
- {tinybird-0.0.1.dev133.dist-info → tinybird-0.0.1.dev134.dist-info}/RECORD +18 -18
- {tinybird-0.0.1.dev133.dist-info → tinybird-0.0.1.dev134.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev133.dist-info → tinybird-0.0.1.dev134.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev133.dist-info → tinybird-0.0.1.dev134.dist-info}/top_level.txt +0 -0
tinybird/tb/__cli__.py
CHANGED
|
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
|
|
|
4
4
|
__url__ = 'https://www.tinybird.co/docs/cli/introduction.html'
|
|
5
5
|
__author__ = 'Tinybird'
|
|
6
6
|
__author_email__ = 'support@tinybird.co'
|
|
7
|
-
__version__ = '0.0.1.
|
|
8
|
-
__revision__ = '
|
|
7
|
+
__version__ = '0.0.1.dev134'
|
|
8
|
+
__revision__ = '921269a'
|
tinybird/tb/config.py
CHANGED
|
@@ -42,7 +42,7 @@ CLOUD_HOSTS = {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
def get_config(
|
|
46
46
|
host: str, token: Optional[str], semver: Optional[str] = None, config_file: Optional[str] = None
|
|
47
47
|
) -> Dict[str, Any]:
|
|
48
48
|
if host:
|
|
@@ -50,8 +50,8 @@ async def get_config(
|
|
|
50
50
|
|
|
51
51
|
config = {}
|
|
52
52
|
try:
|
|
53
|
-
|
|
54
|
-
res =
|
|
53
|
+
with open(config_file or Path(getcwd()) / ".tinyb") as file:
|
|
54
|
+
res = file.read()
|
|
55
55
|
config = json.loads(res)
|
|
56
56
|
except OSError:
|
|
57
57
|
pass
|
tinybird/tb/modules/build.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
3
|
import logging
|
|
4
|
-
import sys
|
|
5
4
|
import threading
|
|
6
5
|
import time
|
|
7
6
|
from copy import deepcopy
|
|
@@ -16,7 +15,7 @@ import requests
|
|
|
16
15
|
import tinybird.context as context
|
|
17
16
|
from tinybird.tb.client import TinyB
|
|
18
17
|
from tinybird.tb.modules.cli import cli
|
|
19
|
-
from tinybird.tb.modules.common import push_data
|
|
18
|
+
from tinybird.tb.modules.common import push_data, sys_exit
|
|
20
19
|
from tinybird.tb.modules.config import CLIConfig
|
|
21
20
|
from tinybird.tb.modules.datafile.build import folder_build
|
|
22
21
|
from tinybird.tb.modules.datafile.exceptions import ParseException
|
|
@@ -99,12 +98,10 @@ def build_project(project: Project, tb_client: TinyB, file_changed: Optional[str
|
|
|
99
98
|
except Exception as e:
|
|
100
99
|
logging.debug(e, exc_info=True)
|
|
101
100
|
click.echo(FeedbackManager.error(message="Couldn't parse response from server"))
|
|
102
|
-
|
|
101
|
+
sys_exit("build_error", str(e))
|
|
103
102
|
|
|
104
103
|
logging.debug(json.dumps(result, indent=2))
|
|
105
104
|
|
|
106
|
-
output_pre_formatted = False
|
|
107
|
-
|
|
108
105
|
build_result = result.get("result")
|
|
109
106
|
if build_result == "success":
|
|
110
107
|
build = result.get("build")
|
|
@@ -174,8 +171,7 @@ def build_project(project: Project, tb_client: TinyB, file_changed: Optional[str
|
|
|
174
171
|
build_errors = result.get("errors")
|
|
175
172
|
full_error_msg = ""
|
|
176
173
|
for build_error in build_errors:
|
|
177
|
-
|
|
178
|
-
filename_bit = FeedbackManager.build_result_error_title(message=f"{build_error.get('filename', '')}")
|
|
174
|
+
filename_bit = build_error.get("filename", "")
|
|
179
175
|
error_bit = build_error.get("error") or build_error.get("message") or ""
|
|
180
176
|
error_msg = ((filename_bit + "\n") if filename_bit else "") + error_bit
|
|
181
177
|
full_error_msg += error_msg + "\n\n"
|
|
@@ -187,11 +183,9 @@ def build_project(project: Project, tb_client: TinyB, file_changed: Optional[str
|
|
|
187
183
|
finally:
|
|
188
184
|
for fd in fds:
|
|
189
185
|
fd.close()
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
click.echo(error)
|
|
194
|
-
raise click.ClickException("") # Needed so the caller shows the right message
|
|
186
|
+
|
|
187
|
+
if error:
|
|
188
|
+
raise click.ClickException(error)
|
|
195
189
|
|
|
196
190
|
|
|
197
191
|
def append_fixture(
|
|
@@ -270,6 +264,7 @@ def process(
|
|
|
270
264
|
) -> None:
|
|
271
265
|
time_start = time.time()
|
|
272
266
|
build_failed = False
|
|
267
|
+
build_error: Optional[str] = None
|
|
273
268
|
if file_changed and (file_changed.endswith(FixtureExtension.NDJSON) or file_changed.endswith(FixtureExtension.CSV)):
|
|
274
269
|
rebuild_fixture(project, tb_client, file_changed)
|
|
275
270
|
elif file_changed and file_changed.endswith(".sql"):
|
|
@@ -278,7 +273,8 @@ def process(
|
|
|
278
273
|
try:
|
|
279
274
|
build_project(project, tb_client, file_changed, silent)
|
|
280
275
|
except click.ClickException as e:
|
|
281
|
-
click.echo(FeedbackManager.
|
|
276
|
+
click.echo(FeedbackManager.info(message=str(e)))
|
|
277
|
+
build_error = str(e)
|
|
282
278
|
build_failed = True
|
|
283
279
|
try:
|
|
284
280
|
if file_changed and not build_failed:
|
|
@@ -294,7 +290,7 @@ def process(
|
|
|
294
290
|
if build_failed:
|
|
295
291
|
click.echo(FeedbackManager.error(message=f"✗ {rebuild_str} failed"))
|
|
296
292
|
if not watch:
|
|
297
|
-
|
|
293
|
+
sys_exit("build_error", build_error or "Unknown error")
|
|
298
294
|
else:
|
|
299
295
|
if not silent:
|
|
300
296
|
click.echo(FeedbackManager.success(message=f"\n✓ {rebuild_str} completed in {elapsed_time:.1f}s"))
|
tinybird/tb/modules/common.py
CHANGED
|
@@ -250,6 +250,7 @@ line experience. To opt-out, set TB_CLI_TELEMETRY_OPTOUT to '1' or 'true'."""
|
|
|
250
250
|
|
|
251
251
|
def __call__(self, *args, **kwargs) -> None:
|
|
252
252
|
error_msg: Optional[str] = None
|
|
253
|
+
silent_error_msg: Optional[str] = None
|
|
253
254
|
error_event: str = "error"
|
|
254
255
|
|
|
255
256
|
exit_code: int = 0
|
|
@@ -265,14 +266,19 @@ line experience. To opt-out, set TB_CLI_TELEMETRY_OPTOUT to '1' or 'true'."""
|
|
|
265
266
|
error_event = "auth_error"
|
|
266
267
|
exit_code = 1
|
|
267
268
|
except SystemExit as ex:
|
|
268
|
-
|
|
269
|
+
if isinstance(ex.code, str):
|
|
270
|
+
exit_code = 1
|
|
271
|
+
error_event, silent_error_msg = get_error_event(ex.code)
|
|
272
|
+
else:
|
|
273
|
+
exit_code = ex.code or 0
|
|
269
274
|
except Exception as ex:
|
|
270
275
|
error_msg = str(ex)
|
|
271
276
|
exit_code = 1
|
|
272
277
|
|
|
273
|
-
if error_msg:
|
|
274
|
-
|
|
275
|
-
|
|
278
|
+
if error_msg or silent_error_msg:
|
|
279
|
+
if error_msg:
|
|
280
|
+
click.echo(error_msg)
|
|
281
|
+
add_telemetry_event(error_event, error=error_msg or silent_error_msg)
|
|
276
282
|
flush_telemetry(wait=True)
|
|
277
283
|
|
|
278
284
|
sys.exit(exit_code)
|
|
@@ -2232,3 +2238,20 @@ async def ask_for_organization(
|
|
|
2232
2238
|
else:
|
|
2233
2239
|
return None, None
|
|
2234
2240
|
return organization_id, organization_name
|
|
2241
|
+
|
|
2242
|
+
|
|
2243
|
+
event_error_separator = "__error__"
|
|
2244
|
+
|
|
2245
|
+
|
|
2246
|
+
def sys_exit(event: str, msg: str) -> None:
|
|
2247
|
+
sys.exit(f"{event}{event_error_separator}{msg}")
|
|
2248
|
+
|
|
2249
|
+
|
|
2250
|
+
def get_error_event(error: str) -> Tuple[str, str]:
|
|
2251
|
+
try:
|
|
2252
|
+
error_event = error.split(event_error_separator)[0]
|
|
2253
|
+
silent_error_msg = error.split(event_error_separator)[1]
|
|
2254
|
+
except Exception:
|
|
2255
|
+
error_event = "error"
|
|
2256
|
+
silent_error_msg = "Unknown error"
|
|
2257
|
+
return error_event, silent_error_msg
|
tinybird/tb/modules/create.py
CHANGED
|
@@ -12,7 +12,7 @@ from tinybird.tb.modules.cli import cli
|
|
|
12
12
|
from tinybird.tb.modules.common import _generate_datafile, coro, generate_datafile
|
|
13
13
|
from tinybird.tb.modules.config import CLIConfig
|
|
14
14
|
from tinybird.tb.modules.datafile.fixture import persist_fixture
|
|
15
|
-
from tinybird.tb.modules.exceptions import
|
|
15
|
+
from tinybird.tb.modules.exceptions import CLICreateException
|
|
16
16
|
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
17
17
|
from tinybird.tb.modules.llm import LLM
|
|
18
18
|
from tinybird.tb.modules.llm_utils import extract_xml, parse_xml
|
|
@@ -70,17 +70,9 @@ async def create(
|
|
|
70
70
|
user_token: Optional[str] = None
|
|
71
71
|
created_something = False
|
|
72
72
|
if prompt:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
raise CLIException("No user token found")
|
|
77
|
-
except Exception as e:
|
|
78
|
-
click.echo(
|
|
79
|
-
FeedbackManager.error(
|
|
80
|
-
message=f"This action requires authentication. Run 'tb login' first. Error: {e}"
|
|
81
|
-
)
|
|
82
|
-
)
|
|
83
|
-
return
|
|
73
|
+
user_token = ctx_config.get("user_token")
|
|
74
|
+
if not user_token:
|
|
75
|
+
raise Exception("This action requires authentication. Run 'tb login' first.")
|
|
84
76
|
|
|
85
77
|
if not validate_project_structure(folder):
|
|
86
78
|
click.echo(FeedbackManager.highlight(message="\n» Creating new project structure..."))
|
|
@@ -164,7 +156,7 @@ async def create(
|
|
|
164
156
|
if not created_something:
|
|
165
157
|
click.echo(FeedbackManager.warning(message="△ No resources created\n"))
|
|
166
158
|
except Exception as e:
|
|
167
|
-
|
|
159
|
+
raise CLICreateException(FeedbackManager.error(message=str(e)))
|
|
168
160
|
|
|
169
161
|
|
|
170
162
|
PROJECT_PATHS = ("datasources", "endpoints", "materializations", "copies", "pipes", "fixtures", "tests", "connections")
|
|
@@ -328,7 +320,7 @@ def init_git(folder: str):
|
|
|
328
320
|
|
|
329
321
|
click.echo(FeedbackManager.info_file_created(file=".gitignore"))
|
|
330
322
|
except Exception as e:
|
|
331
|
-
raise
|
|
323
|
+
raise Exception(f"Error initializing Git: {e}")
|
|
332
324
|
|
|
333
325
|
|
|
334
326
|
def generate_pipe_file(name: str, content: str, folder: str) -> Path:
|
|
@@ -10,7 +10,11 @@ import click
|
|
|
10
10
|
import requests
|
|
11
11
|
|
|
12
12
|
from tinybird.tb.modules.cli import cli
|
|
13
|
-
from tinybird.tb.modules.common import
|
|
13
|
+
from tinybird.tb.modules.common import (
|
|
14
|
+
echo_safe_humanfriendly_tables_format_smart_table,
|
|
15
|
+
get_display_cloud_host,
|
|
16
|
+
sys_exit,
|
|
17
|
+
)
|
|
14
18
|
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
15
19
|
from tinybird.tb.modules.project import Project
|
|
16
20
|
|
|
@@ -27,10 +31,12 @@ def api_fetch(url: str, headers: dict) -> dict:
|
|
|
27
31
|
error = result.get("error")
|
|
28
32
|
logging.debug(json.dumps(result, indent=2))
|
|
29
33
|
click.echo(FeedbackManager.error(message=f"Error: {error}"))
|
|
30
|
-
|
|
34
|
+
sys_exit("deployment_error", error)
|
|
31
35
|
except Exception:
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
message = "Error parsing response from API"
|
|
37
|
+
click.echo(FeedbackManager.error(message=message))
|
|
38
|
+
sys_exit("deployment_error", message)
|
|
39
|
+
return {}
|
|
34
40
|
|
|
35
41
|
|
|
36
42
|
def api_post(
|
|
@@ -50,11 +56,13 @@ def api_post(
|
|
|
50
56
|
error = result.get("error")
|
|
51
57
|
if error:
|
|
52
58
|
click.echo(FeedbackManager.error(message=f"Error: {error}"))
|
|
53
|
-
|
|
59
|
+
sys_exit("deployment_error", error)
|
|
54
60
|
return result
|
|
55
61
|
except Exception:
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
message = "Error parsing response from API"
|
|
63
|
+
click.echo(FeedbackManager.error(message=message))
|
|
64
|
+
sys_exit("deployment_error", message)
|
|
65
|
+
return {}
|
|
58
66
|
|
|
59
67
|
|
|
60
68
|
# TODO(eclbg): This logic should be in the server, and there should be a dedicated endpoint for promoting a deployment
|
|
@@ -65,12 +73,16 @@ def promote_deployment(host: Optional[str], headers: dict, wait: bool) -> None:
|
|
|
65
73
|
|
|
66
74
|
deployments = result.get("deployments")
|
|
67
75
|
if not deployments:
|
|
68
|
-
|
|
69
|
-
|
|
76
|
+
message = "No deployments found"
|
|
77
|
+
click.echo(FeedbackManager.error(message=message))
|
|
78
|
+
sys_exit("deployment_error", message)
|
|
79
|
+
return
|
|
70
80
|
|
|
71
81
|
if len(deployments) < 2:
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
message = "Only one deployment found"
|
|
83
|
+
click.echo(FeedbackManager.error(message=message))
|
|
84
|
+
sys_exit("deployment_error", message)
|
|
85
|
+
return
|
|
74
86
|
|
|
75
87
|
last_deployment, candidate_deployment = deployments[0], deployments[1]
|
|
76
88
|
|
|
@@ -79,17 +91,16 @@ def promote_deployment(host: Optional[str], headers: dict, wait: bool) -> None:
|
|
|
79
91
|
deploy_errors = candidate_deployment.get("errors", [])
|
|
80
92
|
for deploy_error in deploy_errors:
|
|
81
93
|
click.echo(FeedbackManager.error(message=f"* {deploy_error}"))
|
|
82
|
-
|
|
94
|
+
sys_exit("deployment_error", "Current deployment is not ready: " + str(deploy_errors))
|
|
95
|
+
return
|
|
83
96
|
|
|
84
97
|
if candidate_deployment.get("live"):
|
|
85
98
|
click.echo(FeedbackManager.error(message="Candidate deployment is already live"))
|
|
86
99
|
else:
|
|
87
|
-
click.echo(FeedbackManager.success(message="Setting candidate deployment as live"))
|
|
88
|
-
|
|
89
100
|
TINYBIRD_API_URL = f"{host}/v1/deployments/{candidate_deployment.get('id')}/set-live"
|
|
90
101
|
result = api_post(TINYBIRD_API_URL, headers=headers)
|
|
91
102
|
|
|
92
|
-
click.echo(FeedbackManager.
|
|
103
|
+
click.echo(FeedbackManager.highlight(message="» Removing old deployment"))
|
|
93
104
|
|
|
94
105
|
TINYBIRD_API_URL = f"{host}/v1/deployments/{last_deployment.get('id')}"
|
|
95
106
|
r = requests.delete(TINYBIRD_API_URL, headers=headers)
|
|
@@ -97,9 +108,10 @@ def promote_deployment(host: Optional[str], headers: dict, wait: bool) -> None:
|
|
|
97
108
|
logging.debug(json.dumps(result, indent=2))
|
|
98
109
|
if result.get("error"):
|
|
99
110
|
click.echo(FeedbackManager.error(message=result.get("error")))
|
|
100
|
-
|
|
111
|
+
sys_exit("deployment_error", result.get("error", "Unknown error"))
|
|
112
|
+
click.echo(FeedbackManager.info(message="✓ Old deployment removed"))
|
|
101
113
|
|
|
102
|
-
click.echo(FeedbackManager.
|
|
114
|
+
click.echo(FeedbackManager.highlight(message="» Waiting for deployment to be promoted..."))
|
|
103
115
|
|
|
104
116
|
if wait:
|
|
105
117
|
while True:
|
|
@@ -108,7 +120,7 @@ def promote_deployment(host: Optional[str], headers: dict, wait: bool) -> None:
|
|
|
108
120
|
|
|
109
121
|
last_deployment = result.get("deployment")
|
|
110
122
|
if last_deployment.get("status") == "deleted":
|
|
111
|
-
click.echo(FeedbackManager.success(message="Deployment
|
|
123
|
+
click.echo(FeedbackManager.success(message="✓ Deployment is live!"))
|
|
112
124
|
break
|
|
113
125
|
|
|
114
126
|
time.sleep(5)
|
|
@@ -154,7 +166,7 @@ def discard_deployment(host: Optional[str], headers: dict, wait: bool) -> None:
|
|
|
154
166
|
logging.debug(json.dumps(result, indent=2))
|
|
155
167
|
if result.get("error"):
|
|
156
168
|
click.echo(FeedbackManager.error(message=result.get("error")))
|
|
157
|
-
|
|
169
|
+
sys_exit("deployment_error", result.get("error", "Unknown error"))
|
|
158
170
|
|
|
159
171
|
click.echo(FeedbackManager.success(message="Discard process successfully started"))
|
|
160
172
|
|
|
@@ -404,7 +416,7 @@ def create_deployment(
|
|
|
404
416
|
sys.exit(0)
|
|
405
417
|
|
|
406
418
|
click.echo(FeedbackManager.error(message="\n✗ Deployment is not valid"))
|
|
407
|
-
|
|
419
|
+
sys_exit("deployment_error", "Deployment is not valid: " + str(deployment.get("errors")))
|
|
408
420
|
|
|
409
421
|
status = result.get("result")
|
|
410
422
|
if status == "success":
|
|
@@ -415,15 +427,15 @@ def create_deployment(
|
|
|
415
427
|
)
|
|
416
428
|
|
|
417
429
|
if wait:
|
|
418
|
-
click.echo(FeedbackManager.info(message="\n
|
|
430
|
+
click.echo(FeedbackManager.info(message="\n* Deployment submitted"))
|
|
419
431
|
else:
|
|
420
432
|
click.echo(FeedbackManager.success(message="\n✓ Deployment submitted successfully"))
|
|
421
433
|
elif status == "no_changes":
|
|
422
|
-
click.echo(FeedbackManager.
|
|
434
|
+
click.echo(FeedbackManager.warning(message="△ Not deploying. No changes."))
|
|
423
435
|
sys.exit(0)
|
|
424
436
|
elif status == "failed":
|
|
425
437
|
click.echo(FeedbackManager.error(message="Deployment failed"))
|
|
426
|
-
|
|
438
|
+
sys_exit("deployment_error", "Deployment failed" + str(deployment.get("errors")))
|
|
427
439
|
else:
|
|
428
440
|
click.echo(FeedbackManager.error(message=f"Unknown deployment result {status}"))
|
|
429
441
|
except Exception as e:
|
|
@@ -433,16 +445,17 @@ def create_deployment(
|
|
|
433
445
|
fd.close()
|
|
434
446
|
|
|
435
447
|
if not deployment and not check:
|
|
436
|
-
|
|
448
|
+
sys_exit("deployment_error", "Deployment failed")
|
|
437
449
|
|
|
438
450
|
if deployment and wait and not check:
|
|
451
|
+
click.echo(FeedbackManager.highlight(message="» Waiting for deployment to be ready..."))
|
|
439
452
|
while True:
|
|
440
453
|
url = f"{client.host}/v1/deployments/{deployment.get('id')}"
|
|
441
454
|
res = api_fetch(url, HEADERS)
|
|
442
455
|
deployment = res.get("deployment")
|
|
443
456
|
if not deployment:
|
|
444
457
|
click.echo(FeedbackManager.error(message="Error parsing deployment from response"))
|
|
445
|
-
|
|
458
|
+
sys_exit("deployment_error", "Error parsing deployment from response")
|
|
446
459
|
if deployment.get("status") == "failed":
|
|
447
460
|
click.echo(FeedbackManager.error(message="Deployment failed"))
|
|
448
461
|
deploy_errors = deployment.get("errors")
|
|
@@ -452,14 +465,14 @@ def create_deployment(
|
|
|
452
465
|
if auto:
|
|
453
466
|
click.echo(FeedbackManager.error(message="Rolling back deployment"))
|
|
454
467
|
discard_deployment(client.host, HEADERS, wait=wait)
|
|
455
|
-
|
|
468
|
+
sys_exit("deployment_error", "Deployment failed: " + str(deployment.get("errors")))
|
|
456
469
|
|
|
457
470
|
if deployment.get("status") == "data_ready":
|
|
458
471
|
break
|
|
459
472
|
|
|
460
473
|
time.sleep(5)
|
|
461
474
|
|
|
462
|
-
click.echo(FeedbackManager.
|
|
475
|
+
click.echo(FeedbackManager.info(message="✓ Deployment is ready"))
|
|
463
476
|
|
|
464
477
|
if auto:
|
|
465
478
|
promote_deployment(client.host, HEADERS, wait=wait)
|
|
@@ -513,12 +526,12 @@ def print_changes(result: dict, project: Project) -> None:
|
|
|
513
526
|
tokens.append((change_type, token_name, "\n".join(added_perms), "\n".join(removed_perms)))
|
|
514
527
|
|
|
515
528
|
if resources:
|
|
516
|
-
click.echo(FeedbackManager.
|
|
529
|
+
click.echo(FeedbackManager.info(message="\n* Changes to be deployed:"))
|
|
517
530
|
echo_safe_humanfriendly_tables_format_smart_table(resources, column_names=resources_columns)
|
|
518
531
|
else:
|
|
519
|
-
click.echo(FeedbackManager.
|
|
532
|
+
click.echo(FeedbackManager.info(message="\n* No changes to be deployed"))
|
|
520
533
|
if tokens:
|
|
521
|
-
click.echo(FeedbackManager.
|
|
534
|
+
click.echo(FeedbackManager.info(message="\n* Changes in tokens to be deployed:"))
|
|
522
535
|
echo_safe_humanfriendly_tables_format_smart_table(tokens, column_names=tokens_columns)
|
|
523
536
|
else:
|
|
524
|
-
click.echo(FeedbackManager.
|
|
537
|
+
click.echo(FeedbackManager.info(message="* No changes in tokens to be deployed"))
|
|
@@ -15,7 +15,8 @@ class CLIException(click.exceptions.ClickException):
|
|
|
15
15
|
|
|
16
16
|
def __init__(self, message: str, telemetry_event: Optional[str] = None, **kw_telemetry_event_data: Any) -> None:
|
|
17
17
|
telemetry_event = telemetry_event or "error"
|
|
18
|
-
|
|
18
|
+
message_without_color = message.replace("\033[91m", "").replace("\033[0m", "")
|
|
19
|
+
data: Dict[str, Any] = {"error": message_without_color}
|
|
19
20
|
data.update(kw_telemetry_event_data)
|
|
20
21
|
add_telemetry_event(telemetry_event, **data)
|
|
21
22
|
super().__init__(message)
|
|
@@ -89,3 +90,52 @@ class CLITokenException(CLIException):
|
|
|
89
90
|
|
|
90
91
|
def __init__(self, message: str, **kw_telemetry_event_data: Any) -> None:
|
|
91
92
|
super().__init__(message, "token_error", **kw_telemetry_event_data)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class CLIBuildException(CLIException):
|
|
96
|
+
"""Exceptions generated by the build commands"""
|
|
97
|
+
|
|
98
|
+
def __init__(self, message: str, **kw_telemetry_event_data: Any) -> None:
|
|
99
|
+
super().__init__(message, "build_error", **kw_telemetry_event_data)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class CLIDeploymentException(CLIException):
|
|
103
|
+
"""Exceptions generated by the deployment commands"""
|
|
104
|
+
|
|
105
|
+
def __init__(self, message: str, **kw_telemetry_event_data: Any) -> None:
|
|
106
|
+
super().__init__(message, "deployment_error", **kw_telemetry_event_data)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class CLITestException(CLIException):
|
|
110
|
+
"""Exceptions generated by the test commands"""
|
|
111
|
+
|
|
112
|
+
def __init__(self, message: str, **kw_telemetry_event_data: Any) -> None:
|
|
113
|
+
super().__init__(message, "test_error", **kw_telemetry_event_data)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class CLICreateException(CLIException):
|
|
117
|
+
"""Exceptions generated by the test commands"""
|
|
118
|
+
|
|
119
|
+
def __init__(self, message: str, **kw_telemetry_event_data: Any) -> None:
|
|
120
|
+
super().__init__(message, "create_error", **kw_telemetry_event_data)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class CLIMockException(CLIException):
|
|
124
|
+
"""Exceptions generated by the mock commands"""
|
|
125
|
+
|
|
126
|
+
def __init__(self, message: str, **kw_telemetry_event_data: Any) -> None:
|
|
127
|
+
super().__init__(message, "mock_error", **kw_telemetry_event_data)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class CLILoginException(CLIException):
|
|
131
|
+
"""Exceptions generated by the login commands"""
|
|
132
|
+
|
|
133
|
+
def __init__(self, message: str, **kw_telemetry_event_data: Any) -> None:
|
|
134
|
+
super().__init__(message, "login_error", **kw_telemetry_event_data)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class CLILocalException(CLIException):
|
|
138
|
+
"""Exceptions generated by the local commands"""
|
|
139
|
+
|
|
140
|
+
def __init__(self, message: str, **kw_telemetry_event_data: Any) -> None:
|
|
141
|
+
super().__init__(message, "local_error", **kw_telemetry_event_data)
|
tinybird/tb/modules/local.py
CHANGED
|
@@ -11,7 +11,7 @@ from docker.client import DockerClient
|
|
|
11
11
|
from docker.models.containers import Container
|
|
12
12
|
from tinybird.tb.modules.cli import cli
|
|
13
13
|
from tinybird.tb.modules.common import coro
|
|
14
|
-
from tinybird.tb.modules.exceptions import CLIException
|
|
14
|
+
from tinybird.tb.modules.exceptions import CLIException, CLILocalException
|
|
15
15
|
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
16
16
|
from tinybird.tb.modules.local_common import TB_CONTAINER_NAME, TB_IMAGE_NAME, TB_LOCAL_PORT
|
|
17
17
|
|
|
@@ -71,7 +71,11 @@ def start_tinybird_local(
|
|
|
71
71
|
if health == "healthy":
|
|
72
72
|
break
|
|
73
73
|
if health == "unhealthy":
|
|
74
|
-
raise
|
|
74
|
+
raise CLILocalException(
|
|
75
|
+
FeedbackManager.error(
|
|
76
|
+
message="Tinybird Local is unhealthy. Try running `tb local restart` in a few seconds."
|
|
77
|
+
)
|
|
78
|
+
)
|
|
75
79
|
|
|
76
80
|
time.sleep(5)
|
|
77
81
|
|
|
@@ -125,8 +129,10 @@ def get_docker_client() -> DockerClient:
|
|
|
125
129
|
client.ping()
|
|
126
130
|
return client
|
|
127
131
|
except Exception:
|
|
128
|
-
raise
|
|
129
|
-
|
|
132
|
+
raise CLILocalException(
|
|
133
|
+
FeedbackManager.error(
|
|
134
|
+
message="No container runtime is running. Make sure a Docker-compatible runtime is installed and running."
|
|
135
|
+
)
|
|
130
136
|
)
|
|
131
137
|
|
|
132
138
|
|
|
@@ -7,7 +7,8 @@ import requests
|
|
|
7
7
|
|
|
8
8
|
from tinybird.tb.client import AuthNoTokenException, TinyB
|
|
9
9
|
from tinybird.tb.modules.config import CLIConfig
|
|
10
|
-
from tinybird.tb.modules.exceptions import
|
|
10
|
+
from tinybird.tb.modules.exceptions import CLILocalException
|
|
11
|
+
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
11
12
|
|
|
12
13
|
TB_IMAGE_NAME = "tinybirdco/tinybird-local:latest"
|
|
13
14
|
TB_CONTAINER_NAME = "tinybird-local"
|
|
@@ -34,7 +35,9 @@ async def get_tinybird_local_config(config_obj: Dict[str, Any], build: bool = Fa
|
|
|
34
35
|
# ruff: noqa: ASYNC210
|
|
35
36
|
tokens = requests.get(f"{TB_LOCAL_HOST}/tokens").json()
|
|
36
37
|
except Exception:
|
|
37
|
-
raise
|
|
38
|
+
raise CLILocalException(
|
|
39
|
+
FeedbackManager.error(message="Tinybird local is not running. Please run `tb local start` first.")
|
|
40
|
+
)
|
|
38
41
|
|
|
39
42
|
user_token = tokens["user_token"]
|
|
40
43
|
admin_token = tokens["admin_token"]
|
tinybird/tb/modules/login.py
CHANGED
|
@@ -13,7 +13,7 @@ import requests
|
|
|
13
13
|
|
|
14
14
|
from tinybird.tb.modules.cli import CLIConfig, cli
|
|
15
15
|
from tinybird.tb.modules.common import coro
|
|
16
|
-
from tinybird.tb.modules.exceptions import
|
|
16
|
+
from tinybird.tb.modules.exceptions import CLILoginException
|
|
17
17
|
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
18
18
|
|
|
19
19
|
|
|
@@ -115,69 +115,74 @@ def start_server(auth_callback, auth_host):
|
|
|
115
115
|
@coro
|
|
116
116
|
async def login(host: str, auth_host: str, workspace: str):
|
|
117
117
|
"""Authenticate using the browser."""
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
118
|
+
try:
|
|
119
|
+
host = host.rstrip("/")
|
|
120
|
+
auth_host = auth_host.rstrip("/")
|
|
121
|
+
|
|
122
|
+
auth_event = threading.Event()
|
|
123
|
+
auth_code: list[str] = [] # Using a list to store the code, as it's mutable
|
|
124
|
+
|
|
125
|
+
def auth_callback(code):
|
|
126
|
+
auth_code.append(code)
|
|
127
|
+
auth_event.set()
|
|
128
|
+
|
|
129
|
+
click.echo(FeedbackManager.highlight(message="» Opening browser for authentication..."))
|
|
130
|
+
|
|
131
|
+
# Start the local server in a separate thread
|
|
132
|
+
server_thread = threading.Thread(target=start_server, args=(auth_callback, auth_host))
|
|
133
|
+
server_thread.daemon = True
|
|
134
|
+
server_thread.start()
|
|
135
|
+
|
|
136
|
+
# Open the browser to the auth page
|
|
137
|
+
callback_url = f"http://localhost:{AUTH_SERVER_PORT}"
|
|
138
|
+
params = {
|
|
139
|
+
"redirect_uri": callback_url,
|
|
140
|
+
"apiHost": host,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if workspace:
|
|
144
|
+
params["workspace"] = workspace
|
|
145
|
+
|
|
146
|
+
auth_url = f"{auth_host}/api/cli-login?{urlencode(params)}"
|
|
147
|
+
webbrowser.open(auth_url)
|
|
148
|
+
|
|
149
|
+
# Wait for the authentication to complete or timeout
|
|
150
|
+
if auth_event.wait(timeout=60): # Wait for up to 60 seconds
|
|
151
|
+
params = {}
|
|
152
|
+
params["code"] = auth_code[0]
|
|
153
|
+
response = requests.get( # noqa: ASYNC210
|
|
154
|
+
f"{auth_host}/api/cli-login?{urlencode(params)}",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
data = response.json()
|
|
158
|
+
cli_config = CLIConfig.get_project_config()
|
|
159
|
+
cli_config.set_token(data.get("workspace_token", ""))
|
|
160
|
+
cli_config.set_token_for_host(data.get("workspace_token", ""), host)
|
|
161
|
+
cli_config.set_user_token(data.get("user_token", ""))
|
|
162
|
+
host = data.get("api_host", host)
|
|
163
|
+
cli_config.set_host(host)
|
|
164
|
+
ws = await cli_config.get_client(token=data.get("workspace_token", ""), host=host).workspace_info(
|
|
165
|
+
version="v1"
|
|
166
|
+
)
|
|
167
|
+
for k in ("id", "name", "user_email", "user_id", "scope"):
|
|
168
|
+
if k in ws:
|
|
169
|
+
cli_config[k] = ws[k]
|
|
170
|
+
|
|
171
|
+
path = os.path.join(os.getcwd(), ".tinyb")
|
|
172
|
+
cli_config.persist_to_file(override_with_path=path)
|
|
173
|
+
|
|
174
|
+
auth_info: Dict[str, Any] = await cli_config.get_user_client().check_auth_login()
|
|
175
|
+
if not auth_info.get("is_valid", False):
|
|
176
|
+
raise Exception(FeedbackManager.error_auth_login_not_valid(host=cli_config.get_host()))
|
|
177
|
+
|
|
178
|
+
if not auth_info.get("is_user", False):
|
|
179
|
+
raise Exception(FeedbackManager.error_auth_login_not_user(host=cli_config.get_host()))
|
|
180
|
+
|
|
181
|
+
click.echo(FeedbackManager.gray(message="\nWorkspace: ") + FeedbackManager.info(message=ws["name"]))
|
|
182
|
+
click.echo(FeedbackManager.gray(message="User: ") + FeedbackManager.info(message=ws["user_email"]))
|
|
183
|
+
click.echo(FeedbackManager.gray(message="Host: ") + FeedbackManager.info(message=host))
|
|
184
|
+
click.echo(FeedbackManager.success(message="\n✓ Authentication successful!"))
|
|
185
|
+
else:
|
|
186
|
+
raise Exception("Authentication failed or timed out.")
|
|
187
|
+
except Exception as e:
|
|
188
|
+
raise CLILoginException(FeedbackManager.error(message=str(e)))
|
tinybird/tb/modules/mock.py
CHANGED
|
@@ -6,9 +6,10 @@ import click
|
|
|
6
6
|
from tinybird.prompts import mock_prompt
|
|
7
7
|
from tinybird.tb.client import TinyB
|
|
8
8
|
from tinybird.tb.modules.cli import cli
|
|
9
|
-
from tinybird.tb.modules.common import
|
|
9
|
+
from tinybird.tb.modules.common import coro, push_data
|
|
10
10
|
from tinybird.tb.modules.config import CLIConfig
|
|
11
11
|
from tinybird.tb.modules.datafile.fixture import persist_fixture, persist_fixture_sql
|
|
12
|
+
from tinybird.tb.modules.exceptions import CLIMockException
|
|
12
13
|
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
13
14
|
from tinybird.tb.modules.llm import LLM
|
|
14
15
|
from tinybird.tb.modules.llm_utils import extract_xml
|
|
@@ -60,19 +61,15 @@ async def mock(ctx: click.Context, datasource: str, rows: int, prompt: str, form
|
|
|
60
61
|
datasource_path = Path(datasource_from_glob[0])
|
|
61
62
|
|
|
62
63
|
if not datasource_path.exists():
|
|
63
|
-
raise
|
|
64
|
+
raise Exception(f"Datasource '{datasource_path.stem}' not fosund")
|
|
64
65
|
|
|
65
66
|
datasource_content = datasource_path.read_text()
|
|
66
67
|
config = CLIConfig.get_project_config()
|
|
67
68
|
user_client = config.get_client(token=ctx_config.get("token"), host=ctx_config.get("host"))
|
|
68
69
|
user_token = ctx_config.get("user_token")
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
raise CLIException("No user token found")
|
|
73
|
-
except Exception:
|
|
74
|
-
click.echo(FeedbackManager.error(message="This action requires authentication. Run 'tb login' first."))
|
|
75
|
-
return
|
|
71
|
+
if not user_token:
|
|
72
|
+
raise Exception("This action requires authentication. Run 'tb login' first.")
|
|
76
73
|
|
|
77
74
|
llm = LLM(user_token=user_token, host=user_client.host)
|
|
78
75
|
prompt = f"<datasource_schema>{datasource_content}</datasource_schema>\n<user_input>{prompt}</user_input>"
|
|
@@ -92,7 +89,7 @@ async def mock(ctx: click.Context, datasource: str, rows: int, prompt: str, form
|
|
|
92
89
|
data = result.get("data", [])[:rows]
|
|
93
90
|
error_response = result.get("error", None)
|
|
94
91
|
if error_response:
|
|
95
|
-
raise
|
|
92
|
+
raise Exception(error_response)
|
|
96
93
|
else:
|
|
97
94
|
data = result
|
|
98
95
|
break
|
|
@@ -112,7 +109,7 @@ async def mock(ctx: click.Context, datasource: str, rows: int, prompt: str, form
|
|
|
112
109
|
click.echo(FeedbackManager.success(message=f"✓ Sample data for {datasource_name} created with {rows} rows"))
|
|
113
110
|
|
|
114
111
|
except Exception as e:
|
|
115
|
-
|
|
112
|
+
raise CLIMockException(FeedbackManager.error(message=str(e)))
|
|
116
113
|
|
|
117
114
|
|
|
118
115
|
async def append_fixture(
|
tinybird/tb/modules/telemetry.py
CHANGED
|
@@ -14,7 +14,6 @@ from urllib.parse import urlencode
|
|
|
14
14
|
|
|
15
15
|
import requests
|
|
16
16
|
|
|
17
|
-
from tinybird.syncasync import async_to_sync
|
|
18
17
|
from tinybird.tb.config import CURRENT_VERSION, get_config
|
|
19
18
|
|
|
20
19
|
TELEMETRY_TIMEOUT: int = 1
|
|
@@ -120,7 +119,7 @@ class TelemetryHelper:
|
|
|
120
119
|
|
|
121
120
|
event_data["fingerprint_id"] = hashlib.sha256(platform.node().encode()).hexdigest()
|
|
122
121
|
|
|
123
|
-
config =
|
|
122
|
+
config = get_config(host="", token=None)
|
|
124
123
|
if config:
|
|
125
124
|
event_data["workspace_id"] = config.get("workspace_id", "")
|
|
126
125
|
event_data["workspace_name"] = config.get("workspace_name", "")
|
tinybird/tb/modules/test.py
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import asyncio
|
|
7
7
|
import difflib
|
|
8
8
|
import glob
|
|
9
|
-
import os
|
|
10
9
|
import sys
|
|
11
10
|
import urllib.parse
|
|
12
11
|
from pathlib import Path
|
|
@@ -21,7 +20,7 @@ from tinybird.tb.client import TinyB
|
|
|
21
20
|
from tinybird.tb.modules.build import process
|
|
22
21
|
from tinybird.tb.modules.cli import cli
|
|
23
22
|
from tinybird.tb.modules.config import CLIConfig
|
|
24
|
-
from tinybird.tb.modules.exceptions import
|
|
23
|
+
from tinybird.tb.modules.exceptions import CLITestException
|
|
25
24
|
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
26
25
|
from tinybird.tb.modules.llm import LLM
|
|
27
26
|
from tinybird.tb.modules.llm_utils import extract_xml, parse_xml
|
|
@@ -74,32 +73,17 @@ def test_create(ctx: click.Context, name_or_filename: str, prompt: str) -> None:
|
|
|
74
73
|
"""
|
|
75
74
|
Create a test for an existing pipe
|
|
76
75
|
"""
|
|
77
|
-
project: Project = ctx.ensure_object(dict)["project"]
|
|
78
|
-
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
79
|
-
click.echo(FeedbackManager.highlight(message="\n» Building project"))
|
|
80
|
-
process(project=project, tb_client=client, watch=False, silent=True)
|
|
81
|
-
click.echo(FeedbackManager.info(message="✓ Done!"))
|
|
82
|
-
config = CLIConfig.get_project_config()
|
|
83
|
-
root_path = project.path
|
|
84
|
-
folder = project.folder
|
|
85
|
-
|
|
86
76
|
try:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
for folder in pipe_folders
|
|
96
|
-
if os.path.exists(root_path / folder / f"{name_or_filename}.pipe")
|
|
97
|
-
)
|
|
98
|
-
if not pipe_path:
|
|
99
|
-
raise CLIException(FeedbackManager.error(message=f"Pipe {name_or_filename} not found"))
|
|
100
|
-
|
|
77
|
+
project: Project = ctx.ensure_object(dict)["project"]
|
|
78
|
+
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
79
|
+
click.echo(FeedbackManager.highlight(message="\n» Building project"))
|
|
80
|
+
process(project=project, tb_client=client, watch=False, silent=True)
|
|
81
|
+
click.echo(FeedbackManager.info(message="✓ Done!\n"))
|
|
82
|
+
config = CLIConfig.get_project_config()
|
|
83
|
+
folder = project.folder
|
|
84
|
+
pipe_path = get_pipe_path(name_or_filename, folder)
|
|
101
85
|
pipe_name = pipe_path.stem
|
|
102
|
-
click.echo(FeedbackManager.highlight(message=f"
|
|
86
|
+
click.echo(FeedbackManager.highlight(message=f"» Creating tests for {pipe_name} endpoint..."))
|
|
103
87
|
pipe_content = pipe_path.read_text()
|
|
104
88
|
pipe = asyncio.run(client._req(f"/v0/pipes/{pipe_name}"))
|
|
105
89
|
parameters = set([param["name"] for node in pipe["nodes"] for param in node["params"]])
|
|
@@ -111,7 +95,7 @@ def test_create(ctx: click.Context, name_or_filename: str, prompt: str) -> None:
|
|
|
111
95
|
)
|
|
112
96
|
user_token = config.get_user_token()
|
|
113
97
|
if not user_token:
|
|
114
|
-
raise
|
|
98
|
+
raise Exception("No user token found")
|
|
115
99
|
|
|
116
100
|
llm = LLM(user_token=user_token, host=config.get_client().host)
|
|
117
101
|
response_llm = llm.ask(system_prompt=system_prompt, prompt=prompt)
|
|
@@ -155,7 +139,7 @@ def test_create(ctx: click.Context, name_or_filename: str, prompt: str) -> None:
|
|
|
155
139
|
|
|
156
140
|
click.echo(FeedbackManager.success(message="✓ Done!\n"))
|
|
157
141
|
except Exception as e:
|
|
158
|
-
raise
|
|
142
|
+
raise CLITestException(FeedbackManager.error(message=str(e)))
|
|
159
143
|
|
|
160
144
|
|
|
161
145
|
@test.command(
|
|
@@ -172,12 +156,12 @@ def test_update(ctx: click.Context, pipe: str) -> None:
|
|
|
172
156
|
click.echo(FeedbackManager.highlight(message="\n» Building project"))
|
|
173
157
|
process(project=project, tb_client=client, watch=False, silent=True)
|
|
174
158
|
click.echo(FeedbackManager.info(message="✓ Done!"))
|
|
175
|
-
pipe_tests_path =
|
|
176
|
-
pipe_name =
|
|
159
|
+
pipe_tests_path = get_pipe_path(pipe, folder)
|
|
160
|
+
pipe_name = pipe_tests_path.stem
|
|
177
161
|
if pipe_tests_path.suffix == ".yaml":
|
|
178
162
|
pipe_name = pipe_tests_path.stem
|
|
179
163
|
else:
|
|
180
|
-
pipe_tests_path = Path("tests", f"{
|
|
164
|
+
pipe_tests_path = Path("tests", f"{pipe_name}.yaml")
|
|
181
165
|
|
|
182
166
|
click.echo(FeedbackManager.highlight(message=f"\n» Updating tests expectations for {pipe_name} endpoint..."))
|
|
183
167
|
pipe_tests_path = Path(project.folder) / pipe_tests_path
|
|
@@ -206,7 +190,7 @@ def test_update(ctx: click.Context, pipe: str) -> None:
|
|
|
206
190
|
|
|
207
191
|
click.echo(FeedbackManager.success(message="✓ Done!\n"))
|
|
208
192
|
except Exception as e:
|
|
209
|
-
|
|
193
|
+
raise CLITestException(FeedbackManager.error(message=str(e)))
|
|
210
194
|
|
|
211
195
|
|
|
212
196
|
@test.command(
|
|
@@ -216,70 +200,74 @@ def test_update(ctx: click.Context, pipe: str) -> None:
|
|
|
216
200
|
@click.argument("name", nargs=-1)
|
|
217
201
|
@click.pass_context
|
|
218
202
|
def run_tests(ctx: click.Context, name: Tuple[str, ...]) -> None:
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
click.echo(FeedbackManager.highlight(message="\n» Running tests"))
|
|
226
|
-
paths = [Path(n) for n in name]
|
|
227
|
-
endpoints = [f"{project.path}/tests/{p.stem}.yaml" for p in paths]
|
|
228
|
-
test_files: List[str] = (
|
|
229
|
-
endpoints if len(endpoints) > 0 else glob.glob(f"{project.path}/tests/**/*.y*ml", recursive=True)
|
|
230
|
-
)
|
|
203
|
+
try:
|
|
204
|
+
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
205
|
+
project: Project = ctx.ensure_object(dict)["project"]
|
|
206
|
+
click.echo(FeedbackManager.highlight(message="\n» Building project"))
|
|
207
|
+
process(project=project, tb_client=client, watch=False, silent=True)
|
|
208
|
+
click.echo(FeedbackManager.info(message="✓ Done!"))
|
|
231
209
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
210
|
+
click.echo(FeedbackManager.highlight(message="\n» Running tests"))
|
|
211
|
+
paths = [Path(n) for n in name]
|
|
212
|
+
endpoints = [f"{project.path}/tests/{p.stem}.yaml" for p in paths]
|
|
213
|
+
test_files: List[str] = (
|
|
214
|
+
endpoints if len(endpoints) > 0 else glob.glob(f"{project.path}/tests/**/*.y*ml", recursive=True)
|
|
215
|
+
)
|
|
236
216
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
try:
|
|
242
|
-
response = asyncio.run(
|
|
243
|
-
get_pipe_data(client, pipe_name=test_file_path.stem, test_params=test_params)
|
|
244
|
-
)
|
|
245
|
-
except Exception:
|
|
246
|
-
continue
|
|
217
|
+
def run_test(test_file):
|
|
218
|
+
test_file_path = Path(test_file)
|
|
219
|
+
click.echo(FeedbackManager.info(message=f"* {test_file_path.stem}{test_file_path.suffix}"))
|
|
220
|
+
test_file_content = yaml.safe_load(test_file_path.read_text())
|
|
247
221
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
222
|
+
for test in test_file_content:
|
|
223
|
+
try:
|
|
224
|
+
test_params = test["parameters"].split("?")[1] if "?" in test["parameters"] else test["parameters"]
|
|
225
|
+
response = None
|
|
226
|
+
try:
|
|
227
|
+
response = asyncio.run(
|
|
228
|
+
get_pipe_data(client, pipe_name=test_file_path.stem, test_params=test_params)
|
|
229
|
+
)
|
|
230
|
+
except Exception:
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
expected_result = response.text
|
|
234
|
+
if response.status_code >= 400:
|
|
235
|
+
expected_result = response.json()["error"]
|
|
236
|
+
if "expected_http_status" not in test:
|
|
237
|
+
raise Exception("Expected to not fail but got an error")
|
|
238
|
+
if test["expected_http_status"] != response.status_code:
|
|
239
|
+
raise Exception(f"Expected {test['expected_http_status']} but got {response.status_code}")
|
|
240
|
+
|
|
241
|
+
if test["expected_result"] != expected_result:
|
|
242
|
+
diff = difflib.ndiff(
|
|
243
|
+
test["expected_result"].splitlines(keepends=True), expected_result.splitlines(keepends=True)
|
|
244
|
+
)
|
|
245
|
+
printable_diff = "".join(diff)
|
|
246
|
+
raise Exception(
|
|
247
|
+
f"\nExpected: \n{test['expected_result']}\nGot: \n{expected_result}\nDiff: \n{printable_diff}"
|
|
248
|
+
)
|
|
249
|
+
click.echo(FeedbackManager.info(message=f"✓ {test['name']} passed"))
|
|
250
|
+
except Exception as e:
|
|
251
|
+
click.echo(FeedbackManager.error(message=f"✗ {test['name']} failed"))
|
|
252
|
+
click.echo(FeedbackManager.error(message=f"\n** Output and expected output are different: \n{e}"))
|
|
253
|
+
return False
|
|
254
|
+
return True
|
|
255
|
+
|
|
256
|
+
failed_tests_count = 0
|
|
257
|
+
test_count = len(test_files)
|
|
258
|
+
|
|
259
|
+
for test_file in test_files:
|
|
260
|
+
if not run_test(test_file):
|
|
261
|
+
failed_tests_count += 1
|
|
262
|
+
|
|
263
|
+
if failed_tests_count:
|
|
264
|
+
error = f"\n✗ {test_count - failed_tests_count}/{test_count} passed"
|
|
265
|
+
click.echo(FeedbackManager.error(message=error))
|
|
266
|
+
sys.exit(1)
|
|
267
|
+
else:
|
|
268
|
+
click.echo(FeedbackManager.success(message=f"\n✓ {test_count}/{test_count} passed"))
|
|
269
|
+
except Exception as e:
|
|
270
|
+
raise CLITestException(FeedbackManager.error(message=str(e)))
|
|
283
271
|
|
|
284
272
|
|
|
285
273
|
async def get_pipe_data(client: TinyB, pipe_name: str, test_params: str) -> Response:
|
|
@@ -296,3 +284,20 @@ async def get_pipe_data(client: TinyB, pipe_name: str, test_params: str) -> Resp
|
|
|
296
284
|
"pipeline": pipe_name,
|
|
297
285
|
}
|
|
298
286
|
return await client._req_raw(f"""/v0/sql?{urllib.parse.urlencode(params)}&{test_params}""")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def get_pipe_path(name_or_filename: str, folder: str) -> Path:
|
|
290
|
+
pipe_path: Optional[Path] = None
|
|
291
|
+
|
|
292
|
+
if ".pipe" in name_or_filename:
|
|
293
|
+
pipe_path = Path(name_or_filename)
|
|
294
|
+
if not pipe_path.exists():
|
|
295
|
+
pipe_path = None
|
|
296
|
+
else:
|
|
297
|
+
pipes = glob.glob(f"{folder}/**/{name_or_filename}.pipe", recursive=True)
|
|
298
|
+
pipe_path = next((Path(p) for p in pipes if Path(p).exists()), None)
|
|
299
|
+
|
|
300
|
+
if not pipe_path:
|
|
301
|
+
raise Exception(f"Pipe {name_or_filename} not found")
|
|
302
|
+
|
|
303
|
+
return pipe_path
|
|
@@ -12,34 +12,34 @@ tinybird/syncasync.py,sha256=IPnOx6lMbf9SNddN1eBtssg8vCLHMt76SuZ6YNYm-Yk,27761
|
|
|
12
12
|
tinybird/tornado_template.py,sha256=jjNVDMnkYFWXflmT8KU_Ssbo5vR8KQq3EJMk5vYgXRw,41959
|
|
13
13
|
tinybird/ch_utils/constants.py,sha256=aYvg2C_WxYWsnqPdZB1ZFoIr8ZY-XjUXYyHKE9Ansj0,3890
|
|
14
14
|
tinybird/ch_utils/engine.py,sha256=BZuPM7MFS7vaEKK5tOMR2bwSAgJudPrJt27uVEwZmTY,40512
|
|
15
|
-
tinybird/tb/__cli__.py,sha256=
|
|
15
|
+
tinybird/tb/__cli__.py,sha256=erSaiNuWIQUqfvRFXRdp6KocbeKh_UOlcrPrViJUkzI,252
|
|
16
16
|
tinybird/tb/cli.py,sha256=Xw5EhwCQu6Xx55DzoJsZJODdi7tkA391mii-xgGnpEU,1066
|
|
17
17
|
tinybird/tb/client.py,sha256=aaPKq5C77e72kR7IMv9WrvnvNki8mKMOTi9EsCp0eUc,55962
|
|
18
|
-
tinybird/tb/config.py,sha256=
|
|
18
|
+
tinybird/tb/config.py,sha256=HLMHbJg6BjeGZ2KiJA-BCCVnk8w959xsSaDEEePakZg,3981
|
|
19
19
|
tinybird/tb/modules/auth.py,sha256=_OeYnmTH83lnqCgQEdS6K0bx1KBUeRmZk2M7JnRmWpk,9037
|
|
20
|
-
tinybird/tb/modules/build.py,sha256=
|
|
20
|
+
tinybird/tb/modules/build.py,sha256=ohHiYwygJtldWsina-2QpvIt-g2hIqOkm5BEnh4JhdA,17187
|
|
21
21
|
tinybird/tb/modules/cicd.py,sha256=Ug7LijuHDIqKR6rm8OLZispWp75Zv6TyonAme8K9jaI,7114
|
|
22
22
|
tinybird/tb/modules/cli.py,sha256=Y_5hu9xwyTIZw4bQoe0MYLnRIzmR7hUjql_oZBxd4Qg,13407
|
|
23
|
-
tinybird/tb/modules/common.py,sha256=
|
|
23
|
+
tinybird/tb/modules/common.py,sha256=Uad-hW9VDYYQsqMYVTqysOTFoBMuVCwQqPhzVj99Nh0,84182
|
|
24
24
|
tinybird/tb/modules/config.py,sha256=ziqW_t_mRVvWOd85VoB4vKyvgMkEfpXDf9H4v38p2xc,11422
|
|
25
25
|
tinybird/tb/modules/connection.py,sha256=7qJGhTG0U__ZhY2HBWP8F1Z6tXrxwgAU-eOup6pXhhU,6905
|
|
26
26
|
tinybird/tb/modules/copy.py,sha256=2Mm4FWKehOG7CoOhiF1m9UZJgJn0W1_cMolqju8ONYg,5805
|
|
27
|
-
tinybird/tb/modules/create.py,sha256=
|
|
27
|
+
tinybird/tb/modules/create.py,sha256=hgfnV0zMOYDdXZ1nEK_eEQK66TBFM8XBHvHi_Y9R7z8,16439
|
|
28
28
|
tinybird/tb/modules/datasource.py,sha256=XvY7iTRefG9iEBLQkqBSF47bK-CrLu2AzDnigGI2mSc,16925
|
|
29
|
-
tinybird/tb/modules/deployment.py,sha256=
|
|
29
|
+
tinybird/tb/modules/deployment.py,sha256=M3NPFQXIEsDh9-J-pzPLuHF9k4SIZVvgUnHp05KOf-E,20448
|
|
30
30
|
tinybird/tb/modules/endpoint.py,sha256=XySDt3pk66vxOZ0egUfz4bY8bEk3BjOXkv-L0OIJ3sc,12083
|
|
31
|
-
tinybird/tb/modules/exceptions.py,sha256=
|
|
31
|
+
tinybird/tb/modules/exceptions.py,sha256=5jK91w1LPmtqIUfDpHe_Op5OxGz8-p1BPgtLREMIni0,5217
|
|
32
32
|
tinybird/tb/modules/feedback_manager.py,sha256=8I6n5JFPS4xP-Pfb9iu7hqx5VX773uGDXH2E5QeANeg,74179
|
|
33
33
|
tinybird/tb/modules/infra.py,sha256=eX0StUipaXaxIhMxO82cnAnFKTCa7EPOt-e8pWoSLX0,33206
|
|
34
34
|
tinybird/tb/modules/job.py,sha256=n4dSSBgnA8NqD7srGahf2xRj6wxkmX9Vl0J-QJ_a2w0,2966
|
|
35
35
|
tinybird/tb/modules/llm.py,sha256=KfsCYmKeW1VQz0iDZhGKCRkQv_Y3kTHh6JuxvofOguE,1076
|
|
36
36
|
tinybird/tb/modules/llm_utils.py,sha256=nS9r4FAElJw8yXtmdYrx-rtI2zXR8qXfi1QqUDCfxvg,3469
|
|
37
|
-
tinybird/tb/modules/local.py,sha256
|
|
38
|
-
tinybird/tb/modules/local_common.py,sha256=
|
|
39
|
-
tinybird/tb/modules/login.py,sha256=
|
|
37
|
+
tinybird/tb/modules/local.py,sha256=-oXIeVXjzdfxVPQaXw68YyQOb48EFQZeUys8_U0xePs,11300
|
|
38
|
+
tinybird/tb/modules/local_common.py,sha256=lyJ8vIFsDcVGYbfoCEBNgkwROsF5YJovxME1CRykEj8,3321
|
|
39
|
+
tinybird/tb/modules/login.py,sha256=8oybH3rwTqNrHx0gN2zG-9MGSYsKwLuD2hKZsKJtrQA,7152
|
|
40
40
|
tinybird/tb/modules/logout.py,sha256=ULooy1cDBD02-r7voZmhV7udA0ML5tVuflJyShrh56Y,1022
|
|
41
41
|
tinybird/tb/modules/materialization.py,sha256=GvnItYXzBKRZjcuOIuOEwtdM4LxYbTH4M5vGZR233Hc,5323
|
|
42
|
-
tinybird/tb/modules/mock.py,sha256=
|
|
42
|
+
tinybird/tb/modules/mock.py,sha256=tyhs6izqVGmDzMHdW5yHiprNPjeJDa2I-cLwrLRDC_U,4816
|
|
43
43
|
tinybird/tb/modules/open.py,sha256=OuctINN77oexpSjth9uoIZPCelKO4Li-yyVxeSnk1io,1371
|
|
44
44
|
tinybird/tb/modules/pipe.py,sha256=AQKEDagO6e3psPVjJkS_MDbn8aK-apAiLp26k7jgAV0,2432
|
|
45
45
|
tinybird/tb/modules/project.py,sha256=RdVOzJiuFPqkCzPhzGetbgcFeiMEn9IMviFCG58XKgI,3142
|
|
@@ -48,8 +48,8 @@ tinybird/tb/modules/secret.py,sha256=WsqzxxLh9W_jkuHL2JofMXdIJy0lT5WEI-7bQSIDgAc
|
|
|
48
48
|
tinybird/tb/modules/shell.py,sha256=cvT0EKpnSRHfuo3avykBRMsCwLBqmzSR7sNUdbQ6lXA,14116
|
|
49
49
|
tinybird/tb/modules/table.py,sha256=4XrtjM-N0zfNtxVkbvLDQQazno1EPXnxTyo7llivfXk,11035
|
|
50
50
|
tinybird/tb/modules/tag.py,sha256=anPmMUBc-TbFovlpFi8GPkKA18y7Y0GczMsMms5TZsU,3502
|
|
51
|
-
tinybird/tb/modules/telemetry.py,sha256=
|
|
52
|
-
tinybird/tb/modules/test.py,sha256=
|
|
51
|
+
tinybird/tb/modules/telemetry.py,sha256=IHHks8-jnrkyQIY_Y6ZmU8hdVHMGSS0pkSUOgCPcYY8,11346
|
|
52
|
+
tinybird/tb/modules/test.py,sha256=WmPoZBSHBORiM66xp54KxfY3s0YwjFMN_ZTT8xObFO4,12377
|
|
53
53
|
tinybird/tb/modules/token.py,sha256=2fmKwu10_M0pqs6YmJVeILR9ZQB0ejRAET86agASbKM,13488
|
|
54
54
|
tinybird/tb/modules/watch.py,sha256=_92co0BjTikQuy7MbHr4TDu9A75QdHsrAO8v7DlYLg4,8898
|
|
55
55
|
tinybird/tb/modules/workspace.py,sha256=SlK08psp0tu5t_URHyIczm696buW6KD6FPs9Lg1aNRE,6614
|
|
@@ -78,8 +78,8 @@ tinybird/tb_cli_modules/config.py,sha256=IsgdtFRnUrkY8-Zo32lmk6O7u3bHie1QCxLwgp4
|
|
|
78
78
|
tinybird/tb_cli_modules/exceptions.py,sha256=pmucP4kTF4irIt7dXiG-FcnI-o3mvDusPmch1L8RCWk,3367
|
|
79
79
|
tinybird/tb_cli_modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
|
|
80
80
|
tinybird/tb_cli_modules/telemetry.py,sha256=Hh2Io8ZPROSunbOLuMvuIFU4TqwWPmQTqal4WS09K1A,10449
|
|
81
|
-
tinybird-0.0.1.
|
|
82
|
-
tinybird-0.0.1.
|
|
83
|
-
tinybird-0.0.1.
|
|
84
|
-
tinybird-0.0.1.
|
|
85
|
-
tinybird-0.0.1.
|
|
81
|
+
tinybird-0.0.1.dev134.dist-info/METADATA,sha256=BVIpZF5O2MRJZ4HoxPRmqDbz-s0dG_-sQQKNZuC9hh0,1612
|
|
82
|
+
tinybird-0.0.1.dev134.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
83
|
+
tinybird-0.0.1.dev134.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
|
|
84
|
+
tinybird-0.0.1.dev134.dist-info/top_level.txt,sha256=VqqqEmkAy7UNaD8-V51FCoMMWXjLUlR0IstvK7tJYVY,54
|
|
85
|
+
tinybird-0.0.1.dev134.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|