tinybird 0.0.1.dev29__py3-none-any.whl → 0.0.1.dev31__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.

@@ -9,7 +9,7 @@ import os
9
9
  import pprint
10
10
  from os import getcwd
11
11
  from pathlib import Path
12
- from typing import Any, Callable, Iterable, List, Optional, Tuple, Union
12
+ from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
13
13
 
14
14
  import click
15
15
  import humanfriendly
@@ -20,7 +20,8 @@ from tinybird.client import (
20
20
  AuthNoTokenException,
21
21
  TinyB,
22
22
  )
23
- from tinybird.config import SUPPORTED_CONNECTORS, get_config
23
+ from tinybird.config import get_config
24
+ from tinybird.prompts import ask_prompt
24
25
  from tinybird.tb import __cli__
25
26
  from tinybird.tb.modules.common import (
26
27
  CatchAuthExceptions,
@@ -30,7 +31,6 @@ from tinybird.tb.modules.common import (
30
31
  echo_safe_format_table,
31
32
  get_current_main_workspace,
32
33
  getenv_bool,
33
- load_connector_config,
34
34
  try_update_config_with_remote,
35
35
  )
36
36
  from tinybird.tb.modules.config import CLIConfig
@@ -94,7 +94,7 @@ VERSION = f"{__cli__.__version__} (rev {__cli__.__revision__})"
94
94
  "--with-headers", help="Flag to enable connector to export with headers", is_flag=True, default=False, hidden=True
95
95
  )
96
96
  @click.option("--show-tokens", is_flag=True, default=False, help="Enable the output of tokens")
97
- @click.option("--local", is_flag=True, default=False, help="Run in local mode")
97
+ @click.option("--prod", is_flag=True, default=False, help="Run against production")
98
98
  @click.version_option(version=VERSION)
99
99
  @click.pass_context
100
100
  @coro
@@ -117,14 +117,13 @@ async def cli(
117
117
  sf_stage,
118
118
  with_headers: bool,
119
119
  show_tokens: bool,
120
- local: bool,
120
+ prod: bool,
121
121
  ) -> None:
122
122
  """
123
123
  Use `OBFUSCATE_REGEX_PATTERN` and `OBFUSCATE_PATTERN_SEPARATOR` environment variables to define a regex pattern and a separator (in case of a single string with multiple regex) to obfuscate secrets in the CLI output.
124
124
  """
125
-
126
125
  # We need to unpatch for our tests not to break
127
- if show_tokens or local or ctx.invoked_subcommand == "build":
126
+ if show_tokens or not prod or ctx.invoked_subcommand == "build":
128
127
  __unpatch_click_output()
129
128
  else:
130
129
  __patch_click_output()
@@ -211,12 +210,11 @@ async def cli(
211
210
 
212
211
  logging.debug("debug enabled")
213
212
 
214
- ctx.ensure_object(dict)["client"] = (
215
- await get_tinybird_local_client() if local else _get_tb_client(config.get("token", None), config["host"])
216
- )
213
+ skip_client = ctx.invoked_subcommand in ["login", "workspace"]
214
+ client = await create_ctx_client(config, prod, skip_client)
217
215
 
218
- for connector in SUPPORTED_CONNECTORS:
219
- load_connector_config(ctx, connector, debug, check_uninstalled=True)
216
+ if client:
217
+ ctx.ensure_object(dict)["client"] = client
220
218
 
221
219
 
222
220
  @cli.command(hidden=True)
@@ -489,11 +487,10 @@ async def sql(
489
487
 
490
488
  @cli.command(hidden=True)
491
489
  @click.argument("prompt")
492
- @click.option("--model", default="gpt-4o", help="The model to use for the LLM")
493
490
  @click.option("--folder", default=".", help="The folder to use for the project")
494
491
  @coro
495
- async def llm(prompt: str, model: str, folder: str) -> None:
496
- """Run a prompt against the LLM api."""
492
+ async def ask(prompt: str, folder: str) -> None:
493
+ """Ask things about your data project."""
497
494
  try:
498
495
  config = CLIConfig.get_project_config(folder)
499
496
  user_token = config.get_user_token()
@@ -501,9 +498,39 @@ async def llm(prompt: str, model: str, folder: str) -> None:
501
498
  if not user_token:
502
499
  raise CLIException("This action requires authentication. Run 'tb login' first.")
503
500
 
501
+ datasource_paths = [
502
+ Path(folder) / "datasources" / f
503
+ for f in os.listdir(Path(folder) / "datasources")
504
+ if f.endswith(".datasource")
505
+ ]
506
+ pipe_folders = ["endpoints", "pipes", "copies", "materializations", "sinks"]
507
+ pipe_paths = [
508
+ Path(folder) / pipe_folder / f
509
+ for pipe_folder in pipe_folders
510
+ if (Path(folder) / pipe_folder).exists()
511
+ for f in os.listdir(Path(folder) / pipe_folder)
512
+ if f.endswith(".pipe")
513
+ ]
514
+ resources_xml = "\n".join(
515
+ [
516
+ f"<resource><type>{resource_type}</type><name>{resource_name}</name><content>{resource_content}</content></resource>"
517
+ for resource_type, resource_name, resource_content in [
518
+ ("datasource", ds.stem, ds.read_text()) for ds in datasource_paths
519
+ ]
520
+ + [
521
+ (
522
+ "pipe",
523
+ pipe.stem,
524
+ pipe.read_text(),
525
+ )
526
+ for pipe in pipe_paths
527
+ ]
528
+ ]
529
+ )
530
+
504
531
  client = config.get_client()
505
532
  llm = LLM(user_token=user_token, client=client)
506
- click.echo(await llm.ask(prompt, model=model))
533
+ click.echo(await llm.ask(system_prompt=ask_prompt(resources_xml), prompt=prompt))
507
534
  except Exception as e:
508
535
  raise CLIException(FeedbackManager.error_exception(error=e))
509
536
 
@@ -543,3 +570,13 @@ def __patch_click_output():
543
570
  def __unpatch_click_output():
544
571
  click.echo = __old_click_echo
545
572
  click.secho = __old_click_secho
573
+
574
+
575
+ async def create_ctx_client(config: Dict[str, Any], prod: bool, skip_client: bool):
576
+ if skip_client:
577
+ return None
578
+
579
+ if prod:
580
+ return _get_tb_client(config.get("token", None), config["host"])
581
+
582
+ return await get_tinybird_local_client()
@@ -175,7 +175,7 @@ def generate_datafile(
175
175
  if not f.exists() or force:
176
176
  with open(f"{f}", "w") as ds_file:
177
177
  ds_file.write(datafile)
178
- click.echo(FeedbackManager.info_file_created(file=f.relative_to(folder)))
178
+ click.echo(FeedbackManager.info_file_created(file=f.relative_to(folder or ".")))
179
179
 
180
180
  if data and (base / "fixtures").exists():
181
181
  # Generating a fixture for Parquet files is not so trivial, since Parquet format
@@ -1177,31 +1177,6 @@ async def print_current_workspace(config: CLIConfig) -> None:
1177
1177
  echo_safe_humanfriendly_tables_format_smart_table(table, column_names=columns)
1178
1178
 
1179
1179
 
1180
- async def print_current_branch(config: CLIConfig) -> None:
1181
- _ = await try_update_config_with_remote(config, only_if_needed=True)
1182
-
1183
- response = await config.get_client().user_workspaces_and_branches()
1184
-
1185
- columns = ["name", "id", "workspace"]
1186
- table = []
1187
-
1188
- for workspace in response["workspaces"]:
1189
- if config["id"] == workspace["id"]:
1190
- click.echo(FeedbackManager.info_current_branch())
1191
- if workspace.get("is_branch"):
1192
- name = workspace["name"]
1193
- main_workspace = await get_current_main_workspace(config)
1194
- assert isinstance(main_workspace, dict)
1195
- main_name = main_workspace["name"]
1196
- else:
1197
- name = MAIN_BRANCH
1198
- main_name = workspace["name"]
1199
- table.append([name, workspace["id"], main_name])
1200
- break
1201
-
1202
- echo_safe_humanfriendly_tables_format_smart_table(table, column_names=columns)
1203
-
1204
-
1205
1180
  class ConnectionReplacements:
1206
1181
  _PARAMS_REPLACEMENTS: Dict[str, Dict[str, str]] = {
1207
1182
  "s3": {
@@ -323,14 +323,6 @@ class CLIConfig:
323
323
  path: str = os.path.join(working_dir, ".tinyb")
324
324
  return CLIConfig(path, parent=CLIConfig.get_global_config())
325
325
 
326
- @staticmethod
327
- def get_llm_config(working_dir: Optional[str] = None) -> Dict[str, Any]:
328
- return (
329
- CLIConfig.get_project_config(working_dir)
330
- .get("llms", {})
331
- .get("openai", {"model": "gpt-4o-mini", "api_key": None})
332
- )
333
-
334
326
  @staticmethod
335
327
  def reset() -> None:
336
328
  CLIConfig._global = None
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import re
2
3
  from os import getcwd
3
4
  from pathlib import Path
4
5
  from typing import Optional
@@ -108,7 +109,7 @@ async def create(
108
109
  has_json_path = "`json:" in datasource_content
109
110
  if has_json_path:
110
111
  prompt = f"<datasource_schema>{datasource_content}</datasource_schema>\n<user_input>{prompt}</user_input>"
111
- response = await llm.ask(prompt, system_prompt=mock_prompt(rows))
112
+ response = await llm.ask(system_prompt=mock_prompt(rows), prompt=prompt)
112
113
  sql = extract_xml(response, "sql")
113
114
  sql = sql.split("FORMAT")[0]
114
115
  result = await local_client.query(f"{sql} FORMAT JSON")
@@ -205,7 +206,7 @@ TYPE ENDPOINT
205
206
  ]
206
207
  )
207
208
  llm = LLM(user_token=user_token, client=tb_client)
208
- result = await llm.ask(prompt, system_prompt=create_prompt(resources_xml))
209
+ result = await llm.ask(system_prompt=create_prompt(resources_xml), prompt=prompt)
209
210
  result = extract_xml(result, "response")
210
211
  resources = parse_xml(result, "resource")
211
212
  datasources = []
@@ -260,7 +261,25 @@ def init_git(folder: str):
260
261
 
261
262
 
262
263
  def generate_pipe_file(name: str, content: str, folder: str):
263
- base = Path(folder) / "endpoints"
264
+ def is_copy(content: str) -> bool:
265
+ return re.search(r"TYPE copy", content, re.IGNORECASE) is not None
266
+
267
+ def is_materialization(content: str) -> bool:
268
+ return re.search(r"TYPE materialized", content, re.IGNORECASE) is not None
269
+
270
+ def is_sink(content: str) -> bool:
271
+ return re.search(r"TYPE sink", content, re.IGNORECASE) is not None
272
+
273
+ if is_copy(content):
274
+ pathname = "copies"
275
+ elif is_materialization(content):
276
+ pathname = "materializations"
277
+ elif is_sink(content):
278
+ pathname = "sinks"
279
+ else:
280
+ pathname = "endpoints"
281
+
282
+ base = Path(folder) / pathname
264
283
  if not base.exists():
265
284
  base = Path()
266
285
  f = base / (f"{name}.pipe")
@@ -768,13 +768,13 @@ async def process(
768
768
  raise click.ClickException(FeedbackManager.error_forkdownstream_pipes_with_engine(pipe=resource_name))
769
769
 
770
770
  to_run[resource_name] = r
771
- file_deps = r.get("deps", [])
771
+ file_deps: List[str] = r.get("deps", [])
772
772
  deps += file_deps
773
773
  # calculate and look for deps
774
774
  dep_list = []
775
775
  for x in file_deps:
776
776
  if x not in INTERNAL_TABLES or is_internal:
777
- f, ds = find_file_by_name(dir_path, x, verbose, vendor_paths=vendor_paths, resource=r)
777
+ f, ds = find_file_by_name(dir_path or ".", x, verbose, vendor_paths=vendor_paths, resource=r)
778
778
  if f:
779
779
  dep_list.append(f.rsplit(".", 1)[0])
780
780
  if ds:
@@ -266,7 +266,19 @@ async def new_pipe(
266
266
 
267
267
  if data.get("type") == "endpoint":
268
268
  token = tb_client.token
269
- click.echo(f"""** => Test endpoint with:\n** $ curl {host}/v0/pipes/{p["name"]}.json?token={token}""")
269
+ try:
270
+ example_params = {
271
+ "format": "json",
272
+ "pipe": p["name"],
273
+ "q": "",
274
+ "token": token,
275
+ }
276
+ endpoint_url = await tb_client._req(f"/examples/query.http?{urlencode(example_params)}")
277
+ if endpoint_url:
278
+ endpoint_url = endpoint_url.replace("http://localhost:8001", host)
279
+ click.echo(f"""** => Test endpoint with:\n** $ curl {endpoint_url}""")
280
+ except Exception:
281
+ pass
270
282
 
271
283
 
272
284
  async def get_token_from_main_branch(branch_tb_client: TinyB) -> Optional[str]:
@@ -542,7 +542,7 @@ async def datasource_share(ctx: Context, datasource_name: str, workspace_name_or
542
542
  """Share a datasource"""
543
543
 
544
544
  config = CLIConfig.get_project_config()
545
- client = config.get_client()
545
+ client: TinyB = ctx.ensure_object(dict)["client"]
546
546
  host = config.get_host() or CLIConfig.DEFAULTS["host"]
547
547
  ui_host = get_display_host(host)
548
548
 
@@ -1,33 +1,9 @@
1
- import json
2
- import urllib.parse
3
1
  from copy import deepcopy
4
- from typing import List
5
-
6
- from pydantic import BaseModel
2
+ from typing import Optional
7
3
 
8
4
  from tinybird.client import TinyB
9
5
 
10
6
 
11
- class DataFile(BaseModel):
12
- name: str
13
- content: str
14
-
15
-
16
- class DataProject(BaseModel):
17
- datasources: List[DataFile]
18
- pipes: List[DataFile]
19
-
20
-
21
- class TestExpectation(BaseModel):
22
- name: str
23
- description: str
24
- parameters: str
25
-
26
-
27
- class TestExpectations(BaseModel):
28
- tests: List[TestExpectation]
29
-
30
-
31
7
  class LLM:
32
8
  def __init__(
33
9
  self,
@@ -37,69 +13,26 @@ class LLM:
37
13
  self.user_client = deepcopy(client)
38
14
  self.user_client.token = user_token
39
15
 
40
- async def ask(self, prompt: str, system_prompt: str = "", model: str = "o1-mini") -> str:
16
+ async def ask(self, system_prompt: str, prompt: Optional[str] = None) -> str:
41
17
  """
42
18
  Calls the model with the given prompt and returns the response.
43
19
 
44
20
  Args:
21
+ system_prompt (str): The system prompt to send to the model.
45
22
  prompt (str): The user prompt to send to the model.
46
23
 
47
24
  Returns:
48
25
  str: The response from the language model.
49
26
  """
50
- messages = []
51
27
 
52
- if system_prompt:
53
- messages.append({"role": "user", "content": system_prompt})
28
+ data = {"system": system_prompt}
54
29
 
55
30
  if prompt:
56
- messages.append({"role": "user", "content": prompt})
31
+ data["prompt"] = prompt
57
32
 
58
- data = {
59
- "model": model,
60
- "messages": messages,
61
- }
62
33
  response = await self.user_client._req(
63
34
  "/v0/llm",
64
35
  method="POST",
65
- data=json.dumps(data),
66
- headers={"Content-Type": "application/json"},
36
+ data=data,
67
37
  )
68
38
  return response.get("result", "")
69
-
70
- async def create_project(self, prompt: str) -> DataProject:
71
- try:
72
- prompt = (
73
- prompt
74
- + "\n#More extra context\n- If you add some array data type remember that the json path should be like this: `json:$.array_field[:]`"
75
- )
76
- response = await self.user_client._req(
77
- "/v0/llm/create",
78
- method="POST",
79
- data=f'{{"prompt": {json.dumps(prompt)}}}',
80
- headers={"Content-Type": "application/json"},
81
- )
82
-
83
- return DataProject.model_validate(response.get("result", {}))
84
- except Exception:
85
- return DataProject(datasources=[], pipes=[])
86
-
87
- async def generate_sql_sample_data(self, schema: str, rows: int = 20, prompt: str = "") -> str:
88
- response = await self.user_client._req(
89
- "/v0/llm/mock",
90
- method="POST",
91
- data=f'{{"schema": "{urllib.parse.quote(schema)}", "rows": {rows}, "context": "{prompt}"}}',
92
- headers={"Content-Type": "application/json"},
93
- )
94
- result = response.get("result", "")
95
- return result.replace("elementAt", "arrayElement")
96
-
97
- async def create_tests(self, pipe_content: str, pipe_params: set[str], prompt: str = "") -> TestExpectations:
98
- response = await self.user_client._req(
99
- "/v0/llm/create/tests",
100
- method="POST",
101
- data=json.dumps({"pipe_content": pipe_content, "pipe_params": list(pipe_params), "prompt": prompt}),
102
- headers={"Content-Type": "application/json"},
103
- )
104
- result = response.get("result", "")
105
- return TestExpectations.model_validate(result)
@@ -82,7 +82,7 @@ def get_docker_client():
82
82
  client.ping()
83
83
  return client
84
84
  except Exception:
85
- raise CLIException("Docker is not running or installed. Make sure Docker is installed and running.")
85
+ raise CLIException("No container runtime is running. Make sure a Docker-compatible runtime is installed and running.")
86
86
 
87
87
 
88
88
  def stop_tinybird_local(docker_client):
@@ -8,7 +8,7 @@ from tinybird.client import TinyB
8
8
  from tinybird.tb.modules.config import CLIConfig
9
9
  from tinybird.tb.modules.exceptions import CLIException
10
10
 
11
- TB_IMAGE_NAME = "tinybirdco/tinybird-local:latest"
11
+ TB_IMAGE_NAME = "tinybirdco/tinybird-local:beta"
12
12
  TB_CONTAINER_NAME = "tinybird-local"
13
13
  TB_LOCAL_PORT = int(os.getenv("TB_LOCAL_PORT", 80))
14
14
  TB_LOCAL_HOST = f"http://localhost:{TB_LOCAL_PORT}"
@@ -135,7 +135,12 @@ async def login(host: str, workspace: str):
135
135
  server_thread.start()
136
136
 
137
137
  # Open the browser to the auth page
138
- client_id = "T6excMo8IKguvUw4vFNYfqlt9pe6msCU"
138
+ if "wadus" in host:
139
+ client_id = "Rpl7Uy9aSjqoPCSvHgGl3zNQuZcSOXBe"
140
+ base_auth_url = "https://auth.wadus1.tinybird.co"
141
+ else:
142
+ client_id = "T6excMo8IKguvUw4vFNYfqlt9pe6msCU"
143
+ base_auth_url = "https://auth.tinybird.co"
139
144
  callback_url = f"http://localhost:{AUTH_SERVER_PORT}"
140
145
  params = {
141
146
  "client_id": client_id,
@@ -143,7 +148,7 @@ async def login(host: str, workspace: str):
143
148
  "response_type": "token",
144
149
  "scope": "openid profile email",
145
150
  }
146
- auth_url = f"https://auth.tinybird.co/authorize?{urlencode(params)}"
151
+ auth_url = f"{base_auth_url}/authorize?{urlencode(params)}"
147
152
  webbrowser.open(auth_url)
148
153
 
149
154
  # Wait for the authentication to complete or timeout
@@ -66,7 +66,7 @@ async def mock(datasource: str, rows: int, prompt: str, folder: str) -> None:
66
66
  llm = LLM(user_token=user_token, client=user_client)
67
67
  tb_client = await get_tinybird_local_client(os.path.abspath(folder))
68
68
  prompt = f"<datasource_schema>{datasource_content}</datasource_schema>\n<user_input>{prompt}</user_input>"
69
- response = await llm.ask(prompt, system_prompt=mock_prompt(rows))
69
+ response = await llm.ask(system_prompt=mock_prompt(rows), prompt=prompt)
70
70
  sql = extract_xml(response, "sql")
71
71
  if os.environ.get("TB_DEBUG", "") != "":
72
72
  logging.debug(sql)
@@ -1,8 +1,10 @@
1
1
  import asyncio
2
2
  import concurrent.futures
3
+ import glob
3
4
  import os
4
5
  import subprocess
5
6
  import sys
7
+ from pathlib import Path
6
8
  from typing import List
7
9
 
8
10
  import click
@@ -21,17 +23,22 @@ from tinybird.tb.modules.table import format_table
21
23
 
22
24
 
23
25
  class DynamicCompleter(Completer):
24
- def __init__(self, datasources: List[str], shared_datasources: List[str], endpoints: List[str], pipes: List[str]):
25
- self.datasources = datasources
26
- self.shared_datasources = shared_datasources
27
- self.endpoints = endpoints
28
- self.pipes = pipes
26
+ def __init__(self, folder: str):
27
+ self.folder = folder
29
28
  self.static_commands = ["create", "mock", "test", "select"]
30
29
  self.test_commands = ["create", "run", "update"]
31
30
  self.mock_flags = ["--prompt", "--rows"]
32
31
  self.common_rows = ["10", "50", "100", "500", "1000"]
33
32
  self.sql_keywords = ["select", "from", "where", "group by", "order by", "limit"]
34
33
 
34
+ @property
35
+ def datasources(self):
36
+ return [Path(f).stem for f in glob.glob(f"{self.folder}/**/*.datasource", recursive=True)]
37
+
38
+ @property
39
+ def pipes(self):
40
+ return [Path(f).stem for f in glob.glob(f"{self.folder}/**/*.pipe", recursive=True)]
41
+
35
42
  def get_completions(self, document, complete_event):
36
43
  text = document.text_before_cursor.strip()
37
44
  words = text.split()
@@ -75,9 +82,9 @@ class DynamicCompleter(Completer):
75
82
  if words[-1] == "from" or (
76
83
  "from" in words and len(words) > words.index("from") + 1 and text_lower.endswith(" ")
77
84
  ):
78
- for x in self.datasources + self.shared_datasources:
85
+ for x in self.datasources:
79
86
  yield Completion(x, start_position=0, display=x, style="class:completion.datasource")
80
- for x in self.endpoints + self.pipes:
87
+ for x in self.pipes:
81
88
  yield Completion(x, start_position=0, display=x, style="class:completion.pipe")
82
89
  return
83
90
 
@@ -116,7 +123,7 @@ class DynamicCompleter(Completer):
116
123
  yield Completion(cmd, start_position=0, display=cmd, style="class:completion.cmd")
117
124
  return
118
125
  elif len(words) == 2:
119
- for cmd in self.endpoints:
126
+ for cmd in self.pipes:
120
127
  yield Completion(cmd, start_position=0, display=cmd, style="class:completion.pipe")
121
128
  return
122
129
 
@@ -130,7 +137,7 @@ class DynamicCompleter(Completer):
130
137
  style="class:completion.cmd",
131
138
  )
132
139
 
133
- for cmd in self.datasources + self.shared_datasources:
140
+ for cmd in self.datasources:
134
141
  if cmd.startswith(current_word):
135
142
  yield Completion(
136
143
  cmd,
@@ -139,7 +146,7 @@ class DynamicCompleter(Completer):
139
146
  style="class:completion.datasource",
140
147
  )
141
148
 
142
- for cmd in self.endpoints + self.pipes:
149
+ for cmd in self.pipes:
143
150
  if cmd.startswith(current_word):
144
151
  yield Completion(
145
152
  cmd,
@@ -176,26 +183,14 @@ def _(event):
176
183
 
177
184
 
178
185
  class Shell:
179
- def __init__(
180
- self,
181
- folder: str,
182
- client: TinyB,
183
- datasources: List[str],
184
- shared_datasources: List[str],
185
- pipes: List[str],
186
- endpoints: List[str],
187
- ):
186
+ def __init__(self, folder: str, client: TinyB):
188
187
  self.history = self.get_history()
189
188
  self.folder = folder
190
189
  self.client = client
191
- self.datasources = datasources
192
- self.shared_datasources = shared_datasources
193
- self.pipes = pipes
194
- self.endpoints = endpoints
195
190
  self.prompt_message = "\ntb > "
196
191
  self.commands = ["create", "mock", "test", "tb", "select"]
197
192
  self.session: PromptSession = PromptSession(
198
- completer=DynamicCompleter(self.datasources, self.shared_datasources, self.endpoints, self.pipes),
193
+ completer=DynamicCompleter(folder),
199
194
  complete_style=CompleteStyle.COLUMN,
200
195
  complete_while_typing=True,
201
196
  history=self.history,
@@ -274,7 +269,7 @@ class Shell:
274
269
  elif arg.startswith("mock"):
275
270
  self.handle_mock(arg)
276
271
  else:
277
- subprocess.run(f"tb --local {arg}", shell=True, text=True)
272
+ subprocess.run(f"tb {arg}", shell=True, text=True)
278
273
 
279
274
  def default(self, argline):
280
275
  click.echo("")
@@ -283,10 +278,10 @@ class Shell:
283
278
  return
284
279
  if arg.startswith("with") or arg.startswith("select"):
285
280
  self.run_sql(argline)
286
- elif len(arg.split()) == 1 and arg in self.endpoints + self.pipes + self.datasources + self.shared_datasources:
281
+ elif len(arg.split()) == 1 and arg in self.pipes + self.datasources:
287
282
  self.run_sql(f"select * from {arg}")
288
283
  else:
289
- subprocess.run(f"tb --local {arg}", shell=True, text=True)
284
+ subprocess.run(f"tb {arg}", shell=True, text=True)
290
285
 
291
286
  def run_sql(self, query, rows_limit=20):
292
287
  try:
@@ -310,16 +305,16 @@ class Shell:
310
305
  with concurrent.futures.ThreadPoolExecutor() as executor:
311
306
  res = executor.submit(run_query_in_thread).result()
312
307
 
313
- except Exception as e:
314
- click.echo(FeedbackManager.error_exception(error=str(e)))
308
+ if isinstance(res, dict) and "error" in res:
309
+ click.echo(FeedbackManager.error_exception(error=res["error"]))
315
310
 
316
- if isinstance(res, dict) and "error" in res:
317
- click.echo(FeedbackManager.error_exception(error=res["error"]))
311
+ if isinstance(res, dict) and "data" in res and res["data"]:
312
+ print_table_formatted(res, "QUERY")
313
+ else:
314
+ click.echo(FeedbackManager.info_no_rows())
318
315
 
319
- if isinstance(res, dict) and "data" in res and res["data"]:
320
- print_table_formatted(res, "QUERY")
321
- else:
322
- click.echo(FeedbackManager.info_no_rows())
316
+ except Exception as e:
317
+ click.echo(FeedbackManager.error_exception(error=str(e)))
323
318
 
324
319
  def reprint_prompt(self):
325
320
  click.echo(f"{bcolors.OKGREEN}{self.prompt_message}{bcolors.ENDC}", nl=False)