tinybird 0.0.1.dev9__tar.gz → 0.0.1.dev10__tar.gz

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.

Files changed (92) hide show
  1. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/PKG-INFO +1 -1
  2. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/feedback_manager.py +1 -1
  3. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/build.py +125 -44
  4. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/cicd.py +0 -1
  5. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/cli.py +0 -3
  6. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/common.py +35 -13
  7. tinybird-0.0.1.dev10/tinybird/tb/modules/create.py +176 -0
  8. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/datafile/build.py +13 -4
  9. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/datafile/build_pipe.py +0 -1
  10. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/datafile/common.py +12 -0
  11. tinybird-0.0.1.dev10/tinybird/tb/modules/datafile/fixture.py +53 -0
  12. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/llm.py +1 -1
  13. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/local.py +36 -17
  14. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/mock.py +15 -16
  15. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/prompts.py +28 -2
  16. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird.egg-info/PKG-INFO +1 -1
  17. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird.egg-info/SOURCES.txt +1 -0
  18. tinybird-0.0.1.dev9/tinybird/tb/modules/create.py +0 -214
  19. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/setup.cfg +0 -0
  20. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/__cli__.py +0 -0
  21. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/ch_utils/constants.py +0 -0
  22. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/ch_utils/engine.py +0 -0
  23. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/check_pypi.py +0 -0
  24. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/client.py +0 -0
  25. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/config.py +0 -0
  26. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/connectors.py +0 -0
  27. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/context.py +0 -0
  28. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/datafile.py +0 -0
  29. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/datatypes.py +0 -0
  30. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/git_settings.py +0 -0
  31. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/sql.py +0 -0
  32. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/sql_template.py +0 -0
  33. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/sql_template_fmt.py +0 -0
  34. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/sql_toolset.py +0 -0
  35. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/syncasync.py +0 -0
  36. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/cli.py +0 -0
  37. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/auth.py +0 -0
  38. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/branch.py +0 -0
  39. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/config.py +0 -0
  40. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/connection.py +0 -0
  41. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/datafile/build_common.py +0 -0
  42. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/datafile/build_datasource.py +0 -0
  43. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/datafile/diff.py +0 -0
  44. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/datafile/exceptions.py +0 -0
  45. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/datafile/format_common.py +0 -0
  46. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/datafile/format_datasource.py +0 -0
  47. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/datafile/format_pipe.py +0 -0
  48. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/datafile/parse_datasource.py +0 -0
  49. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/datafile/parse_pipe.py +0 -0
  50. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/datafile/pipe_checker.py +0 -0
  51. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/datafile/pull.py +0 -0
  52. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/datasource.py +0 -0
  53. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/exceptions.py +0 -0
  54. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/fmt.py +0 -0
  55. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/job.py +0 -0
  56. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/pipe.py +0 -0
  57. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/regions.py +0 -0
  58. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/table.py +0 -0
  59. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/tag.py +0 -0
  60. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/telemetry.py +0 -0
  61. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/test.py +0 -0
  62. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/tinyunit/tinyunit.py +0 -0
  63. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/tinyunit/tinyunit_lib.py +0 -0
  64. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/token.py +0 -0
  65. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/workspace.py +0 -0
  66. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb/modules/workspace_members.py +0 -0
  67. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb_cli.py +0 -0
  68. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb_cli_modules/auth.py +0 -0
  69. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb_cli_modules/branch.py +0 -0
  70. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb_cli_modules/cicd.py +0 -0
  71. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb_cli_modules/cli.py +0 -0
  72. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb_cli_modules/common.py +0 -0
  73. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb_cli_modules/config.py +0 -0
  74. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb_cli_modules/connection.py +0 -0
  75. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb_cli_modules/datasource.py +0 -0
  76. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb_cli_modules/exceptions.py +0 -0
  77. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb_cli_modules/fmt.py +0 -0
  78. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb_cli_modules/job.py +0 -0
  79. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb_cli_modules/pipe.py +0 -0
  80. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb_cli_modules/regions.py +0 -0
  81. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb_cli_modules/tag.py +0 -0
  82. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb_cli_modules/telemetry.py +0 -0
  83. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb_cli_modules/test.py +0 -0
  84. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  85. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
  86. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb_cli_modules/workspace.py +0 -0
  87. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  88. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird/tornado_template.py +0 -0
  89. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird.egg-info/dependency_links.txt +0 -0
  90. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird.egg-info/entry_points.txt +0 -0
  91. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird.egg-info/requires.txt +0 -0
  92. {tinybird-0.0.1.dev9 → tinybird-0.0.1.dev10}/tinybird.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tinybird
3
- Version: 0.0.1.dev9
3
+ Version: 0.0.1.dev10
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -792,7 +792,7 @@ Ready? """
792
792
  info_diff_resources_for_git_init = info_message(
793
793
  "** Checking diffs between remote Workspace and local. Hint: use 'tb diff' to check if your Data Project and Workspace synced"
794
794
  )
795
- info_cicd_file_generated = info_message("** File {file_path} generated for CI/CD")
795
+ info_cicd_file_generated = info_message(" {file_path}")
796
796
  info_available_git_providers = info_message("** List of available providers:")
797
797
  info_git_release_init_without_diffs = info_message("** No diffs detected for '{workspace}'")
798
798
  info_deployment_detecting_changes_header = info_message("\n** Detecting changes from last commit ...")
@@ -1,7 +1,9 @@
1
1
  import asyncio
2
+ import cmd
2
3
  import os
3
4
  import random
4
5
  import subprocess
6
+ import sys
5
7
  import threading
6
8
  import time
7
9
  from pathlib import Path
@@ -16,38 +18,80 @@ import tinybird.context as context
16
18
  from tinybird.client import TinyB
17
19
  from tinybird.config import FeatureFlags
18
20
  from tinybird.feedback_manager import FeedbackManager, bcolors
19
- from tinybird.syncasync import sync_to_async
20
21
  from tinybird.tb.modules.cli import cli
21
- from tinybird.tb.modules.common import (
22
- coro,
23
- )
22
+ from tinybird.tb.modules.common import coro, push_data
24
23
  from tinybird.tb.modules.datafile.build import folder_build
25
- from tinybird.tb.modules.datafile.common import get_project_filenames, has_internal_datafiles
24
+ from tinybird.tb.modules.datafile.common import get_project_filenames, get_project_fixtures, has_internal_datafiles
26
25
  from tinybird.tb.modules.datafile.exceptions import ParseException
26
+ from tinybird.tb.modules.datafile.fixture import build_fixture_name, get_fixture_dir
27
27
  from tinybird.tb.modules.datafile.parse_datasource import parse_datasource
28
28
  from tinybird.tb.modules.datafile.parse_pipe import parse_pipe
29
29
  from tinybird.tb.modules.local import get_tinybird_local_client
30
30
  from tinybird.tb.modules.table import format_table
31
31
 
32
32
 
33
+ class BuildShell(cmd.Cmd):
34
+ prompt = "\n\001\033[1;32m\002TB > \001\033[0m\002"
35
+
36
+ def __init__(self, folder: str):
37
+ super().__init__()
38
+ self.folder = folder
39
+
40
+ def do_exit(self, arg):
41
+ sys.exit(0)
42
+
43
+ def do_quit(self, arg):
44
+ sys.exit(0)
45
+
46
+ def default(self, argline):
47
+ click.echo("")
48
+ if argline.startswith("tb build"):
49
+ click.echo(FeedbackManager.error(message="Build command is already running"))
50
+ else:
51
+ arg_stripped = argline.strip().lower()
52
+ if not arg_stripped:
53
+ return
54
+ if arg_stripped.startswith("tb"):
55
+ extra_args = f" --folder {self.folder}" if arg_stripped.startswith("tb mock") else ""
56
+ subprocess.run(arg_stripped + extra_args, shell=True, text=True)
57
+ elif arg_stripped.startswith("with") or arg_stripped.startswith("select"):
58
+ subprocess.run(f'tb sql "{arg_stripped}"', shell=True, text=True)
59
+ elif arg_stripped.startswith("mock "):
60
+ subprocess.run(f"tb {arg_stripped} --folder {self.folder}", shell=True, text=True)
61
+ else:
62
+ click.echo(FeedbackManager.error(message="Invalid command"))
63
+
64
+ def reprint_prompt(self):
65
+ self.stdout.write(self.prompt)
66
+ self.stdout.flush()
67
+
68
+
33
69
  class FileChangeHandler(FileSystemEventHandler):
34
70
  def __init__(self, filenames: List[str], process: Callable[[List[str]], None]):
35
71
  self.filenames = filenames
36
72
  self.process = process
37
73
 
38
74
  def on_modified(self, event: Any) -> None:
39
- if not event.is_directory and any(event.src_path.endswith(ext) for ext in [".datasource", ".pipe"]):
75
+ if not event.is_directory and any(event.src_path.endswith(ext) for ext in [".datasource", ".pipe", ".ndjson"]):
40
76
  filename = event.src_path.split("/")[-1]
41
- click.echo(FeedbackManager.highlight(message=f"\n⟲ Changes detected in {filename}\n"))
77
+ click.echo(FeedbackManager.highlight(message=f"\n\n⟲ Changes detected in {filename}\n"))
42
78
  try:
43
79
  self.process([event.src_path])
44
80
  except Exception as e:
45
81
  click.echo(FeedbackManager.error_exception(error=e))
46
82
 
83
+ def on_created(self, event: Any) -> None:
84
+ click.echo(FeedbackManager.highlight(message=f"\n\n⟲ Changes detected in {event.src_path}\n"))
85
+ try:
86
+ self.process([event.src_path])
87
+ except Exception as e:
88
+ click.echo(FeedbackManager.error_exception(error=e))
89
+
47
90
 
48
91
  def watch_files(
49
92
  filenames: List[str],
50
93
  process: Union[Callable[[List[str]], None], Callable[[List[str]], Awaitable[None]]],
94
+ shell: BuildShell,
51
95
  ) -> None:
52
96
  # Handle both sync and async process functions
53
97
  async def process_wrapper(files: List[str]) -> None:
@@ -61,8 +105,9 @@ def watch_files(
61
105
  elapsed_time = time_end - time_start
62
106
  click.echo(
63
107
  FeedbackManager.success(message="\n✓ ")
64
- + FeedbackManager.gray(message=f"Rebuild completed in {elapsed_time:.1f}s\n")
108
+ + FeedbackManager.gray(message=f"Rebuild completed in {elapsed_time:.1f}s")
65
109
  )
110
+ shell.reprint_prompt()
66
111
 
67
112
  event_handler = FileChangeHandler(filenames, lambda f: asyncio.run(process_wrapper(f)))
68
113
  observer = Observer()
@@ -113,7 +158,9 @@ async def build(
113
158
  ignore_sql_errors = FeatureFlags.ignore_sql_errors()
114
159
  context.disable_template_security_validation.set(True)
115
160
  is_internal = has_internal_datafiles(folder)
116
- tb_client = get_tinybird_local_client()
161
+
162
+ folder_path = os.path.abspath(folder)
163
+ tb_client = await get_tinybird_local_client(folder_path)
117
164
 
118
165
  def check_filenames(filenames: List[str]):
119
166
  parser_matrix = {".pipe": parse_pipe, ".datasource": parse_datasource}
@@ -134,29 +181,53 @@ async def build(
134
181
  parser(filename)
135
182
 
136
183
  async def process(filenames: List[str], watch: bool = False, only_pipes: bool = False):
137
- check_filenames(filenames=filenames)
138
- await folder_build(
139
- tb_client,
140
- filenames,
141
- ignore_sql_errors=ignore_sql_errors,
142
- is_internal=is_internal,
143
- only_pipes=only_pipes,
144
- )
184
+ datafiles = [f for f in filenames if f.endswith(".datasource") or f.endswith(".pipe")]
185
+ if len(datafiles) > 0:
186
+ check_filenames(filenames=datafiles)
187
+ await folder_build(
188
+ tb_client,
189
+ filenames=datafiles,
190
+ ignore_sql_errors=ignore_sql_errors,
191
+ is_internal=is_internal,
192
+ only_pipes=only_pipes,
193
+ )
194
+
195
+ filename = filenames[0]
196
+ if filename.endswith(".ndjson"):
197
+ fixture_path = Path(filename)
198
+ name = "_".join(fixture_path.stem.split("_")[:-1])
199
+ ds_path = Path(folder) / "datasources" / f"{name}.datasource"
200
+ if ds_path.exists():
201
+ await append_datasource({}, tb_client, name, str(fixture_path), silent=True)
145
202
 
146
203
  if watch:
147
- filename = filenames[0]
148
- if filename.endswith(".pipe"):
149
- await build_and_print_pipe(tb_client, filename)
150
-
151
- filenames = get_project_filenames(folder)
204
+ if filename.endswith(".datasource"):
205
+ ds_path = Path(filename)
206
+ name = build_fixture_name(filename, ds_path.stem, ds_path.read_text())
207
+ fixture_path = get_fixture_dir() / f"{name}.ndjson"
208
+ if fixture_path.exists():
209
+ await append_datasource({}, tb_client, ds_path.stem, str(fixture_path), silent=True)
210
+ if not filename.endswith(".ndjson"):
211
+ await build_and_print_resource(tb_client, filename)
212
+
213
+ datafiles = get_project_filenames(folder)
214
+ fixtures = get_project_fixtures(folder)
215
+ filenames = datafiles + fixtures
152
216
 
153
217
  async def build_once(filenames: List[str]):
154
218
  try:
155
- click.echo("⚡ Building project...")
219
+ click.echo("⚡ Building project...\n")
156
220
  time_start = time.time()
157
221
  await process(filenames=filenames, watch=False, only_pipes=skip_datasources)
158
222
  time_end = time.time()
159
223
  elapsed_time = time_end - time_start
224
+ for filename in filenames:
225
+ if filename.endswith(".datasource"):
226
+ ds_path = Path(filename)
227
+ name = build_fixture_name(filename, ds_path.stem, ds_path.read_text())
228
+ fixture_path = get_fixture_dir() / f"{name}.ndjson"
229
+ if fixture_path.exists():
230
+ await append_datasource({}, tb_client, ds_path.stem, str(fixture_path), silent=True)
160
231
  click.echo(FeedbackManager.success(message=f"\n✓ Build completed in {elapsed_time:.1f}s\n"))
161
232
  except Exception as e:
162
233
  click.echo(FeedbackManager.error(message=str(e)))
@@ -164,31 +235,21 @@ async def build(
164
235
  await build_once(filenames)
165
236
 
166
237
  if watch:
167
- click.echo(FeedbackManager.highlight(message="◎ Watching for changes...\n"))
168
- watcher_thread = threading.Thread(target=watch_files, args=(filenames, process), daemon=True)
238
+ shell = BuildShell(folder=folder)
239
+ click.echo(FeedbackManager.highlight(message="◎ Watching for changes..."))
240
+ watcher_thread = threading.Thread(target=watch_files, args=(filenames, process, shell), daemon=True)
169
241
  watcher_thread.start()
242
+ shell.cmdloop()
170
243
 
171
- # Main CLI loop
172
- while True:
173
- user_input = click.prompt("", prompt_suffix="")
174
- if user_input.lower() == "exit":
175
- break
176
244
 
177
- if "tb build" in user_input:
178
- click.echo(FeedbackManager.error(message="Build command is already running"))
179
- else:
180
- # Process the user command
181
- await sync_to_async(subprocess.run, thread_sensitive=True)(user_input, shell=True, text=True)
182
-
183
- click.echo(FeedbackManager.highlight(message="\n◎ Watching for changes...\n"))
184
-
185
-
186
- async def build_and_print_pipe(tb_client: TinyB, filename: str):
245
+ async def build_and_print_resource(tb_client: TinyB, filename: str):
187
246
  rebuild_colors = [bcolors.FAIL, bcolors.OKBLUE, bcolors.WARNING, bcolors.OKGREEN, bcolors.HEADER]
188
247
  rebuild_index = random.randint(0, len(rebuild_colors) - 1)
189
248
  rebuild_color = rebuild_colors[rebuild_index % len(rebuild_colors)]
190
- pipe_name = Path(filename).stem
191
- res = await tb_client.query(f"SELECT * FROM {pipe_name} FORMAT JSON", pipeline=pipe_name)
249
+ resource_path = Path(filename)
250
+ name = resource_path.stem
251
+ pipeline = name if filename.endswith(".pipe") else None
252
+ res = await tb_client.query(f"SELECT * FROM {name} FORMAT JSON", pipeline=pipeline)
192
253
  data = []
193
254
  limit = 5
194
255
  for d in res["data"][:5]:
@@ -197,7 +258,6 @@ async def build_and_print_pipe(tb_client: TinyB, filename: str):
197
258
  row_count = res.get("rows", 0)
198
259
  stats = res.get("statistics", {})
199
260
  elapsed = stats.get("elapsed", 0)
200
- node_name = "endpoint"
201
261
  cols = len(meta)
202
262
  try:
203
263
 
@@ -207,7 +267,7 @@ async def build_and_print_pipe(tb_client: TinyB, filename: str):
207
267
  table = format_table(data, meta)
208
268
  colored_char = print_message("│", rebuild_color)
209
269
  table_with_marker = "\n".join(f"{colored_char} {line}" for line in table.split("\n"))
210
- click.echo(f"\n{colored_char} {print_message('⚡', rebuild_color)} Running {pipe_name} → {node_name}")
270
+ click.echo(f"\n{colored_char} {print_message('⚡', rebuild_color)} Running {name}")
211
271
  click.echo(colored_char)
212
272
  click.echo(table_with_marker)
213
273
  click.echo(colored_char)
@@ -225,3 +285,24 @@ async def build_and_print_pipe(tb_client: TinyB, filename: str):
225
285
  click.echo("------------")
226
286
  else:
227
287
  raise exc
288
+
289
+
290
+ async def append_datasource(
291
+ ctx: click.Context,
292
+ tb_client: TinyB,
293
+ datasource_name: str,
294
+ url: str,
295
+ silent: bool = False,
296
+ ):
297
+ await push_data(
298
+ ctx,
299
+ tb_client,
300
+ datasource_name,
301
+ url,
302
+ connector=None,
303
+ sql=None,
304
+ mode="append",
305
+ ignore_empty=False,
306
+ concurrency=1,
307
+ silent=silent,
308
+ )
@@ -240,7 +240,6 @@ class GitLabCICDGenerator(CICDGeneratorBase):
240
240
  template=GITLAB_CI_YML,
241
241
  file_name="tinybird-ci.yml",
242
242
  dir_path=".gitlab/tinybird",
243
- warning_message="Make sure to import the file in your .gitlab-ci.yml file, e.g., `include: '.gitlab/tinybird/*.yml'`.",
244
243
  ),
245
244
  ]
246
245
 
@@ -158,9 +158,6 @@ async def cli(
158
158
 
159
159
  latest_version = await CheckPypi().get_latest_version()
160
160
 
161
- if "x.y.z" in CURRENT_VERSION:
162
- click.echo(FeedbackManager.warning_development_cli())
163
-
164
161
  if "x.y.z" not in CURRENT_VERSION and latest_version != CURRENT_VERSION:
165
162
  click.echo(FeedbackManager.warning_update_version(latest_version=latest_version))
166
163
  click.echo(FeedbackManager.warning_current_version(current_version=CURRENT_VERSION))
@@ -11,6 +11,7 @@ import os
11
11
  import re
12
12
  import socket
13
13
  import sys
14
+ import time
14
15
  import uuid
15
16
  from contextlib import closing
16
17
  from copy import deepcopy
@@ -166,12 +167,12 @@ def generate_datafile(
166
167
  data: Optional[bytes],
167
168
  force: Optional[bool] = False,
168
169
  _format: Optional[str] = "csv",
169
- parent_dir: Optional[str] = None,
170
+ folder: Optional[str] = None,
170
171
  ):
171
172
  p = Path(filename)
172
173
  base = Path("datasources")
173
- if parent_dir:
174
- base = Path(parent_dir) / base
174
+ if folder:
175
+ base = Path(folder) / base
175
176
  datasource_name = normalize_datasource_name(p.stem)
176
177
  if not base.exists():
177
178
  base = Path()
@@ -179,7 +180,7 @@ def generate_datafile(
179
180
  if not f.exists() or force:
180
181
  with open(f"{f}", "w") as ds_file:
181
182
  ds_file.write(datafile)
182
- click.echo(FeedbackManager.info_file_created(file=f))
183
+ click.echo(FeedbackManager.info_file_created(file=str(f)))
183
184
 
184
185
  if data and (base / "fixtures").exists():
185
186
  # Generating a fixture for Parquet files is not so trivial, since Parquet format
@@ -1078,6 +1079,7 @@ async def push_data(
1078
1079
  replace_options=None,
1079
1080
  ignore_empty: bool = False,
1080
1081
  concurrency: int = 1,
1082
+ silent: bool = False,
1081
1083
  ):
1082
1084
  if url and type(url) is tuple:
1083
1085
  url = url[0]
@@ -1088,7 +1090,8 @@ async def push_data(
1088
1090
  raise CLIException(FeedbackManager.error_connector_not_configured(connector=connector))
1089
1091
  else:
1090
1092
  _connector: "Connector" = ctx.obj[connector]
1091
- click.echo(FeedbackManager.info_starting_export_process(connector=connector))
1093
+ if not silent:
1094
+ click.echo(FeedbackManager.info_starting_export_process(connector=connector))
1092
1095
  try:
1093
1096
  url = _connector.export_to_gcs(sql, datasource_name, mode)
1094
1097
  except ConnectorNothingToLoad as e:
@@ -1116,7 +1119,8 @@ async def push_data(
1116
1119
  cb.First = True # type: ignore[attr-defined]
1117
1120
  cb.prev_done = 0 # type: ignore[attr-defined]
1118
1121
 
1119
- click.echo(FeedbackManager.gray(message=f"\nImporting data to {datasource_name} Data Source..."))
1122
+ if not silent:
1123
+ click.echo(FeedbackManager.gray(message=f"\nImporting data to {datasource_name}..."))
1120
1124
 
1121
1125
  if isinstance(url, list):
1122
1126
  urls = url
@@ -1187,15 +1191,14 @@ async def push_data(
1187
1191
  except Exception as e:
1188
1192
  raise CLIException(FeedbackManager.error_exception(error=e))
1189
1193
  else:
1190
- if mode == "append" and parser and parser != "clickhouse":
1191
- click.echo(FeedbackManager.success_appended_rows(appended_rows=appended_rows))
1194
+ if not silent:
1195
+ if mode == "append" and parser and parser != "clickhouse":
1196
+ click.echo(FeedbackManager.success_appended_rows(appended_rows=appended_rows))
1192
1197
 
1193
- if mode == "replace":
1194
- click.echo(FeedbackManager.success_replaced_datasource(datasource=datasource_name))
1195
- else:
1196
- click.echo(FeedbackManager.highlight(message="» 2.57m rows x 9 cols in 852.04ms"))
1198
+ if mode == "replace":
1199
+ click.echo(FeedbackManager.success_replaced_datasource(datasource=datasource_name))
1197
1200
 
1198
- click.echo(FeedbackManager.success_progress_blocks())
1201
+ click.echo(FeedbackManager.success_progress_blocks())
1199
1202
 
1200
1203
  finally:
1201
1204
  try:
@@ -2108,3 +2111,22 @@ def get_ca_pem_content(ca_pem: Optional[str], filename: Optional[str] = None) ->
2108
2111
 
2109
2112
  requests_get = sync_to_async(requests.get, thread_sensitive=False)
2110
2113
  requests_delete = sync_to_async(requests.delete, thread_sensitive=False)
2114
+
2115
+
2116
+ def format_data_to_ndjson(data: List[Dict[str, Any]]) -> str:
2117
+ return "\n".join([json.dumps(row) for row in data])
2118
+
2119
+
2120
+ async def send_batch_events(
2121
+ client: TinyB, datasource_name: str, data: List[Dict[str, Any]], batch_size: int = 10
2122
+ ) -> None:
2123
+ rows = len(data)
2124
+ time_start = time.time()
2125
+ for i in range(0, rows, batch_size):
2126
+ batch = data[i : i + batch_size]
2127
+ ndjson_data = format_data_to_ndjson(batch)
2128
+ await client.datasource_events(datasource_name, ndjson_data)
2129
+ time_end = time.time()
2130
+ elapsed_time = time_end - time_start
2131
+ cols = len(data[0].keys()) if len(data) > 0 else 0
2132
+ click.echo(FeedbackManager.highlight(message=f"» {rows} rows x {cols} cols in {elapsed_time:.1f}s"))
@@ -0,0 +1,176 @@
1
+ import os
2
+ import subprocess
3
+ from os import getcwd
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ import click
8
+ from click import Context
9
+
10
+ from tinybird.client import TinyB
11
+ from tinybird.feedback_manager import FeedbackManager
12
+ from tinybird.tb.modules.cicd import init_cicd
13
+ from tinybird.tb.modules.cli import cli
14
+ from tinybird.tb.modules.common import _generate_datafile, coro, generate_datafile
15
+ from tinybird.tb.modules.config import CLIConfig
16
+ from tinybird.tb.modules.datafile.fixture import build_fixture_name, persist_fixture
17
+ from tinybird.tb.modules.exceptions import CLIException
18
+ from tinybird.tb.modules.llm import LLM
19
+ from tinybird.tb.modules.local import get_tinybird_local_client
20
+
21
+
22
+ @cli.command()
23
+ @click.option(
24
+ "--data",
25
+ type=click.Path(exists=True),
26
+ default=None,
27
+ help="Initial data to be used to create the project",
28
+ )
29
+ @click.option(
30
+ "--prompt",
31
+ type=str,
32
+ default=None,
33
+ help="Prompt to be used to create the project",
34
+ )
35
+ @click.option(
36
+ "--folder",
37
+ default=None,
38
+ type=click.Path(exists=True, file_okay=False),
39
+ help="Folder where datafiles will be placed",
40
+ )
41
+ @click.option("--rows", type=int, default=10, help="Number of events to send")
42
+ @click.pass_context
43
+ @coro
44
+ async def create(
45
+ ctx: Context,
46
+ data: Optional[str],
47
+ prompt: Optional[str],
48
+ folder: Optional[str],
49
+ rows: int,
50
+ ) -> None:
51
+ """Initialize a new project."""
52
+ folder = folder or getcwd()
53
+ try:
54
+ tb_client = await get_tinybird_local_client(os.path.abspath(folder))
55
+ click.echo(FeedbackManager.gray(message="Creating new project structure..."))
56
+ await project_create(tb_client, data, prompt, folder)
57
+ click.echo(FeedbackManager.success(message="✓ Scaffolding completed!\n"))
58
+
59
+ click.echo(FeedbackManager.gray(message="\nCreating CI/CD files for GitHub and GitLab..."))
60
+ init_git(folder)
61
+ await init_cicd(data_project_dir=os.path.relpath(folder))
62
+ click.echo(FeedbackManager.success(message="✓ Done!\n"))
63
+
64
+ click.echo(FeedbackManager.gray(message="Building fixtures..."))
65
+
66
+ if data:
67
+ ds_name = os.path.basename(data.split(".")[0])
68
+ data_content = Path(data).read_text()
69
+ datasource_path = Path(folder) / "datasources" / f"{ds_name}.datasource"
70
+ fixture_name = build_fixture_name(datasource_path.absolute(), ds_name, datasource_path.read_text())
71
+ click.echo(FeedbackManager.info(message=f"✓ /fixtures/{ds_name}"))
72
+ persist_fixture(fixture_name, data_content)
73
+ elif prompt:
74
+ datasource_files = [f for f in os.listdir(Path(folder) / "datasources") if f.endswith(".datasource")]
75
+ for datasource_file in datasource_files:
76
+ datasource_path = Path(folder) / "datasources" / datasource_file
77
+ llm_config = CLIConfig.get_llm_config()
78
+ llm = LLM(key=llm_config["api_key"])
79
+ datasource_name = datasource_path.stem
80
+ datasource_content = datasource_path.read_text()
81
+ has_json_path = "`json:" in datasource_content
82
+ if has_json_path:
83
+ sql = await llm.generate_sql_sample_data(tb_client, datasource_name, datasource_content, rows)
84
+ result = await tb_client.query(f"{sql} FORMAT JSON")
85
+ data = result.get("data", [])
86
+ fixture_name = build_fixture_name(datasource_path.absolute(), datasource_name, datasource_content)
87
+ persist_fixture(fixture_name, data)
88
+ click.echo(FeedbackManager.info(message=f"✓ /fixtures/{datasource_name}"))
89
+
90
+ click.echo(FeedbackManager.success(message="✓ Done!\n"))
91
+
92
+ click.echo(FeedbackManager.success(message="✓ Tinybird local is ready!"))
93
+ except Exception as e:
94
+ click.echo(FeedbackManager.error(message=f"Error: {str(e)}"))
95
+
96
+
97
+ async def project_create(
98
+ client: TinyB,
99
+ data: Optional[str],
100
+ prompt: Optional[str],
101
+ folder: str,
102
+ ):
103
+ project_paths = ["datasources", "endpoints", "materializations", "copies", "sinks", "fixtures"]
104
+ force = True
105
+ for x in project_paths:
106
+ try:
107
+ f = Path(folder) / x
108
+ f.mkdir()
109
+ except FileExistsError:
110
+ pass
111
+ click.echo(FeedbackManager.info_path_created(path=x))
112
+
113
+ if data:
114
+ path = Path(folder) / data
115
+ format = path.suffix.lstrip(".")
116
+ try:
117
+ await _generate_datafile(str(path), client, format=format, force=force)
118
+ except Exception as e:
119
+ click.echo(FeedbackManager.error(message=f"Ersssssror: {str(e)}"))
120
+ name = data.split(".")[0]
121
+ generate_pipe_file(
122
+ f"{name}_endpoint",
123
+ f"""
124
+ NODE endpoint
125
+ SQL >
126
+ SELECT * from {name}
127
+ TYPE ENDPOINT
128
+ """,
129
+ folder,
130
+ )
131
+ elif prompt:
132
+ try:
133
+ llm_config = CLIConfig.get_llm_config()
134
+ llm = LLM(key=llm_config["api_key"])
135
+ result = await llm.create_project(prompt)
136
+ for ds in result.datasources:
137
+ content = ds.content.replace("```", "")
138
+ generate_datafile(
139
+ content, filename=f"{ds.name}.datasource", data=None, _format="ndjson", force=force, folder=folder
140
+ )
141
+
142
+ for pipe in result.pipes:
143
+ content = pipe.content.replace("```", "")
144
+ generate_pipe_file(pipe.name, content, folder)
145
+ except Exception as e:
146
+ click.echo(FeedbackManager.error(message=f"Error: {str(e)}"))
147
+
148
+
149
+ def init_git(folder: str):
150
+ try:
151
+ path = Path(folder)
152
+ gitignore_file = path / ".gitignore"
153
+ git_folder = path / ".git"
154
+ if not git_folder.exists():
155
+ subprocess.run(["git", "init"], cwd=path, check=True, capture_output=True)
156
+
157
+ if gitignore_file.exists():
158
+ content = gitignore_file.read_text()
159
+ if ".tinyb" not in content:
160
+ gitignore_file.write_text(content + "\n.tinyb\n")
161
+ else:
162
+ gitignore_file.write_text(".tinyb\n")
163
+
164
+ click.echo(FeedbackManager.info_file_created(file=".gitignore"))
165
+ except Exception as e:
166
+ raise CLIException(f"Error initializing Git: {e}")
167
+
168
+
169
+ def generate_pipe_file(name: str, content: str, folder: str):
170
+ base = Path(folder) / "endpoints"
171
+ if not base.exists():
172
+ base = Path()
173
+ f = base / (f"{name}.pipe")
174
+ with open(f"{f}", "w") as file:
175
+ file.write(content)
176
+ click.echo(FeedbackManager.info_file_created(file=f.relative_to(folder)))
@@ -32,7 +32,6 @@ from tinybird.tb.modules.datafile.common import (
32
32
  INTERNAL_TABLES,
33
33
  ON_DEMAND,
34
34
  PREVIEW_CONNECTOR_SERVICES,
35
- TB_LOCAL_WORKSPACE_NAME,
36
35
  CopyModes,
37
36
  CopyParameters,
38
37
  DataFileExtensions,
@@ -58,6 +57,7 @@ async def folder_build(
58
57
  only_pipes: bool = False,
59
58
  is_vendor: bool = False,
60
59
  current_ws: Optional[Dict[str, Any]] = None,
60
+ local_ws: Optional[Dict[str, Any]] = None,
61
61
  workspaces: Optional[List[Dict[str, Any]]] = None,
62
62
  ):
63
63
  if only_pipes:
@@ -117,8 +117,9 @@ async def folder_build(
117
117
  user_client.token = user_token
118
118
 
119
119
  vendor_workspaces = []
120
- user_workspaces = await user_client.user_workspaces()
120
+
121
121
  if vendor_path.exists() and not is_vendor:
122
+ user_workspaces = await user_client.user_workspaces()
122
123
  for x in vendor_path.iterdir():
123
124
  if x.is_dir() and x.name not in existing_workspaces:
124
125
  if user_token:
@@ -133,15 +134,23 @@ async def folder_build(
133
134
  workspace_lib_paths.append((x.name, x))
134
135
 
135
136
  workspaces: List[Dict[str, Any]] = (await user_client.user_workspaces()).get("workspaces", [])
136
- local_ws = next((ws for ws in workspaces if ws["name"] == TB_LOCAL_WORKSPACE_NAME), {})
137
+
138
+ if not is_vendor:
139
+ local_workspace = await tb_client.workspace_info()
140
+ local_ws_id = local_workspace.get("id")
141
+ local_ws = next((ws for ws in workspaces if ws["id"] == local_ws_id), {})
142
+
137
143
  current_ws: Dict[str, Any] = current_ws or local_ws
144
+
138
145
  for vendor_ws in [ws for ws in workspaces if ws["name"] in [ws["name"] for ws in vendor_workspaces]]:
139
146
  ws_client = deepcopy(tb_client)
140
147
  ws_client.token = vendor_ws["token"]
141
148
  shared_ws_path = Path(folder) / "vendor" / vendor_ws["name"]
142
149
 
143
150
  if shared_ws_path.exists() and not is_vendor:
144
- await folder_build(ws_client, folder=shared_ws_path.as_posix(), is_vendor=True, current_ws=vendor_ws)
151
+ await folder_build(
152
+ ws_client, folder=shared_ws_path.as_posix(), is_vendor=True, current_ws=vendor_ws, local_ws=local_ws
153
+ )
145
154
 
146
155
  datasources: List[Dict[str, Any]] = await tb_client.datasources()
147
156
  pipes: List[Dict[str, Any]] = await tb_client.pipes(dependencies=True)
@@ -256,7 +256,6 @@ async def new_pipe(
256
256
  raise click.ClickException(FeedbackManager.error_creating_pipe(error=e))
257
257
  else:
258
258
  token_name = tk["token_name"]
259
- click.echo(FeedbackManager.info_create_not_found_token(token=token_name))
260
259
  try:
261
260
  r = await tb_client.create_token(
262
261
  token_name, [f"PIPES:{tk['permissions']}:{p['name']}"], "P", p["name"]
@@ -662,6 +662,18 @@ def get_project_filenames(folder: str, with_vendor=False) -> List[str]:
662
662
  ]
663
663
  if with_vendor:
664
664
  folders.append(f"{folder}/vendor/**/**/*.datasource")
665
+
666
+ filenames: List[str] = []
667
+ for x in folders:
668
+ filenames += glob.glob(x)
669
+ return filenames
670
+
671
+
672
+ def get_project_fixtures(folder: str) -> List[str]:
673
+ folders: List[str] = [
674
+ f"{folder}/fixtures/*.ndjson",
675
+ f"{folder}/fixtures/*.csv",
676
+ ]
665
677
  filenames: List[str] = []
666
678
  for x in folders:
667
679
  filenames += glob.glob(x)