tinybird 0.0.1.dev133__py3-none-any.whl → 0.0.1.dev135__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 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.dev133'
8
- __revision__ = 'aa46678'
7
+ __version__ = '0.0.1.dev135'
8
+ __revision__ = 'bde3e45'
tinybird/tb/config.py CHANGED
@@ -42,7 +42,7 @@ CLOUD_HOSTS = {
42
42
  }
43
43
 
44
44
 
45
- async def get_config(
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
- async with aiofiles.open(config_file or Path(getcwd()) / ".tinyb") as file:
54
- res = await file.read()
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
@@ -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
- sys.exit(1)
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
- output_pre_formatted = True
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
- if error:
191
- if not output_pre_formatted:
192
- raise click.ClickException(error)
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.error(message=e))
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
- sys.exit(1)
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"))
@@ -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
- exit_code = int(ex.code) if ex.code else 0
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
- click.echo(error_msg)
275
- add_telemetry_event(error_event, error=error_msg)
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
@@ -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 CLIException
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
- try:
74
- user_token = ctx_config.get("user_token")
75
- if not user_token:
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
- click.echo(FeedbackManager.error(message=f"Error: {str(e)}"))
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 CLIException(f"Error initializing Git: {e}")
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 echo_safe_humanfriendly_tables_format_smart_table, get_display_cloud_host
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
- sys.exit(1)
34
+ sys_exit("deployment_error", error)
31
35
  except Exception:
32
- click.echo(FeedbackManager.error(message="Error parsing response from API"))
33
- sys.exit(1)
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
- sys.exit(1)
59
+ sys_exit("deployment_error", error)
54
60
  return result
55
61
  except Exception:
56
- click.echo(FeedbackManager.error(message="Error parsing response from API"))
57
- sys.exit(1)
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
- click.echo(FeedbackManager.error(message="No deployments found"))
69
- sys.exit(1)
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
- click.echo(FeedbackManager.error(message="Only one deployment found"))
73
- sys.exit(1)
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
- sys.exit(1)
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.success(message="Removing old deployment"))
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
- sys.exit(1)
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.success(message="Deployment promotion successfully started"))
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 promoted successfully"))
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
- sys.exit(1)
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
- sys.exit(1)
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 Deployment submitted successfully"))
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.highlight(message="Not deploying. No changes."))
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
- sys.exit(1)
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
- sys.exit(1)
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
- sys.exit(1)
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
- sys.exit(1)
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.success(message="Deployment is ready"))
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.highlight(message="\n» Changes to be deployed...\n"))
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.highlight(message="\n» No changes to be deployed\n"))
532
+ click.echo(FeedbackManager.info(message="\n* No changes to be deployed"))
520
533
  if tokens:
521
- click.echo(FeedbackManager.highlight(message="\n» Changes in tokens to be deployed...\n"))
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.highlight(message="\n» No changes in tokens to be deployed\n"))
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
- data: Dict[str, Any] = {"error": message}
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)
@@ -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 CLIException("Tinybird Local is unhealthy. Try running `tb local restart` in a few seconds.")
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 CLIException(
129
- "No container runtime is running. Make sure a Docker-compatible runtime is installed and running."
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 CLIException
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 CLIException("Tinybird local is not running. Please run `tb local start` first.")
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"]
@@ -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 CLIAuthException
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
- host = host.rstrip("/")
119
- auth_host = auth_host.rstrip("/")
120
-
121
- auth_event = threading.Event()
122
- auth_code: list[str] = [] # Using a list to store the code, as it's mutable
123
-
124
- def auth_callback(code):
125
- auth_code.append(code)
126
- auth_event.set()
127
-
128
- click.echo(FeedbackManager.highlight(message="» Opening browser for authentication..."))
129
-
130
- # Start the local server in a separate thread
131
- server_thread = threading.Thread(target=start_server, args=(auth_callback, auth_host))
132
- server_thread.daemon = True
133
- server_thread.start()
134
-
135
- # Open the browser to the auth page
136
- callback_url = f"http://localhost:{AUTH_SERVER_PORT}"
137
- params = {
138
- "redirect_uri": callback_url,
139
- "apiHost": host,
140
- }
141
-
142
- if workspace:
143
- params["workspace"] = workspace
144
-
145
- auth_url = f"{auth_host}/api/cli-login?{urlencode(params)}"
146
- webbrowser.open(auth_url)
147
-
148
- # Wait for the authentication to complete or timeout
149
- if auth_event.wait(timeout=60): # Wait for up to 60 seconds
150
- params = {}
151
- params["code"] = auth_code[0]
152
- response = requests.get( # noqa: ASYNC210
153
- f"{auth_host}/api/cli-login?{urlencode(params)}",
154
- )
155
-
156
- data = response.json()
157
- cli_config = CLIConfig.get_project_config()
158
- cli_config.set_token(data.get("workspace_token", ""))
159
- cli_config.set_token_for_host(data.get("workspace_token", ""), host)
160
- cli_config.set_user_token(data.get("user_token", ""))
161
- host = data.get("api_host", host)
162
- cli_config.set_host(host)
163
- ws = await cli_config.get_client(token=data.get("workspace_token", ""), host=host).workspace_info(version="v1")
164
- for k in ("id", "name", "user_email", "user_id", "scope"):
165
- if k in ws:
166
- cli_config[k] = ws[k]
167
-
168
- path = os.path.join(os.getcwd(), ".tinyb")
169
- cli_config.persist_to_file(override_with_path=path)
170
-
171
- auth_info: Dict[str, Any] = await cli_config.get_user_client().check_auth_login()
172
- if not auth_info.get("is_valid", False):
173
- raise CLIAuthException(FeedbackManager.error_auth_login_not_valid(host=cli_config.get_host()))
174
-
175
- if not auth_info.get("is_user", False):
176
- raise CLIAuthException(FeedbackManager.error_auth_login_not_user(host=cli_config.get_host()))
177
-
178
- click.echo(FeedbackManager.gray(message="\nWorkspace: ") + FeedbackManager.info(message=ws["name"]))
179
- click.echo(FeedbackManager.gray(message="User: ") + FeedbackManager.info(message=ws["user_email"]))
180
- click.echo(FeedbackManager.gray(message="Host: ") + FeedbackManager.info(message=host))
181
- click.echo(FeedbackManager.success(message="\n✓ Authentication successful!"))
182
- else:
183
- click.echo(FeedbackManager.error(message="Authentication failed or timed out."))
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)))
@@ -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 CLIException, coro, push_data
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 CLIException(f"Datasource '{datasource_path.stem}' not found")
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
- try:
71
- if not user_token:
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 CLIException(error_response)
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
- click.echo(FeedbackManager.error_exception(error=f"Error: {e}"))
112
+ raise CLIMockException(FeedbackManager.error(message=str(e)))
116
113
 
117
114
 
118
115
  async def append_fixture(
@@ -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 = async_to_sync(get_config)(None, None)
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", "")
@@ -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 CLIException
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
- if ".pipe" in name_or_filename:
88
- pipe_path = Path(name_or_filename)
89
- if not pipe_path.exists():
90
- raise CLIException(FeedbackManager.error(message=f"Pipe {name_or_filename} not found"))
91
- else:
92
- pipe_folders = ("endpoints", "copies", "materializations", "sinks", "pipes")
93
- pipe_path = next(
94
- root_path / folder / f"{name_or_filename}.pipe"
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"\n» Creating tests for {pipe_name} endpoint..."))
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 CLIException(FeedbackManager.error(message="No user token found"))
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 CLIException(FeedbackManager.error_exception(error=e))
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 = Path(pipe)
176
- pipe_name = pipe
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"{pipe}.yaml")
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
- click.echo(FeedbackManager.error(message=str(e)))
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
- client: TinyB = ctx.ensure_object(dict)["client"]
220
- project: Project = ctx.ensure_object(dict)["project"]
221
- click.echo(FeedbackManager.highlight(message="\n» Building project"))
222
- process(project=project, tb_client=client, watch=False, silent=True)
223
- click.echo(FeedbackManager.info(message="✓ Done!"))
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
- def run_test(test_file):
233
- test_file_path = Path(test_file)
234
- click.echo(FeedbackManager.info(message=f"* {test_file_path.stem}{test_file_path.suffix}"))
235
- test_file_content = yaml.safe_load(test_file_path.read_text())
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
- for test in test_file_content:
238
- try:
239
- test_params = test["parameters"].split("?")[1] if "?" in test["parameters"] else test["parameters"]
240
- response = None
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
- expected_result = response.text
249
- if response.status_code >= 400:
250
- expected_result = response.json()["error"]
251
- if "expected_http_status" not in test:
252
- raise Exception("Expected to not fail but got an error")
253
- if test["expected_http_status"] != response.status_code:
254
- raise Exception(f"Expected {test['expected_http_status']} but got {response.status_code}")
255
-
256
- if test["expected_result"] != expected_result:
257
- diff = difflib.ndiff(
258
- test["expected_result"].splitlines(keepends=True), expected_result.splitlines(keepends=True)
259
- )
260
- printable_diff = "".join(diff)
261
- raise Exception(
262
- f"\nExpected: \n{test['expected_result']}\nGot: \n{expected_result}\nDiff: \n{printable_diff}"
263
- )
264
- click.echo(FeedbackManager.info(message=f"✓ {test['name']} passed"))
265
- except Exception as e:
266
- click.echo(FeedbackManager.error(message=f"✗ {test['name']} failed"))
267
- click.echo(FeedbackManager.error(message=f"\n** Output and expected output are different: \n{e}"))
268
- return False
269
- return True
270
-
271
- failed_tests_count = 0
272
- test_count = len(test_files)
273
-
274
- for test_file in test_files:
275
- if not run_test(test_file):
276
- failed_tests_count += 1
277
-
278
- if failed_tests_count:
279
- click.echo(FeedbackManager.error(message=f"\n✗ {test_count - failed_tests_count}/{test_count} passed"))
280
- sys.exit(1)
281
- else:
282
- click.echo(FeedbackManager.success(message=f"\n✓ {test_count}/{test_count} passed"))
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: tinybird
3
- Version: 0.0.1.dev133
3
+ Version: 0.0.1.dev135
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -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=pUQYSMtGIN0vu5mnf42KNZzpAaEz0aTgpMckE_sV-c0,252
15
+ tinybird/tb/__cli__.py,sha256=Sv56aDmMPIxanN-B7Nt0rBkTsZFUjbU4DYKqfQU9C5A,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=iIia7F67UI9p51Ja4jALonyttIi4eH7PwOn3HvcvOb8,4008
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=KqXMJJW8JEuTlH3KyV1ylCLUob_oFhwDTM7bYLgrK9s,17359
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=bdKNjHtQKcsZFyZIsLY8vEoTD0Xf-1UNBnh1pAyWPZU,83456
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=PDAuzeBiqN9q7GY2p4zcMWDojdNjz6_aJgI5dxR-FVM,16701
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=4Zt7jPbqt18fB5kPx7DbO91Bh6xzBBTEUFY7O89shuU,19560
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=4A2sSjCEqKUMqpP3WI00zouCWW4uLaghXXLZBSw04mY,3363
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=702U0p9pPFwZHkVNEsp2Y5gJxBKtOvd2XoS4dn1p-m8,11111
38
- tinybird/tb/modules/local_common.py,sha256=75Mje9_vkq7VehSKHS1zJB8UyVsJpqDFmuJGWwQmrXQ,3193
39
- tinybird/tb/modules/login.py,sha256=rLYdV7MLCTT1F4VBAWGv6SO9efMiM9-977ayLecUzjI,6842
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=KScG2WohfCjxakBS2Rfb4-7QKS4OSkoVgiMyUvkCWWE,4924
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=FcFw_70-0SyC_2BdToF_sjVpXpMD7i00fCm2SO4cHx8,11397
52
- tinybird/tb/modules/test.py,sha256=9qNIM5bosYlaZYd5rorC5zcrTmjZeTUSuOIP-wkjXXI,12132
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.dev133.dist-info/METADATA,sha256=y0qf78t8pUbZYXxlRWXKybZCiiGD6UhpW7OombLD4Hs,1612
82
- tinybird-0.0.1.dev133.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
83
- tinybird-0.0.1.dev133.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
84
- tinybird-0.0.1.dev133.dist-info/top_level.txt,sha256=VqqqEmkAy7UNaD8-V51FCoMMWXjLUlR0IstvK7tJYVY,54
85
- tinybird-0.0.1.dev133.dist-info/RECORD,,
81
+ tinybird-0.0.1.dev135.dist-info/METADATA,sha256=wsn6Nt_nggLsKgKmvnM1E9PYRNz38DHGGt0exMJ-3DI,1612
82
+ tinybird-0.0.1.dev135.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
83
+ tinybird-0.0.1.dev135.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
84
+ tinybird-0.0.1.dev135.dist-info/top_level.txt,sha256=VqqqEmkAy7UNaD8-V51FCoMMWXjLUlR0IstvK7tJYVY,54
85
+ tinybird-0.0.1.dev135.dist-info/RECORD,,