tinybird 0.0.1.dev48__py3-none-any.whl → 0.0.1.dev49__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/prompts.py CHANGED
@@ -651,32 +651,6 @@ DATASOURCE sales_by_hour
651
651
  """
652
652
 
653
653
 
654
- def ask_prompt(existing_resources: str) -> str:
655
- return """
656
- You are a Tinybird expert. You will be given a prompt to ask questions about Tinybird resources.
657
- <existing_resources>{existing_resources}</existing_resources>
658
- {datasource_instructions}
659
- {pipe_instructions}
660
- {sql_instructions}
661
- {datasource_example}
662
- {pipe_example}
663
- {copy_pipe_instructions}
664
- {materialized_pipe_instructions}
665
-
666
- The previous instructions are explanations of how things work in Tinybird. Answer in natural language.
667
-
668
- """.format(
669
- existing_resources=existing_resources,
670
- datasource_instructions=datasource_instructions,
671
- datasource_example=datasource_example,
672
- pipe_instructions=pipe_instructions,
673
- pipe_example=pipe_example,
674
- sql_instructions=sql_instructions,
675
- copy_pipe_instructions=copy_pipe_instructions,
676
- materialized_pipe_instructions=materialized_pipe_instructions,
677
- )
678
-
679
-
680
654
  datasource_instructions = """
681
655
  <datasource_file_instructions>
682
656
  - Content cannot be empty.
@@ -790,39 +764,6 @@ sql_instructions = """
790
764
  )
791
765
 
792
766
 
793
- def update_prompt(existing_resources: str) -> str:
794
- return """
795
- You are a Tinybird expert. You will be given a prompt to update the existing Tinybird resources: datasources and/or pipes.
796
- You will return the resources that need to be updated.
797
- <existing_resources>{existing_resources}</existing_resources>
798
- {datasource_instructions}
799
- {pipe_instructions}
800
- {sql_instructions}
801
- {datasource_example}
802
- {pipe_example}
803
- {copy_pipe_instructions}
804
- {materialized_pipe_instructions}
805
- Use the following format to generate the response and do not wrap it in any other text, including the <response> tag.
806
- <response>
807
- <resource>
808
- <type>[datasource or pipe]</type>
809
- <name>[resource name here]</name>
810
- <content>[resource content here]</content>
811
- </resource>
812
- </response>
813
-
814
- """.format(
815
- existing_resources=existing_resources,
816
- datasource_instructions=datasource_instructions,
817
- pipe_instructions=pipe_instructions,
818
- sql_instructions=sql_instructions,
819
- datasource_example=datasource_example,
820
- pipe_example=pipe_example,
821
- copy_pipe_instructions=copy_pipe_instructions,
822
- materialized_pipe_instructions=materialized_pipe_instructions,
823
- )
824
-
825
-
826
767
  def rules_prompt(source: Optional[str] = None) -> str:
827
768
  base_command = source or "tb"
828
769
  return """
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.dev48'
8
- __revision__ = '6d638e9'
7
+ __version__ = '0.0.1.dev49'
8
+ __revision__ = 'a0a2378'
tinybird/tb/cli.py CHANGED
@@ -23,7 +23,6 @@ import tinybird.tb.modules.pipe
23
23
  import tinybird.tb.modules.tag
24
24
  import tinybird.tb.modules.test
25
25
  import tinybird.tb.modules.token
26
- import tinybird.tb.modules.update
27
26
  import tinybird.tb.modules.workspace
28
27
  import tinybird.tb.modules.workspace_members
29
28
 
@@ -1,6 +1,7 @@
1
1
  import asyncio
2
2
  import json
3
3
  import logging
4
+ import sys
4
5
  import threading
5
6
  import time
6
7
  from pathlib import Path
@@ -15,7 +16,6 @@ from tinybird.tb.modules.common import push_data
15
16
  from tinybird.tb.modules.datafile.build import folder_build
16
17
  from tinybird.tb.modules.datafile.fixture import build_fixture_name, get_fixture_dir
17
18
  from tinybird.tb.modules.feedback_manager import FeedbackManager
18
- from tinybird.tb.modules.local_common import get_tinybird_local_client
19
19
  from tinybird.tb.modules.project import Project
20
20
  from tinybird.tb.modules.shell import Shell, print_table_formatted
21
21
  from tinybird.tb.modules.watch import watch_project
@@ -29,7 +29,7 @@ def build(ctx: click.Context, watch: bool) -> None:
29
29
  Validate and build the project server side.
30
30
  """
31
31
  project: Project = ctx.ensure_object(dict)["project"]
32
- tb_client = asyncio.run(get_tinybird_local_client(str(project.path), build=True))
32
+ tb_client: TinyB = ctx.ensure_object(dict)["client"]
33
33
  click.echo(FeedbackManager.highlight_building_project())
34
34
  time_start = time.time()
35
35
 
@@ -40,15 +40,21 @@ def build(ctx: click.Context, watch: bool) -> None:
40
40
  build_project(project, tb_client, file_changed)
41
41
  try:
42
42
  if file_changed:
43
- asyncio.run(folder_build(project, filenames=[file_changed]))
44
- build_and_print_resource(tb_client, file_changed, diff)
43
+ asyncio.run(folder_build(project, tb_client, filenames=[file_changed]))
44
+ show_data(tb_client, file_changed, diff)
45
45
  except Exception:
46
46
  pass
47
47
 
48
- process()
49
- time_end = time.time()
50
- elapsed_time = time_end - time_start
51
- click.echo(FeedbackManager.success(message=f"\n✓ Build completed in {elapsed_time:.1f}s"))
48
+ try:
49
+ process()
50
+ except click.ClickException as e:
51
+ click.echo(e)
52
+ if not watch:
53
+ sys.exit(1)
54
+ else:
55
+ time_end = time.time()
56
+ elapsed_time = time_end - time_start
57
+ click.echo(FeedbackManager.success(message=f"\n✓ Build completed in {elapsed_time:.1f}s"))
52
58
 
53
59
  if watch:
54
60
  shell = Shell(project=project, tb_client=tb_client)
@@ -137,9 +143,16 @@ def build_project(project: Project, tb_client: TinyB, file_changed: Optional[str
137
143
 
138
144
  if fixture_path.exists():
139
145
  append_fixture(tb_client, ds_name, str(fixture_path))
146
+
140
147
  except Exception:
141
148
  pass
142
149
 
150
+ feedback = result.get("feedback", [])
151
+ for f in feedback:
152
+ click.echo(
153
+ FeedbackManager.warning(message=f"△ {f.get('level')}: {f.get('resource')}: {f.get('message')}")
154
+ )
155
+
143
156
  elif build_result == "failed":
144
157
  build_errors = result.get("errors")
145
158
  error = True
@@ -198,7 +211,7 @@ def rebuild_fixture(project: Project, tb_client: TinyB, fixture: str) -> None:
198
211
  click.echo(FeedbackManager.error_exception(error=e))
199
212
 
200
213
 
201
- def build_and_print_resource(tb_client: TinyB, filename: str, diff: Optional[str] = None):
214
+ def show_data(tb_client: TinyB, filename: str, diff: Optional[str] = None):
202
215
  table_name = diff
203
216
  resource_path = Path(filename)
204
217
  resource_name = resource_path.stem
@@ -7,7 +7,6 @@ import json
7
7
  import logging
8
8
  import os
9
9
  from os import getcwd
10
- from pathlib import Path
11
10
  from typing import Any, Callable, Dict, List, Optional, Tuple, Union
12
11
 
13
12
  import click
@@ -20,7 +19,6 @@ from tinybird.client import (
20
19
  TinyB,
21
20
  )
22
21
  from tinybird.config import get_config
23
- from tinybird.prompts import ask_prompt
24
22
  from tinybird.tb import __cli__
25
23
  from tinybird.tb.modules.common import (
26
24
  CatchAuthExceptions,
@@ -37,7 +35,6 @@ from tinybird.tb.modules.datafile.build import build_graph
37
35
  from tinybird.tb.modules.datafile.diff import diff_command
38
36
  from tinybird.tb.modules.datafile.pull import folder_pull
39
37
  from tinybird.tb.modules.feedback_manager import FeedbackManager
40
- from tinybird.tb.modules.llm import LLM
41
38
  from tinybird.tb.modules.local_common import get_tinybird_local_client
42
39
  from tinybird.tb.modules.project import Project
43
40
 
@@ -124,13 +121,9 @@ async def cli(
124
121
 
125
122
  logging.debug("debug enabled")
126
123
 
127
- skip_client = ctx.invoked_subcommand in ["auth", "check", "login", "local", "build"]
128
- client = await create_ctx_client(config, prod, build, skip_client, project)
124
+ client = await create_ctx_client(ctx, config, prod, build, project)
129
125
 
130
126
  if client:
131
- if not build:
132
- target = config.get("name", "production") if prod else "Tinybird local"
133
- click.echo(FeedbackManager.gray(message=f"Running against {target}\n"))
134
127
  ctx.ensure_object(dict)["client"] = client
135
128
 
136
129
  ctx.ensure_object(dict)["project"] = project
@@ -358,56 +351,6 @@ async def sql(
358
351
  click.echo(FeedbackManager.info_no_rows())
359
352
 
360
353
 
361
- @cli.command(hidden=True)
362
- @click.argument("prompt")
363
- @click.option("--folder", default=os.getcwd(), help="The folder to use for the project")
364
- @coro
365
- async def ask(prompt: str, folder: str) -> None:
366
- """Ask things about your data project."""
367
- try:
368
- config = CLIConfig.get_project_config(folder)
369
- user_token = config.get_user_token()
370
-
371
- if not user_token:
372
- raise CLIException("This action requires authentication. Run 'tb login' first.")
373
-
374
- datasource_paths = [
375
- Path(folder) / "datasources" / f
376
- for f in os.listdir(Path(folder) / "datasources")
377
- if f.endswith(".datasource")
378
- ]
379
- pipe_folders = ["endpoints", "pipes", "copies", "materializations", "sinks"]
380
- pipe_paths = [
381
- Path(folder) / pipe_folder / f
382
- for pipe_folder in pipe_folders
383
- if (Path(folder) / pipe_folder).exists()
384
- for f in os.listdir(Path(folder) / pipe_folder)
385
- if f.endswith(".pipe")
386
- ]
387
- resources_xml = "\n".join(
388
- [
389
- f"<resource><type>{resource_type}</type><name>{resource_name}</name><content>{resource_content}</content></resource>"
390
- for resource_type, resource_name, resource_content in [
391
- ("datasource", ds.stem, ds.read_text()) for ds in datasource_paths
392
- ]
393
- + [
394
- (
395
- "pipe",
396
- pipe.stem,
397
- pipe.read_text(),
398
- )
399
- for pipe in pipe_paths
400
- ]
401
- ]
402
- )
403
-
404
- client = config.get_client()
405
- llm = LLM(user_token=user_token, host=client.host)
406
- click.echo(llm.ask(system_prompt=ask_prompt(resources_xml), prompt=prompt))
407
- except Exception as e:
408
- raise CLIException(FeedbackManager.error_exception(error=e))
409
-
410
-
411
354
  def __patch_click_output():
412
355
  import re
413
356
 
@@ -445,11 +388,23 @@ def __unpatch_click_output():
445
388
  click.secho = __old_click_secho
446
389
 
447
390
 
448
- async def create_ctx_client(config: Dict[str, Any], prod: bool, build: bool, skip_client: bool, project: Project):
449
- if skip_client:
391
+ async def create_ctx_client(ctx: Context, config: Dict[str, Any], prod: bool, build: bool, project: Project):
392
+ commands_without_ctx_client = ["auth", "check", "login", "local"]
393
+ command = ctx.invoked_subcommand
394
+ if command in commands_without_ctx_client:
450
395
  return None
451
396
 
452
- if prod:
397
+ commands_always_prod = ["pull"]
398
+ commands_always_build = ["build", "test"]
399
+ commands_always_local = ["create", "mock"]
400
+ if (
401
+ (prod or command in commands_always_prod)
402
+ and command not in commands_always_build
403
+ and command not in commands_always_local
404
+ ):
405
+ click.echo(FeedbackManager.gray(message=f"Running against {config.get('name') or 'production'}"))
453
406
  return _get_tb_client(config.get("token", None), config["host"])
454
-
407
+ build = command in commands_always_build or build
408
+ if not build and command not in commands_always_local and command not in commands_always_build:
409
+ click.echo(FeedbackManager.gray(message="Running against Tinybird local\n"))
455
410
  return await get_tinybird_local_client(str(project.path), build=build)
@@ -17,7 +17,6 @@ from tinybird.tb.modules.exceptions import CLIException
17
17
  from tinybird.tb.modules.feedback_manager import FeedbackManager
18
18
  from tinybird.tb.modules.llm import LLM
19
19
  from tinybird.tb.modules.llm_utils import extract_xml, parse_xml
20
- from tinybird.tb.modules.local_common import get_tinybird_local_client
21
20
  from tinybird.tb.modules.project import Project
22
21
 
23
22
 
@@ -41,8 +40,8 @@ from tinybird.tb.modules.project import Project
41
40
  help="Folder where datafiles will be placed",
42
41
  )
43
42
  @click.option("--rows", type=int, default=10, help="Number of events to send")
44
- @click.option("--cursor", default=False, is_flag=True, help="Create .cursorrules file with Tinybird rules")
45
43
  @click.option("--source", type=str, default="tb", help="Source of the command")
44
+ @click.option("--skip", is_flag=True, default=False, help="Skip following up on the generated data")
46
45
  @click.pass_context
47
46
  @coro
48
47
  async def create(
@@ -51,11 +50,12 @@ async def create(
51
50
  prompt: Optional[str],
52
51
  folder: Optional[str],
53
52
  rows: int,
54
- cursor: bool,
55
53
  source: str,
54
+ skip: bool,
56
55
  ) -> None:
57
56
  """Initialize a new project."""
58
57
  project: Project = ctx.ensure_object(dict)["project"]
58
+ local_client: TinyB = ctx.ensure_object(dict)["client"]
59
59
  folder = folder or getcwd()
60
60
  folder_path = Path(folder)
61
61
  if not folder_path.exists():
@@ -78,7 +78,6 @@ async def create(
78
78
  )
79
79
  )
80
80
  return
81
- local_client = await get_tinybird_local_client(folder)
82
81
 
83
82
  if not validate_project_structure(folder):
84
83
  click.echo(FeedbackManager.highlight(message="\n» Creating new project structure..."))
@@ -88,7 +87,7 @@ async def create(
88
87
  result = ""
89
88
  if data or prompt:
90
89
  click.echo(FeedbackManager.highlight(message="\n» Creating resources..."))
91
- result = await create_resources(local_client, tb_client, user_token, data, prompt, folder)
90
+ result = await create_resources(local_client, tb_client, user_token, data, prompt, folder, skip)
92
91
  click.echo(FeedbackManager.success(message="✓ Done!\n"))
93
92
 
94
93
  if not already_has_cicd(folder):
@@ -97,6 +96,11 @@ async def create(
97
96
  await init_cicd(data_project_dir=os.path.relpath(folder))
98
97
  click.echo(FeedbackManager.success(message="✓ Done!\n"))
99
98
 
99
+ if not already_has_cursor_rules(folder):
100
+ click.echo(FeedbackManager.highlight(message="\n» Creating .cursorrules..."))
101
+ create_rules(folder, source, "cursor")
102
+ click.echo(FeedbackManager.success(message="✓ Done!\n"))
103
+
100
104
  if should_generate_fixtures(result):
101
105
  click.echo(FeedbackManager.highlight(message="\n» Generating fixtures..."))
102
106
 
@@ -130,12 +134,6 @@ async def create(
130
134
  if data:
131
135
  persist_fixture(fixture_name, data, folder)
132
136
  click.echo(FeedbackManager.info(message=f"✓ /fixtures/{datasource_name}"))
133
-
134
- if cursor:
135
- click.echo(FeedbackManager.highlight(message="\n» Creating .cursorrules..."))
136
- create_rules(folder, source, "cursor")
137
- click.echo(FeedbackManager.success(message="✓ Done!\n"))
138
-
139
137
  except Exception as e:
140
138
  click.echo(FeedbackManager.error(message=f"Error: {str(e)}"))
141
139
 
@@ -158,6 +156,11 @@ def already_has_cicd(folder: str) -> bool:
158
156
  return any((Path(folder) / path).exists() for path in ci_cd_paths)
159
157
 
160
158
 
159
+ def already_has_cursor_rules(folder: str) -> bool:
160
+ cursor_rules_paths = (".cursorrules", ".windsurfrules")
161
+ return any((Path(folder) / path).exists() for path in cursor_rules_paths)
162
+
163
+
161
164
  def create_project_structure(folder: str):
162
165
  folder_path = Path(folder)
163
166
  for x in PROJECT_PATHS:
@@ -176,6 +179,7 @@ async def create_resources(
176
179
  data: Optional[str],
177
180
  prompt: Optional[str],
178
181
  folder: str,
182
+ skip: bool,
179
183
  ):
180
184
  result = ""
181
185
  folder_path = Path(folder)
@@ -285,7 +289,8 @@ TYPE ENDPOINT
285
289
  content = pipe["content"].replace("```", "")
286
290
  pipe_path = generate_pipe_file(pipe["name"], content, folder)
287
291
  generated_paths.append(pipe_path)
288
-
292
+ if skip:
293
+ break
289
294
  iterations += 1
290
295
 
291
296
  if iterations == 10:
@@ -42,12 +42,12 @@ from tinybird.tb.modules.datafile.exceptions import AlreadyExistsException, Incl
42
42
  from tinybird.tb.modules.datafile.parse_datasource import parse_datasource
43
43
  from tinybird.tb.modules.datafile.parse_pipe import parse_pipe
44
44
  from tinybird.tb.modules.feedback_manager import FeedbackManager
45
- from tinybird.tb.modules.local_common import get_tinybird_local_client
46
45
  from tinybird.tb.modules.project import Project
47
46
 
48
47
 
49
48
  async def folder_build(
50
49
  project: Project,
50
+ tb_client: TinyB,
51
51
  filenames: Optional[List[str]] = None,
52
52
  is_internal: bool = False,
53
53
  is_vendor: bool = False,
@@ -67,7 +67,6 @@ async def folder_build(
67
67
  fork = False
68
68
  release_created = False
69
69
  folder = str(project.path)
70
- tb_client = await get_tinybird_local_client(folder)
71
70
  datasources: List[Dict[str, Any]] = await tb_client.datasources()
72
71
  pipes: List[Dict[str, Any]] = await tb_client.pipes(dependencies=True)
73
72
 
@@ -207,7 +207,7 @@ class Datafile:
207
207
  # [x] SQL in all nodes
208
208
  # [x] Materialized nodes have target datasource
209
209
  # [x] Only one materialized node
210
- # [ ] Only one node of any specific type
210
+ # [x] Only one node of any specific type
211
211
  # [ ] ...
212
212
  repeated_node_names = [
213
213
  name for name, count in filter(lambda x: x[1] > 1, Counter(n["name"] for n in self.nodes).items())
@@ -219,16 +219,14 @@ class Datafile:
219
219
  for node in self.nodes:
220
220
  if "sql" not in node:
221
221
  raise DatafileValidationError(f"SQL missing for node {repr(node['name'])}")
222
- materialized_nodes_count = 0
222
+ non_standard_nodes_count = 0
223
223
  for node in self.nodes:
224
- if node.get("type", "").lower() == "materialized":
225
- materialized_nodes_count += 1
226
- if materialized_nodes_count > 1:
227
- raise DatafileValidationError("Multiple materialized nodes in pipe. There can only be one")
228
- if "datasource" not in node:
229
- raise DatafileValidationError(
230
- f"Materialized node {repr(node['name'])} missing target datasource"
231
- )
224
+ if node.get("type", "").lower() not in {PipeNodeTypes.STANDARD, ""}:
225
+ non_standard_nodes_count += 1
226
+ if non_standard_nodes_count > 1:
227
+ raise DatafileValidationError("Multiple non-standard nodes in pipe. There can only be one")
228
+ if node.get("type", "").lower() == PipeNodeTypes.MATERIALIZED and "datasource" not in node:
229
+ raise DatafileValidationError(f"Materialized node {repr(node['name'])} missing target datasource")
232
230
  elif self.kind == DatafileKind.datasource:
233
231
  # TODO(eclbg):
234
232
  # [x] Just one node
@@ -1,5 +1,6 @@
1
1
  import json
2
2
  import logging
3
+ import sys
3
4
  import time
4
5
  from datetime import datetime
5
6
  from pathlib import Path
@@ -14,7 +15,7 @@ from tinybird.tb.modules.feedback_manager import FeedbackManager
14
15
  from tinybird.tb.modules.project import Project
15
16
 
16
17
 
17
- def promote_deployment(host: Optional[str], headers: dict) -> None:
18
+ def promote_deployment(host: Optional[str], headers: dict, wait: bool) -> None:
18
19
  TINYBIRD_API_URL = f"{host}/v1/deployments"
19
20
  r = requests.get(TINYBIRD_API_URL, headers=headers)
20
21
  result = r.json()
@@ -23,11 +24,11 @@ def promote_deployment(host: Optional[str], headers: dict) -> None:
23
24
  deployments = result.get("deployments")
24
25
  if not deployments:
25
26
  click.echo(FeedbackManager.error(message="No deployments found"))
26
- return
27
+ sys.exit(1)
27
28
 
28
29
  if len(deployments) < 2:
29
30
  click.echo(FeedbackManager.error(message="Only one deployment found"))
30
- return
31
+ sys.exit(1)
31
32
 
32
33
  last_deployment, candidate_deployment = deployments[0], deployments[1]
33
34
 
@@ -36,7 +37,7 @@ def promote_deployment(host: Optional[str], headers: dict) -> None:
36
37
  deploy_errors = candidate_deployment.get("errors", [])
37
38
  for deploy_error in deploy_errors:
38
39
  click.echo(FeedbackManager.error(message=f"* {deploy_error}"))
39
- return
40
+ sys.exit(1)
40
41
 
41
42
  if candidate_deployment.get("live"):
42
43
  click.echo(FeedbackManager.error(message="Candidate deployment is already live"))
@@ -55,10 +56,23 @@ def promote_deployment(host: Optional[str], headers: dict) -> None:
55
56
  result = r.json()
56
57
  logging.debug(json.dumps(result, indent=2))
57
58
 
59
+ if wait:
60
+ while True:
61
+ TINYBIRD_API_URL = f"{host}/v1/deployments/{last_deployment.get('id')}"
62
+ r = requests.get(TINYBIRD_API_URL, headers=headers)
63
+ result = r.json()
64
+ logging.debug(json.dumps(result, indent=2))
65
+
66
+ last_deployment = result.get("deployment")
67
+ if last_deployment.get("status") == "deleted":
68
+ break
69
+
70
+ time.sleep(5)
71
+
58
72
  click.echo(FeedbackManager.success(message="Deployment promoted successfully"))
59
73
 
60
74
 
61
- def rollback_deployment(host: Optional[str], headers: dict) -> None:
75
+ def rollback_deployment(host: Optional[str], headers: dict, wait: bool) -> None:
62
76
  TINYBIRD_API_URL = f"{host}/v1/deployments"
63
77
  r = requests.get(TINYBIRD_API_URL, headers=headers)
64
78
  result = r.json()
@@ -99,6 +113,18 @@ def rollback_deployment(host: Optional[str], headers: dict) -> None:
99
113
  result = r.json()
100
114
  logging.debug(json.dumps(result, indent=2))
101
115
 
116
+ if wait:
117
+ while True:
118
+ TINYBIRD_API_URL = f"{host}/v1/deployments/{current_deployment.get('id')}"
119
+ r = requests.get(TINYBIRD_API_URL, headers=headers)
120
+ result = r.json()
121
+ logging.debug(json.dumps(result, indent=2))
122
+
123
+ current_deployment = result.get("deployment")
124
+ if current_deployment.get("status") == "deleted":
125
+ break
126
+ time.sleep(5)
127
+
102
128
  click.echo(FeedbackManager.success(message="Deployment rolled back successfully"))
103
129
 
104
130
 
@@ -165,7 +191,13 @@ def deployment_ls(ctx: click.Context) -> None:
165
191
 
166
192
  @deployment_group.command(name="promote")
167
193
  @click.pass_context
168
- def deployment_promote(ctx: click.Context) -> None:
194
+ @click.option(
195
+ "--wait/--no-wait",
196
+ is_flag=True,
197
+ default=False,
198
+ help="Wait for deploy to finish. Disabled by default.",
199
+ )
200
+ def deployment_promote(ctx: click.Context, wait: bool) -> None:
169
201
  """
170
202
  Promote last deploy to ready and remove old one.
171
203
  """
@@ -174,12 +206,18 @@ def deployment_promote(ctx: click.Context) -> None:
174
206
  TINYBIRD_API_KEY = client.token
175
207
  HEADERS = {"Authorization": f"Bearer {TINYBIRD_API_KEY}"}
176
208
 
177
- promote_deployment(client.host, HEADERS)
209
+ promote_deployment(client.host, HEADERS, wait=wait)
178
210
 
179
211
 
180
212
  @deployment_group.command(name="rollback")
181
213
  @click.pass_context
182
- def deployment_rollback(ctx: click.Context) -> None:
214
+ @click.option(
215
+ "--wait/--no-wait",
216
+ is_flag=True,
217
+ default=False,
218
+ help="Wait for deploy to finish. Disabled by default.",
219
+ )
220
+ def deployment_rollback(ctx: click.Context, wait: bool) -> None:
183
221
  """
184
222
  Rollback to the previous deployment.
185
223
  """
@@ -188,7 +226,7 @@ def deployment_rollback(ctx: click.Context) -> None:
188
226
  TINYBIRD_API_KEY = client.token
189
227
  HEADERS = {"Authorization": f"Bearer {TINYBIRD_API_KEY}"}
190
228
 
191
- rollback_deployment(client.host, HEADERS)
229
+ rollback_deployment(client.host, HEADERS, wait=wait)
192
230
 
193
231
 
194
232
  @cli.command(name="deploy", hidden=True)
@@ -248,6 +286,12 @@ def create_deployment(ctx: click.Context, wait: bool, auto: bool) -> None:
248
286
  if deploy_result == "success":
249
287
  click.echo(FeedbackManager.success(message="Deployment submitted successfully"))
250
288
  deployment = result.get("deployment")
289
+ feedback = deployment.get("feedback", [])
290
+ for f in feedback:
291
+ click.echo(
292
+ FeedbackManager.warning(message=f"△ {f.get('level')}: {f.get('resource')}: {f.get('message')}")
293
+ )
294
+
251
295
  elif deploy_result == "failed":
252
296
  click.echo(FeedbackManager.error(message="Deployment failed"))
253
297
  deploy_errors = result.get("errors")
@@ -264,9 +308,11 @@ def create_deployment(ctx: click.Context, wait: bool, auto: bool) -> None:
264
308
  for fd in fds:
265
309
  fd.close()
266
310
 
311
+ if not deployment:
312
+ sys.exit(1)
313
+
267
314
  if deployment and wait:
268
- while deployment.get("status") != "data_ready":
269
- time.sleep(5)
315
+ while True:
270
316
  TINYBIRD_API_URL = f"{client.host}/v1/deployments/{deployment.get('id')}"
271
317
  r = requests.get(TINYBIRD_API_URL, headers=HEADERS)
272
318
  result = r.json()
@@ -281,10 +327,15 @@ def create_deployment(ctx: click.Context, wait: bool, auto: bool) -> None:
281
327
 
282
328
  if auto:
283
329
  click.echo(FeedbackManager.error(message="Rolling back deployment"))
284
- rollback_deployment(client.host, HEADERS)
285
- return
330
+ rollback_deployment(client.host, HEADERS, wait=wait)
331
+ sys.exit(1)
332
+
333
+ if deployment.get("status") == "data_ready":
334
+ break
335
+
336
+ time.sleep(5)
286
337
 
287
338
  click.echo(FeedbackManager.success(message="Deployment is ready"))
288
339
 
289
340
  if auto:
290
- promote_deployment(client.host, HEADERS)
341
+ promote_deployment(client.host, HEADERS, wait=wait)
@@ -73,9 +73,7 @@ class FeedbackManager:
73
73
  error_exception = error_exception("{error}")
74
74
  simple_error_exception = simple_error_message("{error}")
75
75
  error_exception_trace = error_message("{error}\n** Trace:\n{trace}")
76
- error_notoken = error_message(
77
- "No auth token provided. Run 'tb auth' to configure them or re-run the command passing the --token param (example: tb --token <the_token> datasource ls)."
78
- )
76
+ error_notoken = error_message("This action requires authentication. Run 'tb login' first")
79
77
  error_auth_config = error_message("{config_file} does not exist")
80
78
  error_file_config = error_message("{config_file} can't be written, check write permissions on this folder")
81
79
  error_load_file_config = error_message("{config_file} can't be loaded, remove it and run the command again")
@@ -4,7 +4,7 @@ import os
4
4
 
5
5
  import requests
6
6
 
7
- from tinybird.client import TinyB
7
+ from tinybird.client import AuthNoTokenException, TinyB
8
8
  from tinybird.tb.modules.config import CLIConfig
9
9
  from tinybird.tb.modules.exceptions import CLIException
10
10
 
@@ -40,8 +40,10 @@ async def get_tinybird_local_config(path: str, build: bool = False) -> CLIConfig
40
40
  if path:
41
41
  folder_hash = hashlib.sha256(path.encode()).hexdigest()
42
42
  user_client = config.get_client(host=TB_LOCAL_HOST, token=user_token)
43
- ws_name = config.get("name")
44
- ws_name = f"Tinybird_Local_Build_{folder_hash}" if build else ws_name
43
+ ws_name = f"Tinybird_Local_Build_{folder_hash}" if build else config.get("name")
44
+ if not ws_name:
45
+ raise AuthNoTokenException()
46
+
45
47
  logging.debug(f"Workspace used for build: {ws_name}")
46
48
 
47
49
  user_workspaces = requests.get(f"{TB_LOCAL_HOST}/v0/user/workspaces?token={user_token}").json()
@@ -54,7 +56,7 @@ async def get_tinybird_local_config(path: str, build: bool = False) -> CLIConfig
54
56
  user_workspaces = requests.get(f"{TB_LOCAL_HOST}/v0/user/workspaces?token={user_token}").json()
55
57
  ws = next((ws for ws in user_workspaces["workspaces"] if ws["name"] == ws_name), None)
56
58
  if not ws:
57
- raise CLIException(f"Workspace {ws_name} not found after creation")
59
+ raise AuthNoTokenException()
58
60
 
59
61
  ws_token = ws["token"]
60
62
 
@@ -121,7 +121,7 @@ async def login(host: str, workspace: str):
121
121
  """Authenticate using the browser."""
122
122
  auth_event = threading.Event()
123
123
  auth_code = [None] # Using a list to store the code, as it's mutable
124
- host = host or "https://api.tinybird.co"
124
+ host = host or "https://api.wadus2.gcp.tinybird.co"
125
125
 
126
126
  def auth_callback(code):
127
127
  auth_code[0] = code
@@ -5,6 +5,7 @@ from typing import Optional
5
5
 
6
6
  import click
7
7
 
8
+ from tinybird.client import TinyB
8
9
  from tinybird.prompts import mock_prompt
9
10
  from tinybird.tb.modules.cli import cli
10
11
  from tinybird.tb.modules.common import CLIException, check_user_token_with_client, coro
@@ -13,7 +14,6 @@ from tinybird.tb.modules.datafile.fixture import build_fixture_name, persist_fix
13
14
  from tinybird.tb.modules.feedback_manager import FeedbackManager
14
15
  from tinybird.tb.modules.llm import LLM
15
16
  from tinybird.tb.modules.llm_utils import extract_xml
16
- from tinybird.tb.modules.local_common import get_tinybird_local_client
17
17
  from tinybird.tb.modules.project import Project
18
18
 
19
19
 
@@ -40,6 +40,7 @@ async def mock(ctx: click.Context, datasource: str, rows: int, prompt: str, skip
40
40
  """
41
41
 
42
42
  try:
43
+ tb_client: TinyB = ctx.ensure_object(dict)["client"]
43
44
  project: Project = ctx.ensure_object(dict)["project"]
44
45
  datasource_path = Path(datasource)
45
46
  datasource_name = datasource
@@ -76,7 +77,6 @@ async def mock(ctx: click.Context, datasource: str, rows: int, prompt: str, skip
76
77
  click.echo(FeedbackManager.error(message="This action requires authentication. Run 'tb login' first."))
77
78
  return
78
79
  llm = LLM(user_token=user_token, host=user_client.host)
79
- tb_client = await get_tinybird_local_client(folder)
80
80
  prompt = f"<datasource_schema>{datasource_content}</datasource_schema>\n<user_input>{prompt}</user_input>"
81
81
  iterations = 0
82
82
  history = ""
@@ -40,11 +40,11 @@ class Project:
40
40
 
41
41
  @property
42
42
  def datasources(self) -> List[str]:
43
- return [Path(f).stem for f in glob.glob(f"{self.path}/**/*.datasource", recursive=True)]
43
+ return sorted([Path(f).stem for f in glob.glob(f"{self.path}/**/*.datasource", recursive=True)])
44
44
 
45
45
  @property
46
46
  def pipes(self) -> List[str]:
47
- return [Path(f).stem for f in glob.glob(f"{self.path}/**/*.pipe", recursive=True)]
47
+ return sorted([Path(f).stem for f in glob.glob(f"{self.path}/**/*.pipe", recursive=True)])
48
48
 
49
49
  def get_pipe_datafile(self, filename: str) -> Optional[Datafile]:
50
50
  try:
@@ -69,14 +69,27 @@ class DynamicCompleter(Completer):
69
69
  if not words:
70
70
  return
71
71
 
72
- # If we just typed 'from' or there's a space after 'from', suggest datasources
73
- if words[-1] == "from" or (
74
- "from" in words and len(words) > words.index("from") + 1 and text_lower.endswith(" ")
75
- ):
76
- for x in self.project.datasources:
77
- yield Completion(x, start_position=0, display=x, style="class:completion.datasource")
78
- for x in self.project.pipes:
79
- yield Completion(x, start_position=0, display=x, style="class:completion.pipe")
72
+ # If we're after FROM, suggest filtered datasources and pipes
73
+ if "from" in words:
74
+ from_index = words.index("from")
75
+ # Check if we're typing after FROM
76
+ if len(words) > from_index + 1:
77
+ current_word = words[-1].lower()
78
+ # Suggest filtered datasources and pipes based on current word
79
+ for x in self.project.datasources:
80
+ if current_word in x.lower():
81
+ yield Completion(
82
+ x, start_position=-len(current_word), display=x, style="class:completion.datasource"
83
+ )
84
+ for x in self.project.pipes:
85
+ if current_word in x.lower():
86
+ yield Completion(x, start_position=-len(current_word), display=x, style="class:completion.pipe")
87
+ else:
88
+ # Just typed FROM, show all datasources and pipes
89
+ for x in self.project.datasources:
90
+ yield Completion(x, start_position=0, display=x, style="class:completion.datasource")
91
+ for x in self.project.pipes:
92
+ yield Completion(x, start_position=0, display=x, style="class:completion.pipe")
80
93
  return
81
94
 
82
95
  # If we're starting a query, suggest SQL keywords
@@ -88,13 +101,20 @@ class DynamicCompleter(Completer):
88
101
  )
89
102
 
90
103
  def _handle_mock_completions(self, words: List[str]):
91
- if len(words) == 1:
104
+ if len(words) == 1 or len(words) == 2:
92
105
  # After 'mock', show datasources
93
- for ds in self.project.datasources:
94
- yield Completion(ds, start_position=0, display=ds, style="class:completion.datasource")
106
+ current_word = words[-1]
107
+ for cmd in self.project.datasources:
108
+ if current_word in cmd.lower():
109
+ yield Completion(
110
+ cmd,
111
+ start_position=-len(current_word) if current_word else 0,
112
+ display=cmd,
113
+ style="class:completion.datasource",
114
+ )
95
115
  return
96
116
 
97
- if len(words) == 2 or len(words) == 4:
117
+ if len(words) == 3 or len(words) == 4:
98
118
  # After datasource or after a flag value, show available flags
99
119
  available_flags = [f for f in self.mock_flags if f not in words]
100
120
  for flag in available_flags:
@@ -113,9 +133,16 @@ class DynamicCompleter(Completer):
113
133
  for cmd in self.test_commands:
114
134
  yield Completion(cmd, start_position=0, display=cmd, style="class:completion.cmd")
115
135
  return
116
- elif len(words) == 2:
136
+ elif len(words) == 2 or len(words) == 3:
137
+ current_word = words[-1]
117
138
  for cmd in self.project.pipes:
118
- yield Completion(cmd, start_position=0, display=cmd, style="class:completion.pipe")
139
+ if current_word in cmd.lower():
140
+ yield Completion(
141
+ cmd,
142
+ start_position=-len(current_word) if current_word else 0,
143
+ display=cmd,
144
+ style="class:completion.pipe",
145
+ )
119
146
  return
120
147
 
121
148
  def _yield_static_commands(self, current_word: str):
@@ -129,7 +156,7 @@ class DynamicCompleter(Completer):
129
156
  )
130
157
 
131
158
  for cmd in self.project.datasources:
132
- if cmd.startswith(current_word):
159
+ if current_word in cmd.lower():
133
160
  yield Completion(
134
161
  cmd,
135
162
  start_position=-len(current_word) if current_word else 0,
@@ -138,7 +165,7 @@ class DynamicCompleter(Completer):
138
165
  )
139
166
 
140
167
  for cmd in self.project.pipes:
141
- if cmd.startswith(current_word):
168
+ if current_word in cmd.lower():
142
169
  yield Completion(
143
170
  cmd,
144
171
  start_position=-len(current_word) if current_word else 0,
@@ -245,7 +272,7 @@ class Shell:
245
272
  click.echo(FeedbackManager.error(message=f"'tb {arg}' command is not available in watch mode"))
246
273
 
247
274
  def handle_mock(self, arg):
248
- subprocess.run(f"tb --build --folder {self.project.folder} mock {arg}", shell=True, text=True)
275
+ subprocess.run(f"tb --build --folder {self.project.folder} mock {arg} --skip", shell=True, text=True)
249
276
 
250
277
  def handle_tb(self, arg):
251
278
  click.echo("")
@@ -259,6 +286,9 @@ class Shell:
259
286
  elif arg.startswith("mock"):
260
287
  self.handle_mock(arg)
261
288
  else:
289
+ need_skip = ("mock", "test create", "create")
290
+ if any(arg.startswith(cmd) for cmd in need_skip):
291
+ arg = f"{arg} --skip"
262
292
  subprocess.run(f"tb --build --folder {self.project.folder} {arg}", shell=True, text=True)
263
293
 
264
294
  def default(self, argline):
@@ -271,6 +301,9 @@ class Shell:
271
301
  elif len(arg.split()) == 1 and arg in self.project.pipes + self.project.datasources:
272
302
  self.run_sql(f"select * from {arg}")
273
303
  else:
304
+ need_skip = ("mock", "test create", "create")
305
+ if any(arg.startswith(cmd) for cmd in need_skip):
306
+ arg = f"{arg} --skip"
274
307
  subprocess.run(f"tb --build --folder {self.project.folder} {arg}", shell=True, text=True)
275
308
 
276
309
  def run_sql(self, query, rows_limit=20):
@@ -5,7 +5,6 @@
5
5
 
6
6
  import difflib
7
7
  import glob
8
- import os
9
8
  import urllib.parse
10
9
  from pathlib import Path
11
10
  from typing import Any, Dict, List, Optional, Tuple
@@ -14,6 +13,7 @@ import click
14
13
  import yaml
15
14
  from requests import Response
16
15
 
16
+ from tinybird.client import TinyB
17
17
  from tinybird.prompts import test_create_prompt
18
18
  from tinybird.tb.modules.cli import cli
19
19
  from tinybird.tb.modules.common import coro
@@ -22,7 +22,6 @@ from tinybird.tb.modules.exceptions import CLIException
22
22
  from tinybird.tb.modules.feedback_manager import FeedbackManager
23
23
  from tinybird.tb.modules.llm import LLM
24
24
  from tinybird.tb.modules.llm_utils import extract_xml, parse_xml
25
- from tinybird.tb.modules.local_common import get_tinybird_local_client
26
25
  from tinybird.tb.modules.project import Project
27
26
 
28
27
  yaml.SafeDumper.org_represent_str = yaml.SafeDumper.represent_str # type: ignore[attr-defined]
@@ -79,6 +78,7 @@ async def test_create(ctx: click.Context, name_or_filename: str, prompt: str, sk
79
78
  Create a test for an existing pipe
80
79
  """
81
80
  project: Project = ctx.ensure_object(dict)["project"]
81
+ client: TinyB = ctx.ensure_object(dict)["client"]
82
82
  root_path = Path(project.folder)
83
83
  folder = project.folder
84
84
  try:
@@ -102,7 +102,6 @@ async def test_create(ctx: click.Context, name_or_filename: str, prompt: str, sk
102
102
  pipe_path = root_path / pipe_path
103
103
  pipe_content = pipe_path.read_text()
104
104
 
105
- client = await get_tinybird_local_client(folder, build=True)
106
105
  pipe = await client._req(f"/v0/pipes/{pipe_name}")
107
106
  parameters = set([param["name"] for node in pipe["nodes"] for param in node["params"]])
108
107
 
@@ -200,10 +199,11 @@ async def test_create(ctx: click.Context, name_or_filename: str, prompt: str, sk
200
199
  type=click.Path(exists=True, file_okay=False),
201
200
  help="Folder where datafiles will be placed",
202
201
  )
202
+ @click.pass_context
203
203
  @coro
204
- async def test_update(pipe: str, folder: str) -> None:
204
+ async def test_update(ctx: click.Context, pipe: str, folder: str) -> None:
205
205
  try:
206
- client = await get_tinybird_local_client(os.path.abspath(folder))
206
+ client: TinyB = ctx.ensure_object(dict)["client"]
207
207
  pipe_tests_path = Path(pipe)
208
208
  pipe_name = pipe
209
209
  if pipe_tests_path.suffix == ".yaml":
@@ -252,10 +252,11 @@ async def test_update(pipe: str, folder: str) -> None:
252
252
  type=click.Path(exists=True, file_okay=False),
253
253
  help="Folder where tests will be placed",
254
254
  )
255
+ @click.pass_context
255
256
  @coro
256
- async def run_tests(name: Tuple[str, ...], folder: str) -> None:
257
+ async def run_tests(ctx: click.Context, name: Tuple[str, ...], folder: str) -> None:
257
258
  click.echo(FeedbackManager.highlight(message="\n» Running tests"))
258
- client = await get_tinybird_local_client(os.path.abspath(folder))
259
+ client: TinyB = ctx.ensure_object(dict)["client"]
259
260
  paths = [Path(n) for n in name]
260
261
  endpoints = [f"./tests/{p.stem}.yaml" for p in paths]
261
262
  test_files: List[str] = endpoints if len(endpoints) > 0 else glob.glob("./tests/**/*.y*ml", recursive=True)
@@ -311,7 +312,7 @@ async def run_tests(name: Tuple[str, ...], folder: str) -> None:
311
312
  click.echo(FeedbackManager.success(message=f"\n✓ {test_count}/{test_count} passed"))
312
313
 
313
314
 
314
- async def get_pipe_data(client, pipe_name: str, test_params: str) -> Response:
315
+ async def get_pipe_data(client: TinyB, pipe_name: str, test_params: str) -> Response:
315
316
  pipe = await client._req(f"/v0/pipes/{pipe_name}")
316
317
  output_node = next(
317
318
  (node for node in pipe["nodes"] if node["node_type"] != "default" and node["node_type"] != "standard"),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tinybird
3
- Version: 0.0.1.dev48
3
+ Version: 0.0.1.dev49
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -6,7 +6,7 @@ tinybird/context.py,sha256=A3GBApac9xO6hrAMJ1s9dMrI_ou9aKF84CdEjtPddMk,1417
6
6
  tinybird/datatypes.py,sha256=XNypumfqNjsvLJ5iNXnbVHRvAJe0aQwI3lS6Cxox-e0,10979
7
7
  tinybird/feedback_manager.py,sha256=g1r9NcFfKXdk_13soaiTZLvdoUGleVfawl6Yfj3zmRw,67823
8
8
  tinybird/git_settings.py,sha256=Sw_8rGmribEFJ4Z_6idrVytxpFYk7ez8ei0qHULzs3E,3934
9
- tinybird/prompts.py,sha256=M9Gh6XO1utkGZYLB6dmW8iSHl_LxOy4m2On_THMCgEU,32645
9
+ tinybird/prompts.py,sha256=unaqHksayhVtxFaTTxuL6Dftb1IM9vQncXtpTI3pxuY,30545
10
10
  tinybird/sql.py,sha256=LBi74GxhNAYTb6m2-KNGpAkguSKh7rcvBbERbE7nalA,46195
11
11
  tinybird/sql_template.py,sha256=GmMLAI10MTqjQo9qztuQHLRWs67teozsWDxUBdvkAn4,93668
12
12
  tinybird/sql_template_fmt.py,sha256=KUHdj5rYCYm_rKKdXYSJAE9vIyXUQLB0YSZnUXHeBlY,10196
@@ -15,48 +15,47 @@ tinybird/syncasync.py,sha256=IPnOx6lMbf9SNddN1eBtssg8vCLHMt76SuZ6YNYm-Yk,27761
15
15
  tinybird/tornado_template.py,sha256=FL85SMPq2dH4JqKovmSbaolGdEzwOO91NqOzqXo2Qr0,41863
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=1T9gM4V33CKJ7ISsAivZ7G4ve2JMT0lY6SqWdJEd0sg,251
19
- tinybird/tb/cli.py,sha256=X6zLCVI10wOxcu-USGfbp_jjz1cojH5X-9dL7D8c8aI,960
18
+ tinybird/tb/__cli__.py,sha256=Yocwg57d8BTpRpM9BAwbi73DJuatECKd9hhtsD5-Y3U,251
19
+ tinybird/tb/cli.py,sha256=FD1pfbzu9YHJHEG6Vtn_EwPLTYhwqw-I6AxXeTaRHU8,926
20
20
  tinybird/tb/modules/auth.py,sha256=EzRWFmwRkXNhUmRaruEVFLdkbUg8xMSix0cAWl5D4Jg,9029
21
- tinybird/tb/modules/build.py,sha256=lQXivX0kn3ej0CzeJK34ZFYBWAm2QB1JyPWciuH2c-Q,8060
21
+ tinybird/tb/modules/build.py,sha256=LvopBeGSuTey7UDMOABl7xpLRyyVQS2E2wiUHxxWPnc,8340
22
22
  tinybird/tb/modules/cicd.py,sha256=xxXwy-QekJcG14kkJeGNl7LkHduhZXfvBZE8WrU6-t4,5351
23
- tinybird/tb/modules/cli.py,sha256=KlG4ii--pqestUILJwSx9-e-D_X1MBllwojWpejg7s8,17623
23
+ tinybird/tb/modules/cli.py,sha256=882TimWnFyq1v0sfLDTtmPA_lFuVyL-GwmFpufB5Rxg,16075
24
24
  tinybird/tb/modules/common.py,sha256=TWcGJUgzJCQvzI1oMKbNdx-KTRmMGvB25BawHpsaV8Q,70610
25
25
  tinybird/tb/modules/config.py,sha256=mie3oMVTf5YOUFEiLs88P16U4LkJafJjSpjwyAkFHog,10979
26
26
  tinybird/tb/modules/copy.py,sha256=wxyxZg8BPiWDgbW5HXJKYQp7_EumBXmAilo3McbCQOo,5916
27
- tinybird/tb/modules/create.py,sha256=fqEI83cENISWgFnoNPlXNsNx_R0Im-zMaL1PfJDGWUQ,14263
27
+ tinybird/tb/modules/create.py,sha256=_eAMjntZ85uub1HqHoDekWC7ks79FQ2Zbc_6grjhT_g,14472
28
28
  tinybird/tb/modules/datasource.py,sha256=TQ4wSag3CCw34d54FEXPJFGLQNYyNqv2nQbU6QT9uAE,14725
29
- tinybird/tb/modules/deployment.py,sha256=0e00lLMaNoVnONYdA51Zy9Sa_-4jWqePoYeFyHUn5PU,10241
29
+ tinybird/tb/modules/deployment.py,sha256=Ezb_7ZK6ym967GNPzuSUH3K6ukAMWXaHb1gFXNet2pA,11859
30
30
  tinybird/tb/modules/endpoint.py,sha256=9arqN1JQCMb0Nd3-EJ7lukOYkGHHCpQmiiZpp5FqPhc,9432
31
31
  tinybird/tb/modules/exceptions.py,sha256=4A2sSjCEqKUMqpP3WI00zouCWW4uLaghXXLZBSw04mY,3363
32
- tinybird/tb/modules/feedback_manager.py,sha256=0DSaHP4tPT1PnYVrH4N2d2vnealrBS1jI3CSMex_Us8,68572
32
+ tinybird/tb/modules/feedback_manager.py,sha256=mrw5tdYycfvg6WLXlM0KIjfJardm_aNpnJkUg2vH0cA,68463
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
35
  tinybird/tb/modules/llm.py,sha256=AC0VSphTOM2t-v1_3NLvNN_FIbgMo4dTyMqIv5nniPo,835
36
36
  tinybird/tb/modules/llm_utils.py,sha256=nS9r4FAElJw8yXtmdYrx-rtI2zXR8qXfi1QqUDCfxvg,3469
37
37
  tinybird/tb/modules/local.py,sha256=x4xuCGVkoa8KLYGZEJnFUP8HUkKX05Frp_djRVjVjTs,5669
38
- tinybird/tb/modules/local_common.py,sha256=lUcMIqMJxygtpBEf-B57pTt68FpVz-W9YC5tBpnqOws,2744
39
- tinybird/tb/modules/login.py,sha256=0cS-f3MsQFHc6xjw8FRWJm4EJBH9C7Ri68EcO_tiwes,6508
38
+ tinybird/tb/modules/local_common.py,sha256=UW6tNexC98aGRC6ca2w8qDDzyTTn-GDfr3uRy7zRHBM,2767
39
+ tinybird/tb/modules/login.py,sha256=cnB2Vwjc94FpG9C-7Y-l7Lb0Xilp2oUFDzqbQW0tpkU,6519
40
40
  tinybird/tb/modules/materialization.py,sha256=HQKRTH6lkcYiDQJihbFqF_in58ezXG4ggZ_7Ywp_nUM,5738
41
- tinybird/tb/modules/mock.py,sha256=maaPY91mQx8m9x-1-2r9svljRO8VO8-uqD-71VfXBCU,5271
41
+ tinybird/tb/modules/mock.py,sha256=PzFtZL-6bZAZ3EiCC2nYJo058I4m50fD7FcBHISn3cI,5235
42
42
  tinybird/tb/modules/pipe.py,sha256=pH2KwgH6Xbvl3kT8vMelpKvT6bcyB4EKFDvGfOsxXbg,2418
43
- tinybird/tb/modules/project.py,sha256=YH-pwioIBIYxSmy6awM6Ya6WoB7MSM2fte-x9Gbr6lc,2604
43
+ tinybird/tb/modules/project.py,sha256=rucAh_6K1EsW2dxjmqHPJ7bt6IliOWR0dNtEYG454fw,2620
44
44
  tinybird/tb/modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
45
- tinybird/tb/modules/shell.py,sha256=iVJ_TC0WEOMVacc6LximIZWHeL2RGohQKv-oU75HpL4,13018
45
+ tinybird/tb/modules/shell.py,sha256=LShGXwP_kzYAOUiTcvHMmcDugVr0crIfdNb5En6cfxo,14604
46
46
  tinybird/tb/modules/table.py,sha256=4XrtjM-N0zfNtxVkbvLDQQazno1EPXnxTyo7llivfXk,11035
47
47
  tinybird/tb/modules/tag.py,sha256=anPmMUBc-TbFovlpFi8GPkKA18y7Y0GczMsMms5TZsU,3502
48
48
  tinybird/tb/modules/telemetry.py,sha256=iEGnMuCuNhvF6ln__j6X9MSTwL_0Hm-GgFHHHvhfknk,10466
49
- tinybird/tb/modules/test.py,sha256=s15UQg1TzYrI-9WUA7eYpQcx-BsCRTa6Oj71K-Zbcr0,13100
49
+ tinybird/tb/modules/test.py,sha256=UoYGwTSY6_0ijS2sClMq-1OdOBbg-IXMDD8oLfZXvUg,13093
50
50
  tinybird/tb/modules/token.py,sha256=sPdJoBE-6dd3Sd6W-prst7VOoJ0NbvP0uTaB6dXHs5s,12711
51
- tinybird/tb/modules/update.py,sha256=GWYfbKnB0yf4ywAUp8jItaZY0ltBnaIMcLSiPpwtbMc,6832
52
51
  tinybird/tb/modules/watch.py,sha256=Kredt5C7OOiI6YOivuR5QBdiDY4J_xLiwqHOROnfcsU,8591
53
52
  tinybird/tb/modules/workspace.py,sha256=sfT9QkoeFlN7ndUXxyImp4a7EFEHjY9MlGlldOViz0Y,6404
54
53
  tinybird/tb/modules/workspace_members.py,sha256=Ai6iCOzXX1zQ8q9iXIFSFHsBJlT-8Q28DaG5Ie-UweY,8726
55
- tinybird/tb/modules/datafile/build.py,sha256=VgQ0k-dBNE0hXdnxB12NW5I0XpxEosN2vaszboj3LZY,51045
54
+ tinybird/tb/modules/datafile/build.py,sha256=seGFSvmgyRrAM1-icsKBkuog3WccfGUYFTPT-xoA5W8,50940
56
55
  tinybird/tb/modules/datafile/build_common.py,sha256=IXl-Z51zUi1dypV7meNenX0iu2UmowNeqgG6WHyMHlk,4562
57
56
  tinybird/tb/modules/datafile/build_datasource.py,sha256=4aP8_DYCRGghXntZSeWDNJxjps1QRVa7WHoYCzQwQts,17355
58
57
  tinybird/tb/modules/datafile/build_pipe.py,sha256=Jgv3YKIvMfjPiSIdw1k2mpaoDdAWMiMRaSHwRgyI97E,28258
59
- tinybird/tb/modules/datafile/common.py,sha256=rk0jHAmyl9ogWZhayVruoPihtNhNmdgCK2e4PEHadg4,78829
58
+ tinybird/tb/modules/datafile/common.py,sha256=E_JDdlYqsPq3TVEt8FMcMpuF5xbKLtl8uLpj99RXSgE,78848
60
59
  tinybird/tb/modules/datafile/diff.py,sha256=-0J7PsBO64T7LOZSkZ4ZFHHCPvT7cKItnJkbz2PkndU,6754
61
60
  tinybird/tb/modules/datafile/exceptions.py,sha256=8rw2umdZjtby85QbuRKFO5ETz_eRHwUY5l7eHsy1wnI,556
62
61
  tinybird/tb/modules/datafile/fixture.py,sha256=bdZndItV6ibOegPCrN3OgKdkpjDFCFvoSoiZVsCV_XQ,1852
@@ -75,8 +74,8 @@ tinybird/tb_cli_modules/config.py,sha256=6u6B5QCdiQLbJkCkwtnKGs9H3nP-KXXhC75mF7B
75
74
  tinybird/tb_cli_modules/exceptions.py,sha256=pmucP4kTF4irIt7dXiG-FcnI-o3mvDusPmch1L8RCWk,3367
76
75
  tinybird/tb_cli_modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
77
76
  tinybird/tb_cli_modules/telemetry.py,sha256=iEGnMuCuNhvF6ln__j6X9MSTwL_0Hm-GgFHHHvhfknk,10466
78
- tinybird-0.0.1.dev48.dist-info/METADATA,sha256=onz2vfZ9IkLQVpwSoaCIuUPbnzAsqFH1FIoe9wRDceg,2482
79
- tinybird-0.0.1.dev48.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
80
- tinybird-0.0.1.dev48.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
81
- tinybird-0.0.1.dev48.dist-info/top_level.txt,sha256=pgw6AzERHBcW3YTi2PW4arjxLkulk2msOz_SomfOEuc,45
82
- tinybird-0.0.1.dev48.dist-info/RECORD,,
77
+ tinybird-0.0.1.dev49.dist-info/METADATA,sha256=xwzvzDJ7ucjTfGvI0bPguc6fKa-C06ub0NSPwUmqC3g,2482
78
+ tinybird-0.0.1.dev49.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
79
+ tinybird-0.0.1.dev49.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
80
+ tinybird-0.0.1.dev49.dist-info/top_level.txt,sha256=VqqqEmkAy7UNaD8-V51FCoMMWXjLUlR0IstvK7tJYVY,54
81
+ tinybird-0.0.1.dev49.dist-info/RECORD,,
@@ -1,4 +1,5 @@
1
1
  tests
2
2
  tests_e2e
3
3
  tests_e2e_ingestion
4
+ tests_tb
4
5
  tinybird
@@ -1,182 +0,0 @@
1
- import os
2
- import re
3
- from os import getcwd
4
- from pathlib import Path
5
- from typing import Optional
6
-
7
- import click
8
-
9
- from tinybird.client import TinyB
10
- from tinybird.prompts import mock_prompt, update_prompt
11
- from tinybird.tb.modules.cli import cli
12
- from tinybird.tb.modules.common import check_user_token_with_client, coro, generate_datafile
13
- from tinybird.tb.modules.config import CLIConfig
14
- from tinybird.tb.modules.datafile.fixture import build_fixture_name, persist_fixture
15
- from tinybird.tb.modules.exceptions import CLIException
16
- from tinybird.tb.modules.feedback_manager import FeedbackManager
17
- from tinybird.tb.modules.llm import LLM
18
- from tinybird.tb.modules.llm_utils import extract_xml, parse_xml
19
- from tinybird.tb.modules.local_common import get_tinybird_local_client
20
-
21
-
22
- @cli.command(hidden=True)
23
- @click.argument("prompt")
24
- @click.option(
25
- "--folder",
26
- default=".",
27
- type=click.Path(exists=False, file_okay=False),
28
- help="Folder where project files will be placed",
29
- )
30
- @coro
31
- async def update(
32
- prompt: str,
33
- folder: str,
34
- ) -> None:
35
- """Update resources in the project."""
36
- folder = folder or getcwd()
37
- folder_path = Path(folder)
38
- if not folder_path.exists():
39
- folder_path.mkdir()
40
-
41
- try:
42
- config = CLIConfig.get_project_config(folder)
43
- tb_client = config.get_client()
44
- user_token: Optional[str] = None
45
- try:
46
- user_token = config.get_user_token()
47
- if not user_token:
48
- raise CLIException("No user token found")
49
- await check_user_token_with_client(tb_client, token=user_token)
50
- except Exception as e:
51
- click.echo(
52
- FeedbackManager.error(message=f"This action requires authentication. Run 'tb login' first. Error: {e}")
53
- )
54
- return
55
-
56
- local_client = await get_tinybird_local_client(folder)
57
-
58
- click.echo(FeedbackManager.highlight(message="\n» Updating resources..."))
59
- datasources_updated = await update_resources(tb_client, user_token, prompt, folder)
60
- click.echo(FeedbackManager.success(message="✓ Done!\n"))
61
-
62
- if datasources_updated and user_token:
63
- click.echo(FeedbackManager.highlight(message="\n» Generating fixtures..."))
64
-
65
- datasource_files = [f for f in os.listdir(Path(folder) / "datasources") if f.endswith(".datasource")]
66
- for datasource_file in datasource_files:
67
- datasource_path = Path(folder) / "datasources" / datasource_file
68
- llm = LLM(user_token=user_token, host=tb_client.host)
69
- datasource_name = datasource_path.stem
70
- datasource_content = datasource_path.read_text()
71
- has_json_path = "`json:" in datasource_content
72
- if has_json_path:
73
- prompt = f"<datasource_schema>{datasource_content}</datasource_schema>\n<user_input>{prompt}</user_input>"
74
- response = llm.ask(system_prompt=mock_prompt(rows=20), prompt=prompt)
75
- sql = extract_xml(response, "sql")
76
- sql = sql.split("FORMAT")[0]
77
- result = await local_client.query(f"{sql} FORMAT JSON")
78
- data = result.get("data", [])
79
- fixture_name = build_fixture_name(
80
- datasource_path.absolute().as_posix(), datasource_name, datasource_content
81
- )
82
- if data:
83
- persist_fixture(fixture_name, data, folder)
84
- click.echo(FeedbackManager.info(message=f"✓ /fixtures/{datasource_name}"))
85
-
86
- click.echo(FeedbackManager.success(message="✓ Done!\n"))
87
- except Exception as e:
88
- click.echo(FeedbackManager.error(message=f"Error: {str(e)}"))
89
-
90
-
91
- async def update_resources(
92
- tb_client: TinyB,
93
- user_token: str,
94
- prompt: str,
95
- folder: str,
96
- ):
97
- datasource_paths = [
98
- Path(folder) / "datasources" / f for f in os.listdir(Path(folder) / "datasources") if f.endswith(".datasource")
99
- ]
100
- pipes_paths = [
101
- Path(folder) / "endpoints" / f for f in os.listdir(Path(folder) / "endpoints") if f.endswith(".pipe")
102
- ]
103
- resources_xml = "\n".join(
104
- [
105
- f"<resource><type>{resource_type}</type><name>{resource_name}</name><content>{resource_content}</content></resource>"
106
- for resource_type, resource_name, resource_content in [
107
- ("datasource", ds.stem, ds.read_text()) for ds in datasource_paths
108
- ]
109
- + [
110
- (
111
- "pipe",
112
- pipe.stem,
113
- pipe.read_text(),
114
- )
115
- for pipe in pipes_paths
116
- ]
117
- ]
118
- )
119
- llm = LLM(user_token=user_token, host=tb_client.host)
120
- result = llm.ask(system_prompt=update_prompt(resources_xml), prompt=prompt)
121
- result = extract_xml(result, "response")
122
- resources = parse_xml(result, "resource")
123
- datasources = []
124
- pipes = []
125
- for resource_xml in resources:
126
- resource_type = extract_xml(resource_xml, "type")
127
- name = extract_xml(resource_xml, "name")
128
- content = extract_xml(resource_xml, "content")
129
- resource = {
130
- "name": name,
131
- "content": content,
132
- }
133
- if resource_type.lower() == "datasource":
134
- datasources.append(resource)
135
- elif resource_type.lower() == "pipe":
136
- pipes.append(resource)
137
-
138
- for ds in datasources:
139
- content = ds["content"].replace("```", "")
140
- filename = f"{ds['name']}.datasource"
141
- generate_datafile(
142
- content,
143
- filename=filename,
144
- data=None,
145
- _format="ndjson",
146
- force=True,
147
- folder=folder,
148
- )
149
-
150
- for pipe in pipes:
151
- content = pipe["content"].replace("```", "")
152
- generate_pipe_file(pipe["name"], content, folder)
153
-
154
- return len(datasources) > 0
155
-
156
-
157
- def generate_pipe_file(name: str, content: str, folder: str):
158
- def is_copy(content: str) -> bool:
159
- return re.search(r"TYPE copy", content, re.IGNORECASE) is not None
160
-
161
- def is_materialization(content: str) -> bool:
162
- return re.search(r"TYPE materialized", content, re.IGNORECASE) is not None
163
-
164
- def is_sink(content: str) -> bool:
165
- return re.search(r"TYPE sink", content, re.IGNORECASE) is not None
166
-
167
- if is_copy(content):
168
- pathname = "copies"
169
- elif is_materialization(content):
170
- pathname = "materializations"
171
- elif is_sink(content):
172
- pathname = "sinks"
173
- else:
174
- pathname = "endpoints"
175
-
176
- base = Path(folder) / pathname
177
- if not base.exists():
178
- base = Path()
179
- f = base / (f"{name}.pipe")
180
- with open(f"{f}", "w") as file:
181
- file.write(content)
182
- click.echo(FeedbackManager.info_file_created(file=f.relative_to(folder)))