tinybird 0.0.1.dev17__tar.gz → 0.0.1.dev19__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.dev17 → tinybird-0.0.1.dev19}/PKG-INFO +1 -1
  2. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/client.py +22 -2
  3. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/datafile.py +2 -0
  4. tinybird-0.0.1.dev19/tinybird/tb/__cli__.py +8 -0
  5. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/build.py +32 -10
  6. tinybird-0.0.1.dev19/tinybird/tb/modules/build_shell.py +368 -0
  7. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/cicd.py +9 -89
  8. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/cli.py +3 -1
  9. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/common.py +1 -108
  10. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/create.py +2 -6
  11. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/common.py +224 -247
  12. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/parse_datasource.py +8 -0
  13. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/parse_pipe.py +10 -1
  14. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/llm.py +4 -3
  15. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/local_common.py +1 -1
  16. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/login.py +17 -10
  17. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/mock.py +14 -12
  18. tinybird-0.0.1.dev19/tinybird/tb/modules/test.py +230 -0
  19. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird.egg-info/PKG-INFO +1 -1
  20. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird.egg-info/SOURCES.txt +1 -0
  21. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird.egg-info/requires.txt +1 -0
  22. tinybird-0.0.1.dev17/tinybird/tb/modules/build_shell.py +0 -149
  23. tinybird-0.0.1.dev17/tinybird/tb/modules/test.py +0 -138
  24. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/setup.cfg +0 -0
  25. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/__cli__.py +0 -0
  26. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/ch_utils/constants.py +0 -0
  27. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/ch_utils/engine.py +0 -0
  28. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/check_pypi.py +0 -0
  29. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/config.py +0 -0
  30. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/connectors.py +0 -0
  31. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/context.py +0 -0
  32. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/datatypes.py +0 -0
  33. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/feedback_manager.py +0 -0
  34. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/git_settings.py +0 -0
  35. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/prompts.py +0 -0
  36. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/sql.py +0 -0
  37. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/sql_template.py +0 -0
  38. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/sql_template_fmt.py +0 -0
  39. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/sql_toolset.py +0 -0
  40. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/syncasync.py +0 -0
  41. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/cli.py +0 -0
  42. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/auth.py +0 -0
  43. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/config.py +0 -0
  44. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/connection.py +0 -0
  45. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/build.py +0 -0
  46. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/build_common.py +0 -0
  47. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/build_datasource.py +0 -0
  48. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/build_pipe.py +0 -0
  49. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/diff.py +0 -0
  50. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/exceptions.py +0 -0
  51. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/fixture.py +0 -0
  52. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/format_common.py +0 -0
  53. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/format_datasource.py +0 -0
  54. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/format_pipe.py +0 -0
  55. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/pipe_checker.py +0 -0
  56. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datafile/pull.py +0 -0
  57. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/datasource.py +0 -0
  58. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/exceptions.py +0 -0
  59. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/fmt.py +0 -0
  60. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/job.py +0 -0
  61. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/local.py +0 -0
  62. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/pipe.py +0 -0
  63. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/regions.py +0 -0
  64. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/table.py +0 -0
  65. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/tag.py +0 -0
  66. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/telemetry.py +0 -0
  67. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/tinyunit/tinyunit.py +0 -0
  68. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/tinyunit/tinyunit_lib.py +0 -0
  69. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/token.py +0 -0
  70. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/workspace.py +0 -0
  71. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb/modules/workspace_members.py +0 -0
  72. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli.py +0 -0
  73. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/auth.py +0 -0
  74. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/branch.py +0 -0
  75. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/cicd.py +0 -0
  76. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/cli.py +0 -0
  77. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/common.py +0 -0
  78. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/config.py +0 -0
  79. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/connection.py +0 -0
  80. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/datasource.py +0 -0
  81. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/exceptions.py +0 -0
  82. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/fmt.py +0 -0
  83. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/job.py +0 -0
  84. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/pipe.py +0 -0
  85. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/regions.py +0 -0
  86. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/tag.py +0 -0
  87. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/telemetry.py +0 -0
  88. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/test.py +0 -0
  89. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  90. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
  91. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/workspace.py +0 -0
  92. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  93. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird/tornado_template.py +0 -0
  94. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird.egg-info/dependency_links.txt +0 -0
  95. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/tinybird.egg-info/entry_points.txt +0 -0
  96. {tinybird-0.0.1.dev17 → tinybird-0.0.1.dev19}/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.dev17
3
+ Version: 0.0.1.dev19
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -103,7 +103,7 @@ class TinyB:
103
103
  self.send_telemetry = send_telemetry
104
104
  self.semver = semver
105
105
 
106
- async def _req(
106
+ async def _req_raw(
107
107
  self,
108
108
  endpoint: str,
109
109
  data=None,
@@ -155,10 +155,15 @@ class TinyB:
155
155
  response = await sync_to_async(session.get, thread_sensitive=False)(
156
156
  url, verify=verify_ssl, **kwargs
157
157
  )
158
-
159
158
  except Exception as e:
160
159
  raise e
161
160
 
161
+ if self.send_telemetry:
162
+ try:
163
+ add_telemetry_event("api_request", endpoint=url, token=self.token, status_code=response.status_code)
164
+ except Exception as ex:
165
+ logging.exception(f"Can't send telemetry: {ex}")
166
+
162
167
  logging.debug("== server response ==")
163
168
  logging.debug(response.content)
164
169
  logging.debug("== end ==")
@@ -169,6 +174,21 @@ class TinyB:
169
174
  except Exception as ex:
170
175
  logging.exception(f"Can't send telemetry: {ex}")
171
176
 
177
+ return response
178
+
179
+ async def _req(
180
+ self,
181
+ endpoint: str,
182
+ data=None,
183
+ files=None,
184
+ method: str = "GET",
185
+ retries: int = LIMIT_RETRIES,
186
+ use_token: Optional[str] = None,
187
+ **kwargs,
188
+ ):
189
+ token_to_use = use_token if use_token else self.token
190
+ response = await self._req_raw(endpoint, data, files, method, retries, use_token, **kwargs)
191
+
172
192
  if response.status_code == 403:
173
193
  error = parse_error_response(response)
174
194
  if not token_to_use:
@@ -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
 
@@ -0,0 +1,8 @@
1
+
2
+ __name__ = 'tinybird'
3
+ __description__ = 'Tinybird Command Line Tool'
4
+ __url__ = 'https://www.tinybird.co/docs/cli/introduction.html'
5
+ __author__ = 'Tinybird'
6
+ __author_email__ = 'support@tinybird.co'
7
+ __version__ = '0.0.1.dev19'
8
+ __revision__ = '5ef65e7'
@@ -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
@@ -101,10 +100,7 @@ def watch_files(
101
100
  is_flag=True,
102
101
  help="Watch for changes in the files and re-check them.",
103
102
  )
104
- @click.pass_context
105
- @coro
106
- async def build(
107
- ctx: Context,
103
+ def build(
108
104
  folder: str,
109
105
  watch: bool,
110
106
  ) -> None:
@@ -115,7 +111,7 @@ async def build(
115
111
  context.disable_template_security_validation.set(True)
116
112
  is_internal = has_internal_datafiles(folder)
117
113
  folder_path = os.path.abspath(folder)
118
- tb_client = await get_tinybird_local_client(folder_path)
114
+ tb_client = asyncio.run(get_tinybird_local_client(folder_path))
119
115
 
120
116
  def check_filenames(filenames: List[str]):
121
117
  parser_matrix = {".pipe": parse_pipe, ".datasource": parse_datasource}
@@ -191,16 +187,42 @@ async def build(
191
187
  ok = False
192
188
  return ok
193
189
 
194
- build_ok = await build_once(filenames)
190
+ build_ok = asyncio.run(build_once(filenames))
195
191
 
196
192
  if watch:
197
- 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 f.parts[0] == "vendor"
197
+
198
+ def get_vendor_workspace(f: Path) -> str:
199
+ return f.parts[1]
200
+
201
+ def is_endpoint(f: Path) -> bool:
202
+ return f.suffix == ".pipe" and not is_vendor(f) and f.parts[0] == "endpoints"
203
+
204
+ def is_pipe(f: Path) -> bool:
205
+ return f.suffix == ".pipe" and not is_vendor(f)
206
+
207
+ datasource_paths = [f for f in paths if f.suffix == ".datasource"]
208
+ datasources = [f.stem for f in datasource_paths if not is_vendor(f)]
209
+ shared_datasources = [f"{get_vendor_workspace(f)}.{f.stem}" for f in datasource_paths if is_vendor(f)]
210
+ pipes = [f.stem for f in paths if is_pipe(f) and not is_endpoint(f)]
211
+ endpoints = [f.stem for f in paths if is_endpoint(f)]
212
+ shell = BuildShell(
213
+ folder=folder,
214
+ client=tb_client,
215
+ datasources=datasources,
216
+ shared_datasources=shared_datasources,
217
+ pipes=pipes,
218
+ endpoints=endpoints,
219
+ )
198
220
  click.echo(FeedbackManager.highlight(message="◎ Watching for changes..."))
199
221
  watcher_thread = threading.Thread(
200
222
  target=watch_files, args=(filenames, process, shell, folder, build_ok), daemon=True
201
223
  )
202
224
  watcher_thread.start()
203
- shell.cmdloop()
225
+ shell.run_shell()
204
226
 
205
227
 
206
228
  async def build_and_print_resource(tb_client: TinyB, filename: str):
@@ -0,0 +1,368 @@
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], shared_datasources: List[str], endpoints: List[str], pipes: List[str]):
26
+ self.datasources = datasources
27
+ self.shared_datasources = shared_datasources
28
+ self.endpoints = endpoints
29
+ self.pipes = pipes
30
+ self.static_commands = ["create", "mock", "test", "select"]
31
+ self.test_commands = ["create", "run", "update"]
32
+ self.mock_flags = ["--prompt", "--rows"]
33
+ self.common_rows = ["10", "50", "100", "500", "1000"]
34
+ self.sql_keywords = ["select", "from", "where", "group by", "order by", "limit"]
35
+
36
+ def get_completions(self, document, complete_event):
37
+ text = document.text_before_cursor.strip()
38
+ words = text.split()
39
+
40
+ # Normalize command by removing 'tb' prefix if present
41
+ if words and words[0] == "tb":
42
+ words = words[1:]
43
+
44
+ if not words:
45
+ # Show all available commands when no input
46
+ yield from self._yield_static_commands("")
47
+ return
48
+
49
+ command = words[0].lower()
50
+
51
+ if command == "mock":
52
+ yield from self._handle_mock_completions(words)
53
+ elif command == "test":
54
+ yield from self._handle_test_completions(words)
55
+ elif command == "select" or self._is_sql_query(text.lower()):
56
+ yield from self._handle_sql_completions(text)
57
+ else:
58
+ # Handle general command completions
59
+ yield from self._yield_static_commands(words[-1])
60
+
61
+ def _is_sql_query(self, text: str) -> bool:
62
+ """Check if the input looks like a SQL query."""
63
+ sql_starters = ["select", "with"]
64
+ return any(text.startswith(starter) for starter in sql_starters)
65
+
66
+ def _handle_sql_completions(self, text: str):
67
+ """Handle completions for SQL queries."""
68
+ text_lower = text.lower()
69
+
70
+ # Find the last complete word
71
+ words = text_lower.split()
72
+ if not words:
73
+ return
74
+
75
+ # If we just typed 'from' or there's a space after 'from', suggest datasources
76
+ if words[-1] == "from" or (
77
+ "from" in words and len(words) > words.index("from") + 1 and text_lower.endswith(" ")
78
+ ):
79
+ for x in self.datasources + self.shared_datasources:
80
+ yield Completion(x, start_position=0, display=x, style="class:completion.datasource")
81
+ for x in self.endpoints + self.pipes:
82
+ yield Completion(x, start_position=0, display=x, style="class:completion.pipe")
83
+ return
84
+
85
+ # If we're starting a query, suggest SQL keywords
86
+ if len(words) <= 2:
87
+ for keyword in self.sql_keywords:
88
+ if keyword.lower().startswith(words[-1]):
89
+ yield Completion(
90
+ keyword, start_position=-len(words[-1]), display=keyword, style="class:completion.keyword"
91
+ )
92
+
93
+ def _handle_mock_completions(self, words: List[str]):
94
+ if len(words) == 1:
95
+ # After 'mock', show datasources
96
+ for ds in self.datasources:
97
+ yield Completion(ds, start_position=0, display=ds, style="class:completion.datasource")
98
+ return
99
+
100
+ if len(words) == 2 or len(words) == 4:
101
+ # After datasource or after a flag value, show available flags
102
+ available_flags = [f for f in self.mock_flags if f not in words]
103
+ for flag in available_flags:
104
+ yield Completion(flag, start_position=0, display=flag, style="class:completion.cmd")
105
+ return
106
+
107
+ last_word = words[-1]
108
+ if last_word == "--prompt":
109
+ yield Completion('""', start_position=0, display='"Enter your prompt..."', style="class:completion.cmd")
110
+ elif last_word == "--rows":
111
+ for rows in self.common_rows:
112
+ yield Completion(rows, start_position=0, display=rows, style="class:completion.cmd")
113
+
114
+ def _handle_test_completions(self, words: List[str]):
115
+ if len(words) == 1:
116
+ for cmd in self.test_commands:
117
+ yield Completion(cmd, start_position=0, display=cmd, style="class:completion.cmd")
118
+ return
119
+ elif len(words) == 2:
120
+ for cmd in self.endpoints:
121
+ yield Completion(cmd, start_position=0, display=cmd, style="class:completion.pipe")
122
+ return
123
+
124
+ def _yield_static_commands(self, current_word: str):
125
+ for cmd in self.static_commands:
126
+ if cmd.startswith(current_word):
127
+ yield Completion(
128
+ cmd,
129
+ start_position=-len(current_word) if current_word else 0,
130
+ display=cmd,
131
+ style="class:completion.cmd",
132
+ )
133
+
134
+ for cmd in self.datasources + self.shared_datasources:
135
+ if cmd.startswith(current_word):
136
+ yield Completion(
137
+ cmd,
138
+ start_position=-len(current_word) if current_word else 0,
139
+ display=cmd,
140
+ style="class:completion.datasource",
141
+ )
142
+
143
+ for cmd in self.endpoints + self.pipes:
144
+ if cmd.startswith(current_word):
145
+ yield Completion(
146
+ cmd,
147
+ start_position=-len(current_word) if current_word else 0,
148
+ display=cmd,
149
+ style="class:completion.pipe",
150
+ )
151
+
152
+
153
+ style = Style.from_dict(
154
+ {
155
+ "prompt": "fg:#34D399 bold",
156
+ "completion.cmd": "fg:#34D399 bg:#111111 bold",
157
+ "completion.datasource": "fg:#AB49D0 bg:#111111",
158
+ "completion.pipe": "fg:#FEA827 bg:#111111",
159
+ "completion.keyword": "fg:#34D399 bg:#111111",
160
+ }
161
+ )
162
+
163
+ key_bindings = KeyBindings()
164
+
165
+
166
+ @key_bindings.add("c-d")
167
+ def _(event):
168
+ """
169
+ Start auto completion. If the menu is showing already, select the next
170
+ completion.
171
+ """
172
+ b = event.app.current_buffer
173
+ if b.complete_state:
174
+ b.complete_next()
175
+ else:
176
+ b.start_completion(select_first=False)
177
+
178
+
179
+ class BuildShell:
180
+ def __init__(
181
+ self,
182
+ folder: str,
183
+ client: TinyB,
184
+ datasources: List[str],
185
+ shared_datasources: List[str],
186
+ pipes: List[str],
187
+ endpoints: List[str],
188
+ ):
189
+ self.history = self.get_history()
190
+ self.folder = folder
191
+ self.client = client
192
+ self.datasources = datasources
193
+ self.shared_datasources = shared_datasources
194
+ self.pipes = pipes
195
+ self.endpoints = endpoints
196
+ self.prompt_message = "\ntb > "
197
+ self.commands = ["create", "mock", "test", "tb", "select"]
198
+
199
+ self.session = PromptSession(
200
+ completer=DynamicCompleter(self.datasources, self.shared_datasources, self.endpoints, self.pipes),
201
+ complete_style=CompleteStyle.COLUMN,
202
+ complete_while_typing=True,
203
+ history=self.history,
204
+ )
205
+
206
+ def get_history(self):
207
+ try:
208
+ history_file = os.path.expanduser("~/.tb_history")
209
+ return FileHistory(history_file)
210
+ except Exception:
211
+ return None
212
+
213
+ def run_shell(self):
214
+ while True:
215
+ try:
216
+ user_input = self.session.prompt(
217
+ [("class:prompt", self.prompt_message)], style=style, key_bindings=key_bindings
218
+ )
219
+ self.handle_input(user_input)
220
+ except (EOFError, KeyboardInterrupt):
221
+ sys.exit(0)
222
+ except CLIException as e:
223
+ click.echo(str(e))
224
+ except Exception as e:
225
+ # Catch-all for unexpected exceptions
226
+ click.echo(FeedbackManager.error_exception(error=str(e)))
227
+
228
+ def handle_input(self, argline):
229
+ line = argline.strip()
230
+ if not line:
231
+ return
232
+
233
+ # Implement the command logic here
234
+ # Replace do_* methods with equivalent logic:
235
+ command_parts = line.split(maxsplit=1)
236
+ cmd = command_parts[0].lower()
237
+ arg = command_parts[1] if len(command_parts) > 1 else ""
238
+
239
+ if cmd in ["exit", "quit"]:
240
+ sys.exit(0)
241
+ elif cmd == "build":
242
+ self.handle_build(arg)
243
+ elif cmd == "auth":
244
+ self.handle_auth(arg)
245
+ elif cmd == "workspace":
246
+ self.handle_workspace(arg)
247
+ elif cmd == "mock":
248
+ self.handle_mock(arg)
249
+ elif cmd == "tb":
250
+ self.handle_tb(arg)
251
+ else:
252
+ # Check if it looks like a SQL query or run as a tb command
253
+ self.default(line)
254
+
255
+ def handle_build(self, arg):
256
+ click.echo(FeedbackManager.error(message=f"'tb {arg}' command is not available in watch mode"))
257
+
258
+ def handle_auth(self, arg):
259
+ click.echo(FeedbackManager.error(message=f"'tb {arg}' command is not available in watch mode"))
260
+
261
+ def handle_workspace(self, arg):
262
+ click.echo(FeedbackManager.error(message=f"'tb {arg}' command is not available in watch mode"))
263
+
264
+ def handle_mock(self, arg):
265
+ subprocess.run(f"tb mock {arg} --folder {self.folder}", shell=True, text=True)
266
+
267
+ def handle_tb(self, arg):
268
+ click.echo("")
269
+ arg = arg.strip().lower()
270
+ if arg.startswith("build"):
271
+ self.handle_build(arg)
272
+ elif arg.startswith("auth"):
273
+ self.handle_auth(arg)
274
+ elif arg.startswith("workspace"):
275
+ self.handle_workspace(arg)
276
+ elif arg.startswith("mock"):
277
+ self.handle_mock(arg)
278
+ else:
279
+ subprocess.run(f"tb --local {arg}", shell=True, text=True)
280
+
281
+ def default(self, argline):
282
+ click.echo("")
283
+ arg = argline.strip().lower()
284
+ if not arg:
285
+ return
286
+ if arg.startswith("with") or arg.startswith("select"):
287
+ self.run_sql(argline)
288
+ elif len(arg.split()) == 1 and arg in self.endpoints + self.pipes + self.datasources + self.shared_datasources:
289
+ self.run_sql(f"select * from {arg}")
290
+ else:
291
+ subprocess.run(f"tb --local {arg}", shell=True, text=True)
292
+
293
+ def run_sql(self, query, rows_limit=20):
294
+ try:
295
+ q = query.strip()
296
+ if q.lower().startswith("insert"):
297
+ click.echo(FeedbackManager.info_append_data())
298
+ raise CLIException(FeedbackManager.error_invalid_query())
299
+ if q.lower().startswith("delete"):
300
+ raise CLIException(FeedbackManager.error_invalid_query())
301
+
302
+ def run_query_in_thread():
303
+ loop = asyncio.new_event_loop()
304
+ asyncio.set_event_loop(loop)
305
+ try:
306
+ return loop.run_until_complete(
307
+ self.client.query(f"SELECT * FROM ({query}) LIMIT {rows_limit} FORMAT JSON")
308
+ )
309
+ finally:
310
+ loop.close()
311
+
312
+ with concurrent.futures.ThreadPoolExecutor() as executor:
313
+ res = executor.submit(run_query_in_thread).result()
314
+
315
+ except Exception as e:
316
+ click.echo(FeedbackManager.error_exception(error=str(e)))
317
+
318
+ if isinstance(res, dict) and "error" in res:
319
+ click.echo(FeedbackManager.error_exception(error=res["error"]))
320
+
321
+ if isinstance(res, dict) and "data" in res and res["data"]:
322
+ print_table_formatted(res, "QUERY")
323
+ else:
324
+ click.echo(FeedbackManager.info_no_rows())
325
+
326
+ def reprint_prompt(self):
327
+ click.echo(f"{bcolors.OKGREEN}{self.prompt_message}{bcolors.ENDC}", nl=False)
328
+
329
+
330
+ def print_table_formatted(res: dict, name: str):
331
+ rebuild_colors = [bcolors.FAIL, bcolors.OKBLUE, bcolors.WARNING, bcolors.OKGREEN, bcolors.HEADER]
332
+ rebuild_index = random.randint(0, len(rebuild_colors) - 1)
333
+ rebuild_color = rebuild_colors[rebuild_index % len(rebuild_colors)]
334
+ data = []
335
+ limit = 5
336
+ for d in res["data"][:5]:
337
+ data.append(d.values())
338
+ meta = res["meta"]
339
+ row_count = res.get("rows", 0)
340
+ stats = res.get("statistics", {})
341
+ elapsed = stats.get("elapsed", 0)
342
+ cols = len(meta)
343
+ try:
344
+
345
+ def print_message(message: str, color=bcolors.CGREY):
346
+ return f"{color}{message}{bcolors.ENDC}"
347
+
348
+ table = format_table(data, meta)
349
+ colored_char = print_message("│", rebuild_color)
350
+ table_with_marker = "\n".join(f"{colored_char} {line}" for line in table.split("\n"))
351
+ click.echo(f"\n{colored_char} {print_message('⚡', rebuild_color)} Running {name}")
352
+ click.echo(colored_char)
353
+ click.echo(table_with_marker)
354
+ click.echo(colored_char)
355
+ rows_read = humanfriendly.format_number(stats.get("rows_read", 0))
356
+ bytes_read = humanfriendly.format_size(stats.get("bytes_read", 0))
357
+ elapsed = humanfriendly.format_timespan(elapsed) if elapsed >= 1 else f"{elapsed * 1000:.2f}ms"
358
+ stats_message = f"» {bytes_read} ({rows_read} rows x {cols} cols) in {elapsed}"
359
+ rows_message = f"» Showing {limit} first rows" if row_count > limit else "» Showing all rows"
360
+ click.echo(f"{colored_char} {print_message(stats_message, bcolors.OKGREEN)}")
361
+ click.echo(f"{colored_char} {print_message(rows_message, bcolors.CGREY)}")
362
+ except ValueError as exc:
363
+ if str(exc) == "max() arg is an empty sequence":
364
+ click.echo("------------")
365
+ click.echo("Empty")
366
+ click.echo("------------")
367
+ else:
368
+ 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,
@@ -26,8 +26,9 @@ from tinybird.client import (
26
26
  DoesNotExistException,
27
27
  TinyB,
28
28
  )
29
- from tinybird.config import SUPPORTED_CONNECTORS, VERSION, FeatureFlags, get_config
29
+ from tinybird.config import SUPPORTED_CONNECTORS, FeatureFlags, get_config
30
30
  from tinybird.feedback_manager import FeedbackManager
31
+ from tinybird.tb import __cli__
31
32
  from tinybird.tb.modules.common import (
32
33
  OLDEST_ROLLBACK,
33
34
  CatchAuthExceptions,
@@ -70,6 +71,7 @@ __old_click_secho = click.secho
70
71
  DEFAULT_PATTERNS: List[Tuple[str, Union[str, Callable[[str], str]]]] = [
71
72
  (r"p\.ey[A-Za-z0-9-_\.]+", lambda v: f"{v[:4]}...{v[-8:]}")
72
73
  ]
74
+ VERSION = f"{__cli__.__version__} (rev {__cli__.__revision__})"
73
75
 
74
76
 
75
77
  @click.group(cls=CatchAuthExceptions, context_settings={"help_option_names": ["-h", "--help"]})