tinybird 0.0.1.dev16__py3-none-any.whl → 0.0.1.dev18__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tinybird/tb/cli.py +0 -1
- tinybird/tb/modules/build.py +50 -33
- tinybird/tb/modules/build_shell.py +231 -38
- tinybird/tb/modules/cicd.py +9 -89
- tinybird/tb/modules/cli.py +4 -89
- tinybird/tb/modules/common.py +8 -206
- tinybird/tb/modules/config.py +0 -10
- tinybird/tb/modules/create.py +54 -223
- tinybird/tb/modules/datafile/build_pipe.py +1 -1
- tinybird/tb/modules/datafile/common.py +223 -247
- tinybird/tb/modules/datafile/parse_datasource.py +8 -0
- tinybird/tb/modules/datafile/parse_pipe.py +10 -1
- tinybird/tb/modules/datasource.py +0 -10
- tinybird/tb/modules/llm.py +4 -3
- tinybird/tb/modules/local_common.py +1 -1
- tinybird/tb/modules/login.py +1 -1
- tinybird/tb/modules/mock.py +14 -12
- tinybird/tb/modules/pipe.py +0 -4
- tinybird/tb/modules/test.py +90 -17
- {tinybird-0.0.1.dev16.dist-info → tinybird-0.0.1.dev18.dist-info}/METADATA +2 -1
- {tinybird-0.0.1.dev16.dist-info → tinybird-0.0.1.dev18.dist-info}/RECORD +24 -25
- tinybird/tb/modules/branch.py +0 -1023
- {tinybird-0.0.1.dev16.dist-info → tinybird-0.0.1.dev18.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev16.dist-info → tinybird-0.0.1.dev18.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev16.dist-info → tinybird-0.0.1.dev18.dist-info}/top_level.txt +0 -0
tinybird/tb/cli.py
CHANGED
|
@@ -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
|
tinybird/tb/modules/build.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
190
|
+
build_ok = asyncio.run(build_once(filenames))
|
|
188
191
|
|
|
189
192
|
if watch:
|
|
190
|
-
|
|
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(
|
|
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.
|
|
211
|
+
shell.run_shell()
|
|
195
212
|
|
|
196
213
|
|
|
197
214
|
async def build_and_print_resource(tb_client: TinyB, filename: str):
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import
|
|
2
|
+
import concurrent.futures
|
|
3
|
+
import os
|
|
3
4
|
import random
|
|
4
5
|
import subprocess
|
|
5
6
|
import sys
|
|
7
|
+
from typing import List
|
|
6
8
|
|
|
7
9
|
import click
|
|
8
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
|
|
9
17
|
|
|
10
18
|
from tinybird.client import TinyB
|
|
11
19
|
from tinybird.feedback_manager import FeedbackManager, bcolors
|
|
@@ -13,57 +21,242 @@ from tinybird.tb.modules.exceptions import CLIException
|
|
|
13
21
|
from tinybird.tb.modules.table import format_table
|
|
14
22
|
|
|
15
23
|
|
|
16
|
-
class
|
|
17
|
-
|
|
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"]
|
|
18
32
|
|
|
19
|
-
def
|
|
20
|
-
|
|
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()
|
|
21
149
|
self.folder = folder
|
|
22
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"]
|
|
23
155
|
|
|
24
|
-
|
|
25
|
-
|
|
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
|
+
)
|
|
26
162
|
|
|
27
|
-
def
|
|
28
|
-
|
|
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
|
|
29
169
|
|
|
30
|
-
def
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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)
|
|
34
208
|
else:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return
|
|
38
|
-
if arg_stripped.startswith("tb"):
|
|
39
|
-
arg_stripped = arg_stripped.replace("tb", "tb --local")
|
|
40
|
-
extra_args = f" --folder {self.folder}" if arg_stripped.startswith("tb mock") else ""
|
|
41
|
-
subprocess.run(arg_stripped + extra_args, shell=True, text=True)
|
|
42
|
-
elif arg_stripped.startswith("with") or arg_stripped.startswith("select"):
|
|
43
|
-
try:
|
|
44
|
-
self.run_sql(self.client, argline)
|
|
45
|
-
except Exception as e:
|
|
46
|
-
click.echo(FeedbackManager.error(message=str(e)))
|
|
209
|
+
# Check if it looks like a SQL query or run as a tb command
|
|
210
|
+
self.default(line)
|
|
47
211
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
else:
|
|
51
|
-
click.echo(FeedbackManager.error(message="Invalid command"))
|
|
212
|
+
def handle_build(self, arg):
|
|
213
|
+
click.echo(FeedbackManager.error(message=f"'tb {arg}' command is not available in watch mode"))
|
|
52
214
|
|
|
53
|
-
def
|
|
54
|
-
|
|
55
|
-
|
|
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)
|
|
56
250
|
|
|
57
251
|
def run_sql(self, query, rows_limit=20):
|
|
58
252
|
try:
|
|
59
253
|
q = query.strip()
|
|
60
|
-
if q.startswith("insert"):
|
|
254
|
+
if q.lower().startswith("insert"):
|
|
61
255
|
click.echo(FeedbackManager.info_append_data())
|
|
62
256
|
raise CLIException(FeedbackManager.error_invalid_query())
|
|
63
|
-
if q.startswith("delete"):
|
|
257
|
+
if q.lower().startswith("delete"):
|
|
64
258
|
raise CLIException(FeedbackManager.error_invalid_query())
|
|
65
259
|
|
|
66
|
-
# fuck my life
|
|
67
260
|
def run_query_in_thread():
|
|
68
261
|
loop = asyncio.new_event_loop()
|
|
69
262
|
asyncio.set_event_loop(loop)
|
|
@@ -74,9 +267,6 @@ class BuildShell(cmd.Cmd):
|
|
|
74
267
|
finally:
|
|
75
268
|
loop.close()
|
|
76
269
|
|
|
77
|
-
# Run the query in a separate thread
|
|
78
|
-
import concurrent.futures
|
|
79
|
-
|
|
80
270
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
81
271
|
res = executor.submit(run_query_in_thread).result()
|
|
82
272
|
|
|
@@ -91,6 +281,9 @@ class BuildShell(cmd.Cmd):
|
|
|
91
281
|
else:
|
|
92
282
|
click.echo(FeedbackManager.info_no_rows())
|
|
93
283
|
|
|
284
|
+
def reprint_prompt(self):
|
|
285
|
+
click.echo(f"{bcolors.OKGREEN}{self.prompt_message}{bcolors.ENDC}", nl=False)
|
|
286
|
+
|
|
94
287
|
|
|
95
288
|
def print_table_formatted(res: dict, name: str):
|
|
96
289
|
rebuild_colors = [bcolors.FAIL, bcolors.OKBLUE, bcolors.WARNING, bcolors.OKGREEN, bcolors.HEADER]
|
tinybird/tb/modules/cicd.py
CHANGED
|
@@ -14,9 +14,7 @@ class Provider(Enum):
|
|
|
14
14
|
GitLab = 1
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
WORKFLOW_VERSION = "
|
|
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"
|
|
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,
|