tinybird 0.0.1.dev16__tar.gz → 0.0.1.dev18__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 (96) hide show
  1. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/PKG-INFO +1 -1
  2. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/datafile.py +2 -0
  3. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/cli.py +0 -1
  4. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/build.py +50 -33
  5. tinybird-0.0.1.dev18/tinybird/tb/modules/build_shell.py +326 -0
  6. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/cicd.py +9 -89
  7. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/cli.py +4 -89
  8. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/common.py +8 -206
  9. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/config.py +0 -10
  10. tinybird-0.0.1.dev18/tinybird/tb/modules/create.py +242 -0
  11. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/datafile/build_pipe.py +1 -1
  12. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/datafile/common.py +223 -247
  13. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/datafile/parse_datasource.py +8 -0
  14. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/datafile/parse_pipe.py +10 -1
  15. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/datasource.py +0 -10
  16. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/llm.py +4 -3
  17. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/local_common.py +1 -1
  18. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/login.py +1 -1
  19. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/mock.py +14 -12
  20. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/pipe.py +0 -4
  21. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/test.py +90 -17
  22. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird.egg-info/PKG-INFO +1 -1
  23. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird.egg-info/SOURCES.txt +0 -1
  24. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird.egg-info/requires.txt +1 -0
  25. tinybird-0.0.1.dev16/tinybird/tb/modules/branch.py +0 -1023
  26. tinybird-0.0.1.dev16/tinybird/tb/modules/build_shell.py +0 -133
  27. tinybird-0.0.1.dev16/tinybird/tb/modules/create.py +0 -411
  28. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/setup.cfg +0 -0
  29. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/__cli__.py +0 -0
  30. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/ch_utils/constants.py +0 -0
  31. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/ch_utils/engine.py +0 -0
  32. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/check_pypi.py +0 -0
  33. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/client.py +0 -0
  34. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/config.py +0 -0
  35. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/connectors.py +0 -0
  36. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/context.py +0 -0
  37. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/datatypes.py +0 -0
  38. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/feedback_manager.py +0 -0
  39. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/git_settings.py +0 -0
  40. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/prompts.py +0 -0
  41. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/sql.py +0 -0
  42. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/sql_template.py +0 -0
  43. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/sql_template_fmt.py +0 -0
  44. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/sql_toolset.py +0 -0
  45. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/syncasync.py +0 -0
  46. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/auth.py +0 -0
  47. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/connection.py +0 -0
  48. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/datafile/build.py +0 -0
  49. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/datafile/build_common.py +0 -0
  50. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/datafile/build_datasource.py +0 -0
  51. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/datafile/diff.py +0 -0
  52. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/datafile/exceptions.py +0 -0
  53. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/datafile/fixture.py +0 -0
  54. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/datafile/format_common.py +0 -0
  55. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/datafile/format_datasource.py +0 -0
  56. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/datafile/format_pipe.py +0 -0
  57. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/datafile/pipe_checker.py +0 -0
  58. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/datafile/pull.py +0 -0
  59. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/exceptions.py +0 -0
  60. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/fmt.py +0 -0
  61. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/job.py +0 -0
  62. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/local.py +0 -0
  63. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/regions.py +0 -0
  64. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/table.py +0 -0
  65. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/tag.py +0 -0
  66. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/telemetry.py +0 -0
  67. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/tinyunit/tinyunit.py +0 -0
  68. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/tinyunit/tinyunit_lib.py +0 -0
  69. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/token.py +0 -0
  70. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/workspace.py +0 -0
  71. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb/modules/workspace_members.py +0 -0
  72. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb_cli.py +0 -0
  73. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb_cli_modules/auth.py +0 -0
  74. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb_cli_modules/branch.py +0 -0
  75. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb_cli_modules/cicd.py +0 -0
  76. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb_cli_modules/cli.py +0 -0
  77. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb_cli_modules/common.py +0 -0
  78. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb_cli_modules/config.py +0 -0
  79. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb_cli_modules/connection.py +0 -0
  80. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb_cli_modules/datasource.py +0 -0
  81. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb_cli_modules/exceptions.py +0 -0
  82. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb_cli_modules/fmt.py +0 -0
  83. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb_cli_modules/job.py +0 -0
  84. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb_cli_modules/pipe.py +0 -0
  85. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb_cli_modules/regions.py +0 -0
  86. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb_cli_modules/tag.py +0 -0
  87. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb_cli_modules/telemetry.py +0 -0
  88. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb_cli_modules/test.py +0 -0
  89. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  90. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
  91. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb_cli_modules/workspace.py +0 -0
  92. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  93. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird/tornado_template.py +0 -0
  94. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird.egg-info/dependency_links.txt +0 -0
  95. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/tinybird.egg-info/entry_points.txt +0 -0
  96. {tinybird-0.0.1.dev16 → tinybird-0.0.1.dev18}/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.dev16
3
+ Version: 0.0.1.dev18
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -212,6 +212,7 @@ class ExportReplacements:
212
212
  ("export_file_template", "file_template", None),
213
213
  ("export_format", "format", "csv"),
214
214
  ("export_compression", "compression", None),
215
+ ("export_write_strategy", "write_strategy", None),
215
216
  ("export_strategy", "strategy", "@new"),
216
217
  ("export_kafka_topic", "kafka_topic", None),
217
218
  ("kafka_connection_name", "connection", None),
@@ -1239,6 +1240,7 @@ def parse(
1239
1240
  "export_format": assign_var("export_format"),
1240
1241
  "export_strategy": assign_var("export_strategy"),
1241
1242
  "export_compression": assign_var("export_compression"),
1243
+ "export_write_strategy": assign_var("export_write_strategy"),
1242
1244
  "export_kafka_topic": assign_var("export_kafka_topic"),
1243
1245
  }
1244
1246
 
@@ -5,7 +5,6 @@ if sys.platform == "win32":
5
5
  asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
6
6
 
7
7
  import tinybird.tb.modules.auth
8
- import tinybird.tb.modules.branch
9
8
  import tinybird.tb.modules.build
10
9
  import tinybird.tb.modules.cli
11
10
  import tinybird.tb.modules.common
@@ -6,7 +6,6 @@ from pathlib import Path
6
6
  from typing import Any, Awaitable, Callable, List, Union
7
7
 
8
8
  import click
9
- from click import Context
10
9
  from watchdog.events import FileSystemEventHandler
11
10
  from watchdog.observers import Observer
12
11
 
@@ -16,7 +15,7 @@ from tinybird.config import FeatureFlags
16
15
  from tinybird.feedback_manager import FeedbackManager
17
16
  from tinybird.tb.modules.build_shell import BuildShell, print_table_formatted
18
17
  from tinybird.tb.modules.cli import cli
19
- from tinybird.tb.modules.common import coro, push_data
18
+ from tinybird.tb.modules.common import push_data
20
19
  from tinybird.tb.modules.datafile.build import folder_build
21
20
  from tinybird.tb.modules.datafile.common import get_project_filenames, get_project_fixtures, has_internal_datafiles
22
21
  from tinybird.tb.modules.datafile.exceptions import ParseException
@@ -27,9 +26,10 @@ from tinybird.tb.modules.local_common import get_tinybird_local_client
27
26
 
28
27
 
29
28
  class FileChangeHandler(FileSystemEventHandler):
30
- def __init__(self, filenames: List[str], process: Callable[[List[str]], None]):
29
+ def __init__(self, filenames: List[str], process: Callable[[List[str]], None], build_ok: bool):
31
30
  self.filenames = filenames
32
31
  self.process = process
32
+ self.build_ok = build_ok
33
33
 
34
34
  def on_modified(self, event: Any) -> None:
35
35
  is_not_vendor = "vendor/" not in event.src_path
@@ -41,7 +41,9 @@ class FileChangeHandler(FileSystemEventHandler):
41
41
  filename = event.src_path.split("/")[-1]
42
42
  click.echo(FeedbackManager.highlight(message=f"\n\n⟲ Changes detected in {filename}\n"))
43
43
  try:
44
- self.process([event.src_path])
44
+ to_process = [event.src_path] if self.build_ok else self.filenames
45
+ self.process(to_process)
46
+ self.build_ok = True
45
47
  except Exception as e:
46
48
  click.echo(FeedbackManager.error_exception(error=e))
47
49
 
@@ -51,6 +53,7 @@ def watch_files(
51
53
  process: Union[Callable[[List[str]], None], Callable[[List[str]], Awaitable[None]]],
52
54
  shell: BuildShell,
53
55
  folder: str,
56
+ build_ok: bool,
54
57
  ) -> None:
55
58
  # Handle both sync and async process functions
56
59
  async def process_wrapper(files: List[str]) -> None:
@@ -68,7 +71,7 @@ def watch_files(
68
71
  )
69
72
  shell.reprint_prompt()
70
73
 
71
- event_handler = FileChangeHandler(filenames, lambda f: asyncio.run(process_wrapper(f)))
74
+ event_handler = FileChangeHandler(filenames, lambda f: asyncio.run(process_wrapper(f)), build_ok)
72
75
  observer = Observer()
73
76
 
74
77
  observer.schedule(event_handler, path=folder, recursive=True)
@@ -97,10 +100,7 @@ def watch_files(
97
100
  is_flag=True,
98
101
  help="Watch for changes in the files and re-check them.",
99
102
  )
100
- @click.pass_context
101
- @coro
102
- async def build(
103
- ctx: Context,
103
+ def build(
104
104
  folder: str,
105
105
  watch: bool,
106
106
  ) -> None:
@@ -110,9 +110,8 @@ async def build(
110
110
  ignore_sql_errors = FeatureFlags.ignore_sql_errors()
111
111
  context.disable_template_security_validation.set(True)
112
112
  is_internal = has_internal_datafiles(folder)
113
-
114
113
  folder_path = os.path.abspath(folder)
115
- tb_client = await get_tinybird_local_client(folder_path)
114
+ tb_client = asyncio.run(get_tinybird_local_client(folder_path))
116
115
 
117
116
  def check_filenames(filenames: List[str]):
118
117
  parser_matrix = {".pipe": parse_pipe, ".datasource": parse_datasource}
@@ -143,30 +142,31 @@ async def build(
143
142
  is_internal=is_internal,
144
143
  watch=watch,
145
144
  )
146
-
147
- filename = filenames[0]
148
- if filename.endswith(".ndjson"):
149
- fixture_path = Path(filename)
150
- name = "_".join(fixture_path.stem.split("_")[:-1])
151
- ds_path = Path(folder) / "datasources" / f"{name}.datasource"
152
- if ds_path.exists():
153
- await append_datasource({}, tb_client, name, str(fixture_path), silent=True)
154
-
155
- if watch:
156
- if filename.endswith(".datasource"):
157
- ds_path = Path(filename)
158
- name = build_fixture_name(filename, ds_path.stem, ds_path.read_text())
159
- fixture_path = get_fixture_dir() / f"{name}.ndjson"
160
- if fixture_path.exists():
161
- await append_datasource({}, tb_client, ds_path.stem, str(fixture_path), silent=True)
162
- if not filename.endswith(".ndjson"):
163
- await build_and_print_resource(tb_client, filename)
145
+ if len(filenames) > 0:
146
+ filename = filenames[0]
147
+ if filename.endswith(".ndjson"):
148
+ fixture_path = Path(filename)
149
+ name = "_".join(fixture_path.stem.split("_")[:-1])
150
+ ds_path = Path(folder) / "datasources" / f"{name}.datasource"
151
+ if ds_path.exists():
152
+ await append_datasource({}, tb_client, name, str(fixture_path), silent=True)
153
+
154
+ if watch:
155
+ if filename.endswith(".datasource"):
156
+ ds_path = Path(filename)
157
+ name = build_fixture_name(filename, ds_path.stem, ds_path.read_text())
158
+ fixture_path = get_fixture_dir() / f"{name}.ndjson"
159
+ if fixture_path.exists():
160
+ await append_datasource({}, tb_client, ds_path.stem, str(fixture_path), silent=True)
161
+ if not filename.endswith(".ndjson"):
162
+ await build_and_print_resource(tb_client, filename)
164
163
 
165
164
  datafiles = get_project_filenames(folder)
166
165
  fixtures = get_project_fixtures(folder)
167
166
  filenames = datafiles + fixtures
168
167
 
169
168
  async def build_once(filenames: List[str]):
169
+ ok = False
170
170
  try:
171
171
  click.echo("⚡ Building project...\n")
172
172
  time_start = time.time()
@@ -181,17 +181,34 @@ async def build(
181
181
  if fixture_path.exists():
182
182
  await append_datasource({}, tb_client, ds_path.stem, str(fixture_path), silent=True)
183
183
  click.echo(FeedbackManager.success(message=f"\n✓ Build completed in {elapsed_time:.1f}s\n"))
184
+ ok = True
184
185
  except Exception as e:
185
186
  click.echo(FeedbackManager.error(message=str(e)))
187
+ ok = False
188
+ return ok
186
189
 
187
- await build_once(filenames)
190
+ build_ok = asyncio.run(build_once(filenames))
188
191
 
189
192
  if watch:
190
- shell = BuildShell(folder=folder, client=tb_client)
193
+ paths = [Path(f) for f in get_project_filenames(folder, with_vendor=True)]
194
+
195
+ def is_vendor(f: Path) -> bool:
196
+ return "vendor/" in f.parts
197
+
198
+ def get_vendor_workspace(f: Path) -> str:
199
+ return f.parts[1]
200
+
201
+ datasource_paths = [f for f in paths if f.suffix == ".datasource"]
202
+ datasources = [f.stem for f in datasource_paths if not is_vendor(f)]
203
+ shared_datasources = [f"{get_vendor_workspace(f)}.{f.stem}" for f in datasource_paths if is_vendor(f)]
204
+ pipes = [f.stem for f in paths if f.suffix == ".pipe" and not is_vendor(f)]
205
+ shell = BuildShell(folder=folder, client=tb_client, datasources=datasources + shared_datasources, pipes=pipes)
191
206
  click.echo(FeedbackManager.highlight(message="◎ Watching for changes..."))
192
- watcher_thread = threading.Thread(target=watch_files, args=(filenames, process, shell, folder), daemon=True)
207
+ watcher_thread = threading.Thread(
208
+ target=watch_files, args=(filenames, process, shell, folder, build_ok), daemon=True
209
+ )
193
210
  watcher_thread.start()
194
- shell.cmdloop()
211
+ shell.run_shell()
195
212
 
196
213
 
197
214
  async def build_and_print_resource(tb_client: TinyB, filename: str):
@@ -0,0 +1,326 @@
1
+ import asyncio
2
+ import concurrent.futures
3
+ import os
4
+ import random
5
+ import subprocess
6
+ import sys
7
+ from typing import List
8
+
9
+ import click
10
+ import humanfriendly
11
+ from prompt_toolkit import PromptSession
12
+ from prompt_toolkit.completion import Completer, Completion
13
+ from prompt_toolkit.history import FileHistory
14
+ from prompt_toolkit.key_binding import KeyBindings
15
+ from prompt_toolkit.shortcuts import CompleteStyle
16
+ from prompt_toolkit.styles import Style
17
+
18
+ from tinybird.client import TinyB
19
+ from tinybird.feedback_manager import FeedbackManager, bcolors
20
+ from tinybird.tb.modules.exceptions import CLIException
21
+ from tinybird.tb.modules.table import format_table
22
+
23
+
24
+ class DynamicCompleter(Completer):
25
+ def __init__(self, datasources: List[str], pipes: List[str]):
26
+ self.datasources = datasources
27
+ self.pipes = pipes
28
+ self.static_commands = ["create", "mock", "test", "select"]
29
+ self.mock_flags = ["--prompt", "--rows"]
30
+ self.common_rows = ["10", "50", "100", "500", "1000"]
31
+ self.sql_keywords = ["select", "from", "where", "group by", "order by", "limit"]
32
+
33
+ def get_completions(self, document, complete_event):
34
+ text = document.text_before_cursor.strip()
35
+ words = text.split()
36
+
37
+ # Normalize command by removing 'tb' prefix if present
38
+ if words and words[0] == "tb":
39
+ words = words[1:]
40
+
41
+ if not words:
42
+ # Show all available commands when no input
43
+ yield from self._yield_static_commands("")
44
+ return
45
+
46
+ command = words[0].lower()
47
+
48
+ if command == "mock":
49
+ yield from self._handle_mock_completions(words)
50
+ elif command == "select" or self._is_sql_query(text.lower()):
51
+ yield from self._handle_sql_completions(text)
52
+ else:
53
+ # Handle general command completions
54
+ yield from self._yield_static_commands(words[-1])
55
+
56
+ def _is_sql_query(self, text: str) -> bool:
57
+ """Check if the input looks like a SQL query."""
58
+ sql_starters = ["select", "with"]
59
+ return any(text.startswith(starter) for starter in sql_starters)
60
+
61
+ def _handle_sql_completions(self, text: str):
62
+ """Handle completions for SQL queries."""
63
+ text_lower = text.lower()
64
+
65
+ # Find the last complete word
66
+ words = text_lower.split()
67
+ if not words:
68
+ return
69
+
70
+ # If we just typed 'from' or there's a space after 'from', suggest datasources
71
+ if words[-1] == "from" or (
72
+ "from" in words and len(words) > words.index("from") + 1 and text_lower.endswith(" ")
73
+ ):
74
+ for x in self.datasources:
75
+ yield Completion(x, start_position=0, display=x, style="class:completion.datasource")
76
+ for x in self.pipes:
77
+ yield Completion(x, start_position=0, display=x, style="class:completion.pipe")
78
+ return
79
+
80
+ # If we're starting a query, suggest SQL keywords
81
+ if len(words) <= 2:
82
+ for keyword in self.sql_keywords:
83
+ if keyword.lower().startswith(words[-1]):
84
+ yield Completion(
85
+ keyword, start_position=-len(words[-1]), display=keyword, style="class:completion.keyword"
86
+ )
87
+
88
+ def _handle_mock_completions(self, words: List[str]):
89
+ if len(words) == 1:
90
+ # After 'mock', show datasources
91
+ for ds in self.datasources:
92
+ yield Completion(ds, start_position=0, display=ds, style="class:completion.cmd")
93
+ return
94
+
95
+ if len(words) == 2 or len(words) == 4:
96
+ # After datasource or after a flag value, show available flags
97
+ available_flags = [f for f in self.mock_flags if f not in words]
98
+ for flag in available_flags:
99
+ yield Completion(flag, start_position=0, display=flag)
100
+ return
101
+
102
+ last_word = words[-1]
103
+ if last_word == "--prompt":
104
+ yield Completion('""', start_position=0, display='"Enter your prompt..."')
105
+ elif last_word == "--rows":
106
+ for rows in self.common_rows:
107
+ yield Completion(rows, start_position=0, display=rows)
108
+
109
+ def _yield_static_commands(self, current_word: str):
110
+ for cmd in self.static_commands:
111
+ if cmd.startswith(current_word):
112
+ yield Completion(
113
+ cmd,
114
+ start_position=-len(current_word) if current_word else 0,
115
+ display=cmd,
116
+ style="class:completion.cmd",
117
+ )
118
+
119
+
120
+ style = Style.from_dict(
121
+ {
122
+ "prompt": "fg:#34D399 bold",
123
+ "completion.cmd": "fg:#34D399 bg:#111111 bold",
124
+ "completion.datasource": "fg:#AB49D0 bg:#111111",
125
+ "completion.pipe": "fg:#FEA827 bg:#111111",
126
+ "completion.keyword": "fg:#34D399 bg:#111111",
127
+ }
128
+ )
129
+
130
+ key_bindings = KeyBindings()
131
+
132
+
133
+ @key_bindings.add("c-d")
134
+ def _(event):
135
+ """
136
+ Start auto completion. If the menu is showing already, select the next
137
+ completion.
138
+ """
139
+ b = event.app.current_buffer
140
+ if b.complete_state:
141
+ b.complete_next()
142
+ else:
143
+ b.start_completion(select_first=False)
144
+
145
+
146
+ class BuildShell:
147
+ def __init__(self, folder: str, client: TinyB, datasources: List[str], pipes: List[str]):
148
+ self.history = self.get_history()
149
+ self.folder = folder
150
+ self.client = client
151
+ self.datasources = datasources
152
+ self.pipes = pipes
153
+ self.prompt_message = "\ntb > "
154
+ self.commands = ["create", "mock", "test", "tb", "select"]
155
+
156
+ self.session = PromptSession(
157
+ completer=DynamicCompleter(self.datasources, self.pipes),
158
+ complete_style=CompleteStyle.COLUMN,
159
+ complete_while_typing=True,
160
+ history=self.history,
161
+ )
162
+
163
+ def get_history(self):
164
+ try:
165
+ history_file = os.path.expanduser("~/.tb_history")
166
+ return FileHistory(history_file)
167
+ except Exception:
168
+ return None
169
+
170
+ def run_shell(self):
171
+ while True:
172
+ try:
173
+ user_input = self.session.prompt(
174
+ [("class:prompt", self.prompt_message)], style=style, key_bindings=key_bindings
175
+ )
176
+ self.handle_input(user_input)
177
+ except (EOFError, KeyboardInterrupt):
178
+ sys.exit(0)
179
+ except CLIException as e:
180
+ click.echo(str(e))
181
+ except Exception as e:
182
+ # Catch-all for unexpected exceptions
183
+ click.echo(FeedbackManager.error_exception(error=str(e)))
184
+
185
+ def handle_input(self, argline):
186
+ line = argline.strip()
187
+ if not line:
188
+ return
189
+
190
+ # Implement the command logic here
191
+ # Replace do_* methods with equivalent logic:
192
+ command_parts = line.split(maxsplit=1)
193
+ cmd = command_parts[0].lower()
194
+ arg = command_parts[1] if len(command_parts) > 1 else ""
195
+
196
+ if cmd in ["exit", "quit"]:
197
+ sys.exit(0)
198
+ elif cmd == "build":
199
+ self.handle_build(arg)
200
+ elif cmd == "auth":
201
+ self.handle_auth(arg)
202
+ elif cmd == "workspace":
203
+ self.handle_workspace(arg)
204
+ elif cmd == "mock":
205
+ self.handle_mock(arg)
206
+ elif cmd == "tb":
207
+ self.handle_tb(arg)
208
+ else:
209
+ # Check if it looks like a SQL query or run as a tb command
210
+ self.default(line)
211
+
212
+ def handle_build(self, arg):
213
+ click.echo(FeedbackManager.error(message=f"'tb {arg}' command is not available in watch mode"))
214
+
215
+ def handle_auth(self, arg):
216
+ click.echo(FeedbackManager.error(message=f"'tb {arg}' command is not available in watch mode"))
217
+
218
+ def handle_workspace(self, arg):
219
+ click.echo(FeedbackManager.error(message=f"'tb {arg}' command is not available in watch mode"))
220
+
221
+ def handle_mock(self, arg):
222
+ subprocess.run(f"tb mock {arg} --folder {self.folder}", shell=True, text=True)
223
+
224
+ def handle_tb(self, arg):
225
+ click.echo("")
226
+ arg = arg.strip().lower()
227
+ if arg.startswith("build"):
228
+ self.handle_build(arg)
229
+ elif arg.startswith("auth"):
230
+ self.handle_auth(arg)
231
+ elif arg.startswith("workspace"):
232
+ self.handle_workspace(arg)
233
+ elif arg.startswith("mock"):
234
+ self.handle_mock(arg)
235
+ else:
236
+ subprocess.run(f"tb --local {arg}", shell=True, text=True)
237
+
238
+ def default(self, argline):
239
+ click.echo("")
240
+ arg = argline.strip().lower()
241
+ if not arg:
242
+ return
243
+ if arg.startswith("with") or arg.startswith("select"):
244
+ try:
245
+ self.run_sql(argline)
246
+ except Exception as e:
247
+ click.echo(FeedbackManager.error(message=str(e)))
248
+ else:
249
+ subprocess.run(f"tb --local {arg}", shell=True, text=True)
250
+
251
+ def run_sql(self, query, rows_limit=20):
252
+ try:
253
+ q = query.strip()
254
+ if q.lower().startswith("insert"):
255
+ click.echo(FeedbackManager.info_append_data())
256
+ raise CLIException(FeedbackManager.error_invalid_query())
257
+ if q.lower().startswith("delete"):
258
+ raise CLIException(FeedbackManager.error_invalid_query())
259
+
260
+ def run_query_in_thread():
261
+ loop = asyncio.new_event_loop()
262
+ asyncio.set_event_loop(loop)
263
+ try:
264
+ return loop.run_until_complete(
265
+ self.client.query(f"SELECT * FROM ({query}) LIMIT {rows_limit} FORMAT JSON")
266
+ )
267
+ finally:
268
+ loop.close()
269
+
270
+ with concurrent.futures.ThreadPoolExecutor() as executor:
271
+ res = executor.submit(run_query_in_thread).result()
272
+
273
+ except Exception as e:
274
+ raise CLIException(FeedbackManager.error_exception(error=str(e)))
275
+
276
+ if isinstance(res, dict) and "error" in res:
277
+ raise CLIException(FeedbackManager.error_exception(error=res["error"]))
278
+
279
+ if isinstance(res, dict) and "data" in res and res["data"]:
280
+ print_table_formatted(res, "QUERY")
281
+ else:
282
+ click.echo(FeedbackManager.info_no_rows())
283
+
284
+ def reprint_prompt(self):
285
+ click.echo(f"{bcolors.OKGREEN}{self.prompt_message}{bcolors.ENDC}", nl=False)
286
+
287
+
288
+ def print_table_formatted(res: dict, name: str):
289
+ rebuild_colors = [bcolors.FAIL, bcolors.OKBLUE, bcolors.WARNING, bcolors.OKGREEN, bcolors.HEADER]
290
+ rebuild_index = random.randint(0, len(rebuild_colors) - 1)
291
+ rebuild_color = rebuild_colors[rebuild_index % len(rebuild_colors)]
292
+ data = []
293
+ limit = 5
294
+ for d in res["data"][:5]:
295
+ data.append(d.values())
296
+ meta = res["meta"]
297
+ row_count = res.get("rows", 0)
298
+ stats = res.get("statistics", {})
299
+ elapsed = stats.get("elapsed", 0)
300
+ cols = len(meta)
301
+ try:
302
+
303
+ def print_message(message: str, color=bcolors.CGREY):
304
+ return f"{color}{message}{bcolors.ENDC}"
305
+
306
+ table = format_table(data, meta)
307
+ colored_char = print_message("│", rebuild_color)
308
+ table_with_marker = "\n".join(f"{colored_char} {line}" for line in table.split("\n"))
309
+ click.echo(f"\n{colored_char} {print_message('⚡', rebuild_color)} Running {name}")
310
+ click.echo(colored_char)
311
+ click.echo(table_with_marker)
312
+ click.echo(colored_char)
313
+ rows_read = humanfriendly.format_number(stats.get("rows_read", 0))
314
+ bytes_read = humanfriendly.format_size(stats.get("bytes_read", 0))
315
+ elapsed = humanfriendly.format_timespan(elapsed) if elapsed >= 1 else f"{elapsed * 1000:.2f}ms"
316
+ stats_message = f"» {bytes_read} ({rows_read} rows x {cols} cols) in {elapsed}"
317
+ rows_message = f"» Showing {limit} first rows" if row_count > limit else "» Showing all rows"
318
+ click.echo(f"{colored_char} {print_message(stats_message, bcolors.OKGREEN)}")
319
+ click.echo(f"{colored_char} {print_message(rows_message, bcolors.CGREY)}")
320
+ except ValueError as exc:
321
+ if str(exc) == "max() arg is an empty sequence":
322
+ click.echo("------------")
323
+ click.echo("Empty")
324
+ click.echo("------------")
325
+ else:
326
+ raise exc
@@ -14,9 +14,7 @@ class Provider(Enum):
14
14
  GitLab = 1
15
15
 
16
16
 
17
- WORKFLOW_VERSION = "v3.1.0"
18
-
19
- DEFAULT_REQUIREMENTS_FILE = "tinybird-cli>=5,<6"
17
+ WORKFLOW_VERSION = "v0.0.1"
20
18
 
21
19
  GITHUB_CI_YML = """
22
20
  name: Tinybird - CI Workflow
@@ -50,6 +48,8 @@ jobs:
50
48
  run: curl -LsSf https://api.tinybird.co/static/install.sh | sh
51
49
  - name: Build project
52
50
  run: tb build
51
+ - name: Test project
52
+ run: tb test run
53
53
  """
54
54
 
55
55
 
@@ -64,110 +64,30 @@ stages:
64
64
 
65
65
  GITLAB_CI_YML = """
66
66
  tinybird_ci_workflow:
67
+ image: ubuntu:latest
67
68
  stage: tests
68
69
  interruptible: true
69
70
  needs: []
70
71
  rules:
71
- - if: $CI_PIPELINE_SOURCE == "merge_request_event"{% if data_project_dir != '.' %}
72
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
72
73
  changes:
73
- - .gitlab/tinybird/*
74
+ - .gitlab/tinybird/*{% if data_project_dir != '.' %}
74
75
  - {{ data_project_dir }}/*
75
76
  - {{ data_project_dir }}/**/*{% end %}
76
77
  before_script:
78
+ - apt update && apt install -y curl
77
79
  - curl -LsSf https://api.tinybird.co/static/install.sh | sh
78
80
  script:
81
+ - export PATH="$HOME/.local/bin:$PATH"
79
82
  - cd $CI_PROJECT_DIR/{{ data_project_dir }}
80
83
  - tb build
84
+ - tb test run
81
85
  services:
82
86
  - name: tinybirdco/tinybird-local:latest
83
87
  alias: tinybird-local
84
88
  """
85
89
 
86
90
 
87
- EXEC_TEST_SH = """
88
- #!/usr/bin/env bash
89
- set -euxo pipefail
90
-
91
- export TB_VERSION_WARNING=0
92
-
93
- run_test() {
94
- t=$1
95
- echo "** Running $t **"
96
- echo "** $(cat $t)"
97
- tmpfile=$(mktemp)
98
- retries=0
99
- TOTAL_RETRIES=3
100
-
101
- # When appending fixtures, we need to retry in case of the data is not replicated in time
102
- while [ $retries -lt $TOTAL_RETRIES ]; do
103
- # Run the test and store the output in a temporary file
104
- bash $t $2 >$tmpfile
105
- exit_code=$?
106
- if [ "$exit_code" -eq 0 ]; then
107
- # If the test passed, break the loop
108
- if diff -B ${t}.result $tmpfile >/dev/null 2>&1; then
109
- break
110
- # If the test failed, increment the retries counter and try again
111
- else
112
- retries=$((retries+1))
113
- fi
114
- # If the bash command failed, print an error message and break the loop
115
- else
116
- break
117
- fi
118
- done
119
-
120
- if diff -B ${t}.result $tmpfile >/dev/null 2>&1; then
121
- echo "✅ Test $t passed"
122
- rm $tmpfile
123
- return 0
124
- elif [ $retries -eq $TOTAL_RETRIES ]; then
125
- echo "🚨 ERROR: Test $t failed, diff:";
126
- diff -B ${t}.result $tmpfile
127
- rm $tmpfile
128
- return 1
129
- else
130
- echo "🚨 ERROR: Test $t failed with bash command exit code $?"
131
- cat $tmpfile
132
- rm $tmpfile
133
- return 1
134
- fi
135
- echo ""
136
- }
137
- export -f run_test
138
-
139
- fail=0
140
- find ./tests -name "*.test" -print0 | xargs -0 -I {} -P 4 bash -c 'run_test "$@"' _ {} || fail=1
141
-
142
- if [ $fail == 1 ]; then
143
- exit -1;
144
- fi
145
- """
146
-
147
- APPEND_FIXTURES_SH = """
148
- #!/usr/bin/env bash
149
- set -euxo pipefail
150
-
151
- directory="datasources/fixtures"
152
- extensions=("csv" "ndjson")
153
-
154
- absolute_directory=$(realpath "$directory")
155
-
156
- for extension in "${extensions[@]}"; do
157
- file_list=$(find "$absolute_directory" -type f -name "*.$extension")
158
-
159
- for file_path in $file_list; do
160
- file_name=$(basename "$file_path")
161
- file_name_without_extension="${file_name%.*}"
162
-
163
- command="tb datasource append $file_name_without_extension datasources/fixtures/$file_name"
164
- echo $command
165
- $command
166
- done
167
- done
168
- """
169
-
170
-
171
91
  class CICDFile:
172
92
  def __init__(
173
93
  self,