tinybird 0.0.1.dev30__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.

tinybird/__cli__.py CHANGED
@@ -1,6 +1,6 @@
1
- __name__ = "tinybird-cli"
1
+ __name__ = "tinybird_cli"
2
2
  __description__ = "Tinybird Command Line Tool"
3
- __url__ = "https://www.tinybird.co/docs/cli/introduction.html"
3
+ __url__ = "https://www.tinybird.co/docs/cli"
4
4
  __author__ = "Tinybird"
5
5
  __author_email__ = "support@tinybird.co"
6
6
  __version__ = "x.y.z"
tinybird/config.py CHANGED
@@ -29,6 +29,7 @@ LEGACY_HOSTS = {
29
29
  "https://api.us-east.aws.tinybird.co": "https://app.tinybird.co/aws/us-east-1",
30
30
  "https://api.us-west-2.aws.tinybird.co": "https://app.tinybird.co/aws/us-west-2",
31
31
  "https://api.eu-central-1.aws.tinybird.co": "https://app.tinybird.co/aws/eu-central-1",
32
+ "https://api.eu-west-1.aws.tinybird.co": "https://app.tinybird.co/aws/eu-west-1",
32
33
  "https://api.ap-east.aws.tinybird.co": "https://app.tinybird.co/aws/ap-east",
33
34
  "https://api.wadus1.gcp.tinybird.co": "https://app.wadus.tinybird.co/gcp/wadus1",
34
35
  "https://api.wadus2.gcp.tinybird.co": "https://app.wadus.tinybird.co/gcp/wadus2",
tinybird/context.py CHANGED
@@ -21,3 +21,4 @@ wait_parameter: ContextVar[bool] = ContextVar("wait_parameter")
21
21
  api_host: ContextVar[str] = ContextVar("api_host")
22
22
  ff_split_to_array_escape: ContextVar[bool] = ContextVar("ff_split_to_array_escape")
23
23
  ff_preprocess_parameters_circuit_breaker: ContextVar[bool] = ContextVar("ff_preprocess_parameters_circuit_breaker")
24
+ ff_column_json_backticks_circuit_breaker: ContextVar[bool] = ContextVar("ff_column_json_backticks_circuit_breaker")
tinybird/sql_template.py CHANGED
@@ -12,7 +12,11 @@ from typing import Any, Dict, List, Optional, Tuple, Union
12
12
  from tornado import escape
13
13
  from tornado.util import ObjectDict, exec_in, unicode_type
14
14
 
15
- from tinybird.context import ff_preprocess_parameters_circuit_breaker, ff_split_to_array_escape
15
+ from tinybird.context import (
16
+ ff_column_json_backticks_circuit_breaker,
17
+ ff_preprocess_parameters_circuit_breaker,
18
+ ff_split_to_array_escape,
19
+ )
16
20
 
17
21
  from .datatypes import testers
18
22
  from .tornado_template import VALID_CUSTOM_FUNCTION_NAMES, SecurityException, Template
@@ -271,6 +275,8 @@ def columns(x, default=None, fn=None):
271
275
 
272
276
 
273
277
  def column(x, default=None):
278
+ bypass_colunn_json_backticks = ff_column_json_backticks_circuit_breaker.get(False)
279
+
274
280
  if x is None or isinstance(x, Placeholder):
275
281
  if default is None:
276
282
  raise SQLTemplateException(
@@ -278,7 +284,18 @@ def column(x, default=None):
278
284
  documentation="/cli/advanced-templates.html#column",
279
285
  )
280
286
  x = default
281
- return Symbol("`" + sqlescape(x) + "`")
287
+
288
+ quote = "`"
289
+ if bypass_colunn_json_backticks:
290
+ return Symbol(quote + sqlescape(x) + quote)
291
+
292
+ try:
293
+ slices = x.split(".")
294
+ escaped_slices = [quote + sqlescape(s) + quote for s in slices]
295
+ escaped = ".".join(escaped_slices)
296
+ return Symbol(escaped)
297
+ except Exception: # in case there's a problem with .split
298
+ return Symbol(quote + sqlescape(x) + quote)
282
299
 
283
300
 
284
301
  def symbol(x, quote="`"):
@@ -2109,6 +2126,8 @@ def render_sql_template(
2109
2126
  ('SELECT `foo`', {}, [])
2110
2127
  >>> render_sql_template("SELECT {{column(agg)}}", {'agg': '"foo"'})
2111
2128
  ('SELECT `foo`', {}, [])
2129
+ >>> render_sql_template("SELECT {{column(agg)}}", {'agg': 'json.a'})
2130
+ ('SELECT `json`.`a`', {}, [])
2112
2131
  >>> render_sql_template('{% if not defined(test) %}error("This is an error"){% end %}', {})
2113
2132
  ('error("This is an error")', {}, [])
2114
2133
  >>> render_sql_template('{% if not defined(test) %}custom_error({error: "This is an error"}){% end %}', {})
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.dev30'
8
- __revision__ = 'f7bc1dc'
7
+ __version__ = '0.0.1.dev31'
8
+ __revision__ = '0e056e8'
@@ -2,18 +2,49 @@ import asyncio
2
2
  import glob
3
3
  import json
4
4
  import logging
5
+ import threading
5
6
  from pathlib import Path
6
7
  from typing import List
7
8
 
8
9
  import click
9
10
  import requests
10
11
 
12
+ from tinybird.client import TinyB
11
13
  from tinybird.tb.modules.cli import cli
12
14
  from tinybird.tb.modules.feedback_manager import FeedbackManager
13
15
  from tinybird.tb.modules.local_common import get_tinybird_local_client
16
+ from tinybird.tb.modules.shell import Shell
17
+ from tinybird.tb.modules.watch import watch_project
14
18
 
15
19
 
16
- def project_files(project_path: Path) -> List[str]:
20
+ @cli.command()
21
+ @click.option("--folder", type=str, default=".")
22
+ @click.option("--watch", is_flag=True, default=False, help="Watch for changes and rebuild automatically")
23
+ def build(folder: str, watch: bool) -> None:
24
+ """
25
+ Validate and build the project server side.
26
+ """
27
+
28
+ tb_client = asyncio.run(get_tinybird_local_client(folder))
29
+
30
+ def process() -> None:
31
+ build_project(folder, tb_client)
32
+
33
+ process()
34
+
35
+ if watch:
36
+ shell = Shell(folder=folder, client=tb_client)
37
+ click.echo(FeedbackManager.gray(message="\nWatching for changes..."))
38
+ watcher_thread = threading.Thread(
39
+ target=watch_project,
40
+ args=(shell, process, folder),
41
+ daemon=True,
42
+ )
43
+ watcher_thread.start()
44
+ shell.run()
45
+
46
+
47
+ def get_project_files(project_path: Path) -> List[str]:
17
48
  project_file_extensions = ("datasource", "pipe")
18
49
  project_files = []
19
50
  for extension in project_file_extensions:
@@ -23,38 +54,32 @@ def project_files(project_path: Path) -> List[str]:
23
54
  return project_files
24
55
 
25
56
 
26
- @cli.command()
27
- @click.argument("project_path", type=click.Path(exists=True), default=Path.cwd())
28
- def build(project_path: Path) -> None:
29
- """
30
- Validate and build the project server side.
31
- """
57
+ def build_project(folder: str, tb_client: TinyB) -> None:
32
58
  MULTIPART_BOUNDARY_DATA_PROJECT = "data_project://"
33
59
  DATAFILE_TYPE_TO_CONTENT_TYPE = {
34
60
  ".datasource": "text/plain",
35
61
  ".pipe": "text/plain",
36
62
  }
37
-
38
- tb_client = asyncio.run(get_tinybird_local_client(str(project_path)))
39
63
  TINYBIRD_API_URL = tb_client.host + "/v1/build"
40
64
  TINYBIRD_API_KEY = tb_client.token
41
-
42
- files = [
43
- ("context://", ("cli-version", "1.0.0", "text/plain")),
44
- ]
45
- fds = []
46
- for file_path in project_files(project_path):
47
- relative_path = str(Path(file_path).relative_to(project_path))
48
- fd = open(file_path, "rb")
49
- fds.append(fd)
50
- content_type = DATAFILE_TYPE_TO_CONTENT_TYPE.get(Path(file_path).suffix, "application/unknown")
51
- files.append((MULTIPART_BOUNDARY_DATA_PROJECT, (relative_path, fd.read().decode("utf-8"), content_type)))
52
-
53
65
  try:
66
+ files = [
67
+ ("context://", ("cli-version", "1.0.0", "text/plain")),
68
+ ]
69
+ fds = []
70
+ project_path = Path(folder)
71
+ project_files = get_project_files(project_path)
72
+ for file_path in project_files:
73
+ relative_path = str(Path(file_path).relative_to(project_path))
74
+ fd = open(file_path, "rb")
75
+ fds.append(fd)
76
+ content_type = DATAFILE_TYPE_TO_CONTENT_TYPE.get(Path(file_path).suffix, "application/unknown")
77
+ files.append((MULTIPART_BOUNDARY_DATA_PROJECT, (relative_path, fd.read().decode("utf-8"), content_type)))
54
78
  HEADERS = {"Authorization": f"Bearer {TINYBIRD_API_KEY}"}
55
79
 
56
80
  r = requests.post(TINYBIRD_API_URL, files=files, headers=HEADERS)
57
81
  result = r.json()
82
+
58
83
  logging.debug(json.dumps(result, indent=2))
59
84
 
60
85
  build_result = result.get("result")
@@ -69,6 +94,8 @@ def build(project_path: Path) -> None:
69
94
  click.echo(FeedbackManager.error(message=error_msg))
70
95
  else:
71
96
  click.echo(FeedbackManager.error(message=f"Unknown build result. Error: {result.get('error')}"))
97
+ except Exception as e:
98
+ click.echo(FeedbackManager.error_exception(error="Error building project: " + str(e)))
72
99
  finally:
73
100
  for fd in fds:
74
101
  fd.close()
@@ -210,7 +210,8 @@ async def cli(
210
210
 
211
211
  logging.debug("debug enabled")
212
212
 
213
- client = await create_ctx_client(config, prod)
213
+ skip_client = ctx.invoked_subcommand in ["login", "workspace"]
214
+ client = await create_ctx_client(config, prod, skip_client)
214
215
 
215
216
  if client:
216
217
  ctx.ensure_object(dict)["client"] = client
@@ -529,7 +530,7 @@ async def ask(prompt: str, folder: str) -> None:
529
530
 
530
531
  client = config.get_client()
531
532
  llm = LLM(user_token=user_token, client=client)
532
- click.echo(await llm.ask(prompt, system_prompt=ask_prompt(resources_xml)))
533
+ click.echo(await llm.ask(system_prompt=ask_prompt(resources_xml), prompt=prompt))
533
534
  except Exception as e:
534
535
  raise CLIException(FeedbackManager.error_exception(error=e))
535
536
 
@@ -571,11 +572,11 @@ def __unpatch_click_output():
571
572
  click.secho = __old_click_secho
572
573
 
573
574
 
574
- async def create_ctx_client(config: Dict[str, Any], prod: bool):
575
+ async def create_ctx_client(config: Dict[str, Any], prod: bool, skip_client: bool):
576
+ if skip_client:
577
+ return None
578
+
575
579
  if prod:
576
580
  return _get_tb_client(config.get("token", None), config["host"])
577
581
 
578
- try:
579
- return await get_tinybird_local_client()
580
- except Exception:
581
- return None
582
+ return await get_tinybird_local_client()
@@ -109,7 +109,7 @@ async def create(
109
109
  has_json_path = "`json:" in datasource_content
110
110
  if has_json_path:
111
111
  prompt = f"<datasource_schema>{datasource_content}</datasource_schema>\n<user_input>{prompt}</user_input>"
112
- response = await llm.ask(prompt, system_prompt=mock_prompt(rows))
112
+ response = await llm.ask(system_prompt=mock_prompt(rows), prompt=prompt)
113
113
  sql = extract_xml(response, "sql")
114
114
  sql = sql.split("FORMAT")[0]
115
115
  result = await local_client.query(f"{sql} FORMAT JSON")
@@ -206,7 +206,7 @@ TYPE ENDPOINT
206
206
  ]
207
207
  )
208
208
  llm = LLM(user_token=user_token, client=tb_client)
209
- result = await llm.ask(prompt, system_prompt=create_prompt(resources_xml))
209
+ result = await llm.ask(system_prompt=create_prompt(resources_xml), prompt=prompt)
210
210
  result = extract_xml(result, "response")
211
211
  resources = parse_xml(result, "resource")
212
212
  datasources = []
@@ -1,35 +1,9 @@
1
- import json
2
- import os
3
- import urllib.parse
4
1
  from copy import deepcopy
5
- from typing import Any, List
6
-
7
- from anthropic import AnthropicVertex
8
- from pydantic import BaseModel
2
+ from typing import Optional
9
3
 
10
4
  from tinybird.client import TinyB
11
5
 
12
6
 
13
- class DataFile(BaseModel):
14
- name: str
15
- content: str
16
-
17
-
18
- class DataProject(BaseModel):
19
- datasources: List[DataFile]
20
- pipes: List[DataFile]
21
-
22
-
23
- class TestExpectation(BaseModel):
24
- name: str
25
- description: str
26
- parameters: str
27
-
28
-
29
- class TestExpectations(BaseModel):
30
- tests: List[TestExpectation]
31
-
32
-
33
7
  class LLM:
34
8
  def __init__(
35
9
  self,
@@ -39,82 +13,26 @@ class LLM:
39
13
  self.user_client = deepcopy(client)
40
14
  self.user_client.token = user_token
41
15
 
42
- async def ask(self, prompt: str, system_prompt: str = "") -> str:
16
+ async def ask(self, system_prompt: str, prompt: Optional[str] = None) -> str:
43
17
  """
44
18
  Calls the model with the given prompt and returns the response.
45
19
 
46
20
  Args:
21
+ system_prompt (str): The system prompt to send to the model.
47
22
  prompt (str): The user prompt to send to the model.
48
23
 
49
24
  Returns:
50
25
  str: The response from the language model.
51
26
  """
52
- messages: List[Any] = []
53
27
 
54
- if system_prompt:
55
- messages.append({"role": "user", "content": system_prompt})
28
+ data = {"system": system_prompt}
56
29
 
57
30
  if prompt:
58
- messages.append({"role": "user", "content": prompt})
59
-
60
- if gcloud_access_token := os.getenv("GCLOUD_ACCESS_TOKEN"):
61
- client = AnthropicVertex(
62
- region="europe-west1",
63
- project_id="gen-lang-client-0705305160",
64
- access_token=gcloud_access_token,
65
- )
66
- message = client.messages.create(
67
- max_tokens=8000,
68
- messages=messages,
69
- model="claude-3-5-sonnet-v2@20241022",
70
- )
71
- return message.content[0].text or "" # type: ignore
31
+ data["prompt"] = prompt
72
32
 
73
- data = {
74
- "model": "o1-mini",
75
- "messages": messages,
76
- }
77
33
  response = await self.user_client._req(
78
34
  "/v0/llm",
79
35
  method="POST",
80
- data=json.dumps(data),
81
- headers={"Content-Type": "application/json"},
36
+ data=data,
82
37
  )
83
38
  return response.get("result", "")
84
-
85
- async def create_project(self, prompt: str) -> DataProject:
86
- try:
87
- prompt = (
88
- prompt
89
- + "\n#More extra context\n- If you add some array data type remember that the json path should be like this: `json:$.array_field[:]`"
90
- )
91
- response = await self.user_client._req(
92
- "/v0/llm/create",
93
- method="POST",
94
- data=f'{{"prompt": {json.dumps(prompt)}}}',
95
- headers={"Content-Type": "application/json"},
96
- )
97
-
98
- return DataProject.model_validate(response.get("result", {}))
99
- except Exception:
100
- return DataProject(datasources=[], pipes=[])
101
-
102
- async def generate_sql_sample_data(self, schema: str, rows: int = 20, prompt: str = "") -> str:
103
- response = await self.user_client._req(
104
- "/v0/llm/mock",
105
- method="POST",
106
- data=f'{{"schema": "{urllib.parse.quote(schema)}", "rows": {rows}, "context": "{prompt}"}}',
107
- headers={"Content-Type": "application/json"},
108
- )
109
- result = response.get("result", "")
110
- return result.replace("elementAt", "arrayElement")
111
-
112
- async def create_tests(self, pipe_content: str, pipe_params: set[str], prompt: str = "") -> TestExpectations:
113
- response = await self.user_client._req(
114
- "/v0/llm/create/tests",
115
- method="POST",
116
- data=json.dumps({"pipe_content": pipe_content, "pipe_params": list(pipe_params), "prompt": prompt}),
117
- headers={"Content-Type": "application/json"},
118
- )
119
- result = response.get("result", "")
120
- return TestExpectations.model_validate(result)
@@ -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}"
@@ -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,
@@ -283,7 +278,7 @@ 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
284
  subprocess.run(f"tb {arg}", shell=True, text=True)
@@ -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)
@@ -111,7 +111,7 @@ async def test_create(name_or_filename: str, prompt: str, folder: str) -> None:
111
111
  raise CLIException(FeedbackManager.error(message="No user token found"))
112
112
  llm = LLM(user_token=user_token, client=config.get_client())
113
113
 
114
- response_llm = await llm.ask(prompt, system_prompt=system_prompt)
114
+ response_llm = await llm.ask(system_prompt=system_prompt, prompt=prompt)
115
115
  response_xml = extract_xml(response_llm, "response")
116
116
  tests_content = parse_xml(response_xml, "test")
117
117
 
@@ -71,7 +71,7 @@ async def update(
71
71
  has_json_path = "`json:" in datasource_content
72
72
  if has_json_path:
73
73
  prompt = f"<datasource_schema>{datasource_content}</datasource_schema>\n<user_input>{prompt}</user_input>"
74
- response = await llm.ask(prompt, system_prompt=mock_prompt(rows=20))
74
+ response = await llm.ask(system_prompt=mock_prompt(rows=20), prompt=prompt)
75
75
  sql = extract_xml(response, "sql")
76
76
  sql = sql.split("FORMAT")[0]
77
77
  result = await local_client.query(f"{sql} FORMAT JSON")
@@ -117,7 +117,7 @@ async def update_resources(
117
117
  ]
118
118
  )
119
119
  llm = LLM(user_token=user_token, client=tb_client)
120
- result = await llm.ask(prompt, system_prompt=update_prompt(resources_xml))
120
+ result = await llm.ask(system_prompt=update_prompt(resources_xml), prompt=prompt)
121
121
  result = extract_xml(result, "response")
122
122
  resources = parse_xml(result, "resource")
123
123
  datasources = []
@@ -1,10 +1,18 @@
1
1
  import asyncio
2
2
  import os
3
3
  import time
4
+ from pathlib import Path
4
5
  from typing import Any, Callable, List, Optional, Union
5
6
 
6
7
  import click
7
- from watchdog.events import DirMovedEvent, FileMovedEvent, FileSystemEventHandler
8
+ from watchdog.events import (
9
+ DirDeletedEvent,
10
+ DirMovedEvent,
11
+ FileDeletedEvent,
12
+ FileMovedEvent,
13
+ FileSystemEventHandler,
14
+ PatternMatchingEventHandler,
15
+ )
8
16
  from watchdog.observers import Observer
9
17
 
10
18
  from tinybird.tb.modules.feedback_manager import FeedbackManager
@@ -110,3 +118,70 @@ def watch_files(
110
118
  observer.stop()
111
119
 
112
120
  observer.join()
121
+
122
+
123
+ class WatchProjectHandler(PatternMatchingEventHandler):
124
+ def __init__(self, shell: Shell, folder: str, process: Callable[[List[str]], None]):
125
+ self.shell = shell
126
+ self.process = process
127
+ super().__init__(
128
+ patterns=[f"{folder}/**/*.datasource", f"{folder}/**/*.pipe"],
129
+ ignore_patterns=[f"{folder}/vendor/"],
130
+ )
131
+
132
+ def should_process(self, event: Any) -> Optional[str]:
133
+ if event.is_directory:
134
+ return None
135
+
136
+ if os.path.exists(event.src_path):
137
+ return event.src_path
138
+
139
+ if os.path.exists(event.dest_path):
140
+ return event.dest_path
141
+
142
+ return event.src_path
143
+
144
+ def _process(self) -> None:
145
+ click.echo(FeedbackManager.highlight(message="» Rebuilding..."))
146
+ time_start = time.time()
147
+ self.process()
148
+ time_end = time.time()
149
+ elapsed_time = time_end - time_start
150
+ click.echo(
151
+ FeedbackManager.success(message="\n✓ ")
152
+ + FeedbackManager.gray(message=f"Rebuild completed in {elapsed_time:.1f}s")
153
+ )
154
+ self.shell.reprint_prompt()
155
+
156
+ def on_modified(self, event: Any) -> None:
157
+ if path := self.should_process(event):
158
+ filename = Path(path).name
159
+ click.echo(FeedbackManager.highlight(message=f"\n\n⟲ Changes detected in {filename}\n"))
160
+ self._process()
161
+
162
+ def on_deleted(self, event: Union[DirDeletedEvent, FileDeletedEvent]) -> None:
163
+ filename = Path(event.src_path).name
164
+ if event.is_directory:
165
+ click.echo(FeedbackManager.highlight(message=f"\n\n⟲ Deleted directory: {filename}\n"))
166
+ else:
167
+ click.echo(FeedbackManager.highlight(message=f"\n\n⟲ Deleted file: {filename}\n"))
168
+ self._process()
169
+
170
+
171
+ def watch_project(
172
+ shell: Shell,
173
+ process: Callable,
174
+ folder: str,
175
+ ) -> None:
176
+ event_handler = WatchProjectHandler(shell=shell, folder=folder, process=process)
177
+ observer = Observer()
178
+ observer.schedule(event_handler, path=folder, recursive=True)
179
+ observer.start()
180
+
181
+ try:
182
+ while True:
183
+ time.sleep(1)
184
+ except KeyboardInterrupt:
185
+ observer.stop()
186
+
187
+ observer.join()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tinybird
3
- Version: 0.0.1.dev30
3
+ Version: 0.0.1.dev31
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -1,53 +1,53 @@
1
- tinybird/__cli__.py,sha256=pgYsVLcqL16wtSn6KtKweNZYoYJdEksTgSvQAW7hH64,250
1
+ tinybird/__cli__.py,sha256=esPl5QDTzuQgHe5FuxWLm-fURFigGGwjnYLh9GuWUw4,232
2
2
  tinybird/client.py,sha256=P-bc7s7SbrnzkZkSsb_TO58q4Jxy-bBD5S5qpYiXiZg,51517
3
- tinybird/config.py,sha256=RRNb3Z1zkykTpusBTSyszuIySHGMsfL5aVxxt1kdr6w,6147
3
+ tinybird/config.py,sha256=ENRNyEMXHj_P882o31iFz0hTveziLabVRrxiWE5RRBE,6233
4
4
  tinybird/connectors.py,sha256=lkpVSUmSuViEZBa4QjTK7YmPHUop0a5UFoTrSmlVq6k,15244
5
- tinybird/context.py,sha256=kutUQ0kCwparowI74_YLXx6wtTzGLRouJ6oGHVBPzBo,1291
5
+ tinybird/context.py,sha256=eeFneyn_oTUoTIfst59CnSkskchvnWuRvyI7UKRFq14,1407
6
6
  tinybird/datatypes.py,sha256=XNypumfqNjsvLJ5iNXnbVHRvAJe0aQwI3lS6Cxox-e0,10979
7
7
  tinybird/feedback_manager.py,sha256=cNUbt0Jxim02UiIdlyP12DJfXfFFzxDCfJK9XRWZ9A0,67488
8
8
  tinybird/git_settings.py,sha256=Sw_8rGmribEFJ4Z_6idrVytxpFYk7ez8ei0qHULzs3E,3934
9
9
  tinybird/prompts.py,sha256=xrZMZykphO9DRfW5yvCJNZMhPq0v1zab3d9jKy9plhU,24064
10
10
  tinybird/sql.py,sha256=eulpRe05ZFrKFrxYawgxDxxrktFE8uL6hSL1gHIWKyg,46166
11
- tinybird/sql_template.py,sha256=IqYRfUxDYBCoOYjqqvn--_8QXLv9FSRnJ0bInx7q1Xs,93051
11
+ tinybird/sql_template.py,sha256=GmMLAI10MTqjQo9qztuQHLRWs67teozsWDxUBdvkAn4,93668
12
12
  tinybird/sql_template_fmt.py,sha256=1z-PuqSZXtzso8Z_mPqUc-NxIxUrNUcVIPezNieZk-M,10196
13
13
  tinybird/sql_toolset.py,sha256=HAo6xtCjlbhikdpK6xfMC_RvEG6uzRropwhKSHEkIIc,14343
14
14
  tinybird/syncasync.py,sha256=IPnOx6lMbf9SNddN1eBtssg8vCLHMt76SuZ6YNYm-Yk,27761
15
15
  tinybird/tornado_template.py,sha256=oflXyoL2LSCegvl6bAzqw2JIqRaN5WPjhYYDtQcfuOE,41869
16
16
  tinybird/ch_utils/constants.py,sha256=aYvg2C_WxYWsnqPdZB1ZFoIr8ZY-XjUXYyHKE9Ansj0,3890
17
17
  tinybird/ch_utils/engine.py,sha256=OXkBhlzGjZotjD0vaT-rFIbSGV4tpiHxE8qO_ip0SyQ,40454
18
- tinybird/tb/__cli__.py,sha256=6hEdXKgx0nKv06KQFvgIz7SfdCIFDW1PO1n92A31kH4,251
18
+ tinybird/tb/__cli__.py,sha256=SgyAWbfPDLAaYyoq9rFOPpcXyLPQqCPh2m8Bt_6lxaI,251
19
19
  tinybird/tb/cli.py,sha256=dmrf8O9cqBuOYUmVBbNDSNQpHUhBg6IkBPaISAj4kHo,923
20
20
  tinybird/tb/modules/auth.py,sha256=EzRWFmwRkXNhUmRaruEVFLdkbUg8xMSix0cAWl5D4Jg,9029
21
- tinybird/tb/modules/build.py,sha256=RRDOd887ym79BWqmLUQxwnjPfG4c4kEGEF2dUP255us,2769
21
+ tinybird/tb/modules/build.py,sha256=J_YCaJIkvoM1CMpsYQ4gM1LinaqJmJmvfcmhxEmWq_g,3725
22
22
  tinybird/tb/modules/build_client.py,sha256=SoGaGPBYT_0BoRp2lxNyBBvQQ1GFAMpx_o62jb91DUI,8046
23
23
  tinybird/tb/modules/cicd.py,sha256=SjCyvvy0WUnsjFs2biwwXvcf0Ddpmghhd8-SnMyfsRM,5355
24
- tinybird/tb/modules/cli.py,sha256=qe1JwNqDVxKdk_vyZ3FWG5wRunGfFdgwQ0NSxUJDYC4,22032
24
+ tinybird/tb/modules/cli.py,sha256=i5KQF1kJqd3lflYl0e3tnKrMZBIAOlMF1T5pxa6e7tM,22124
25
25
  tinybird/tb/modules/common.py,sha256=e4U7AT0dUBG6O-7Iq2CVN1UHPd6-ZCFucyW0L5gBi4g,70592
26
26
  tinybird/tb/modules/config.py,sha256=mie3oMVTf5YOUFEiLs88P16U4LkJafJjSpjwyAkFHog,10979
27
27
  tinybird/tb/modules/connection.py,sha256=FhDM-OAnLN2epbO2YonpjJQhHqBjyuanBsZmKlDXrqg,28679
28
- tinybird/tb/modules/create.py,sha256=Jmx89lgk1jk3rSx2jNzn5Xvr-L8oTetEvr1sEl76Lvw,10846
28
+ tinybird/tb/modules/create.py,sha256=HclKekBKTaobVQeYq9ooSmuBwlrebi-YsFOFJJMGTZs,10860
29
29
  tinybird/tb/modules/datasource.py,sha256=PmIoDSLqBNHQvguX9t1T3z9lsG3HB67cmwhQO5_8Lc4,32918
30
30
  tinybird/tb/modules/deploy.py,sha256=j1MuiGsWplG1BQs8eG6-STcYyzvHqVESPCOo6P-dcF8,9242
31
31
  tinybird/tb/modules/exceptions.py,sha256=4A2sSjCEqKUMqpP3WI00zouCWW4uLaghXXLZBSw04mY,3363
32
32
  tinybird/tb/modules/feedback_manager.py,sha256=e8tqehRR0Buhs8O0n8N2Sg2vnnBVb1NLtnZqkPrYD_A,68379
33
33
  tinybird/tb/modules/fmt.py,sha256=poh6_cwVGSf-sBu6LKWuO2TANL_J8Sgm25sPpwxa3Aw,3558
34
34
  tinybird/tb/modules/job.py,sha256=956Pj8BEEsiD2GZsV9RKKVM3I_CveOLgS82lykO5ukk,2963
35
- tinybird/tb/modules/llm.py,sha256=C6piIQARMnuXA1OmZgngp6f5ehe2qHBunzemRgRQ4KI,3774
35
+ tinybird/tb/modules/llm.py,sha256=mguBJaTJ74a5mQXcBQwbCjKugfvcFm_Wzg2e_hSkooQ,953
36
36
  tinybird/tb/modules/llm_utils.py,sha256=hIfBU7vMUHUt25pljim3WdZdJTNr9hDo3mHezqI5B7w,766
37
37
  tinybird/tb/modules/local.py,sha256=bSBBk2A0yj4fQHxsdzbZAH1Q_xrqT7vFOIPoz7TNabo,5260
38
- tinybird/tb/modules/local_common.py,sha256=fRHJrEix19lAOE4EpzRhWBJH8FUk7hCCvynP0Icf7Ww,2298
38
+ tinybird/tb/modules/local_common.py,sha256=Na4zHQP2H9oE840KYUn69_f0PcqExQtaKFikaiNf43s,2296
39
39
  tinybird/tb/modules/login.py,sha256=0cS-f3MsQFHc6xjw8FRWJm4EJBH9C7Ri68EcO_tiwes,6508
40
- tinybird/tb/modules/mock.py,sha256=b0MqSVvJsaXdUKvagXfPGrh-XB8cYmuyXJ2D4P7uyB0,3658
40
+ tinybird/tb/modules/mock.py,sha256=eSfD-m4w5NLGxjfGRlYplQhC0FKpanysIweG_8GsbNE,3665
41
41
  tinybird/tb/modules/pipe.py,sha256=eYmMBiSj1Ur_hXUs74YZ9mCSAyiICDmkuKuTemlxPUY,27018
42
42
  tinybird/tb/modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
43
- tinybird/tb/modules/shell.py,sha256=Ttb7DHjQoQJKLnVS2VObiIn5UwHOvtPZR2yDtvBF1Cc,13453
43
+ tinybird/tb/modules/shell.py,sha256=84WiRppf4bLZSSEscSqFov-7RpDEQ8u0DFovqlnXauk,13093
44
44
  tinybird/tb/modules/table.py,sha256=4XrtjM-N0zfNtxVkbvLDQQazno1EPXnxTyo7llivfXk,11035
45
45
  tinybird/tb/modules/tag.py,sha256=anPmMUBc-TbFovlpFi8GPkKA18y7Y0GczMsMms5TZsU,3502
46
46
  tinybird/tb/modules/telemetry.py,sha256=iEGnMuCuNhvF6ln__j6X9MSTwL_0Hm-GgFHHHvhfknk,10466
47
- tinybird/tb/modules/test.py,sha256=AQMhVduVg1Oo98Ycla_-49G9nuPxedGwxWyMmkwa0h8,11570
47
+ tinybird/tb/modules/test.py,sha256=2UhRiMRPP2vEsa2ayM_ujLc1cB8gHkKHCLJ50GbkQxc,11577
48
48
  tinybird/tb/modules/token.py,sha256=AePr-QMv_vtWwZDWQ92Zp0kPrCjze61i4npiPhoLMZg,12717
49
- tinybird/tb/modules/update.py,sha256=tQVnFG2yvEDHAwHHkJn_hCw06VBbOgMRJs226_uQjnQ,6813
50
- tinybird/tb/modules/watch.py,sha256=9D6NTo9cllyJfvObtOzZEbv3SJN42EgfVdYQPN1liZU,3964
49
+ tinybird/tb/modules/update.py,sha256=wXzLhBzKD1kWEN1HejOVkKpFQqomZGfP1AiCvE-K04A,6827
50
+ tinybird/tb/modules/watch.py,sha256=na1H0lVCyMTCgzm2h8FHyKWhWN5poAehWgJeXT2rLXg,6281
51
51
  tinybird/tb/modules/workspace.py,sha256=6icAgnTvfL3d1kx4L1Z1cGXCD_2Yx0fNRjbZHNxRbYc,10927
52
52
  tinybird/tb/modules/workspace_members.py,sha256=Ai6iCOzXX1zQ8q9iXIFSFHsBJlT-8Q28DaG5Ie-UweY,8726
53
53
  tinybird/tb/modules/datafile/build.py,sha256=pwgsIuvHwb2cdsl3IWOAPyj6S9vB3jn_BXGRcKT7I2Y,57577
@@ -73,8 +73,8 @@ tinybird/tb_cli_modules/config.py,sha256=6u6B5QCdiQLbJkCkwtnKGs9H3nP-KXXhC75mF7B
73
73
  tinybird/tb_cli_modules/exceptions.py,sha256=pmucP4kTF4irIt7dXiG-FcnI-o3mvDusPmch1L8RCWk,3367
74
74
  tinybird/tb_cli_modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
75
75
  tinybird/tb_cli_modules/telemetry.py,sha256=iEGnMuCuNhvF6ln__j6X9MSTwL_0Hm-GgFHHHvhfknk,10466
76
- tinybird-0.0.1.dev30.dist-info/METADATA,sha256=_ggPTXiep4YjUELu26U3P1pKAz2ieqGakQtVM-NlRhE,2482
77
- tinybird-0.0.1.dev30.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
78
- tinybird-0.0.1.dev30.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
79
- tinybird-0.0.1.dev30.dist-info/top_level.txt,sha256=pgw6AzERHBcW3YTi2PW4arjxLkulk2msOz_SomfOEuc,45
80
- tinybird-0.0.1.dev30.dist-info/RECORD,,
76
+ tinybird-0.0.1.dev31.dist-info/METADATA,sha256=nl0L_9CisXRnrHeEukw_5Z733F7vlRvHMVgHPdVrcL0,2482
77
+ tinybird-0.0.1.dev31.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
78
+ tinybird-0.0.1.dev31.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
79
+ tinybird-0.0.1.dev31.dist-info/top_level.txt,sha256=pgw6AzERHBcW3YTi2PW4arjxLkulk2msOz_SomfOEuc,45
80
+ tinybird-0.0.1.dev31.dist-info/RECORD,,