tinybird 0.0.1.dev29__py3-none-any.whl → 0.0.1.dev31__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tinybird might be problematic. Click here for more details.
- tinybird/__cli__.py +2 -2
- tinybird/client.py +1 -1
- tinybird/config.py +1 -0
- tinybird/context.py +1 -0
- tinybird/prompts.py +218 -325
- tinybird/sql_template.py +21 -2
- tinybird/tb/__cli__.py +2 -2
- tinybird/tb/cli.py +2 -1
- tinybird/tb/modules/build.py +75 -188
- tinybird/tb/modules/build_client.py +219 -0
- tinybird/tb/modules/cli.py +53 -16
- tinybird/tb/modules/common.py +1 -26
- tinybird/tb/modules/config.py +0 -8
- tinybird/tb/modules/create.py +22 -3
- tinybird/tb/modules/datafile/build.py +2 -2
- tinybird/tb/modules/datafile/build_pipe.py +13 -1
- tinybird/tb/modules/datasource.py +1 -1
- tinybird/tb/modules/llm.py +6 -73
- tinybird/tb/modules/local.py +1 -1
- tinybird/tb/modules/local_common.py +1 -1
- tinybird/tb/modules/login.py +7 -2
- tinybird/tb/modules/mock.py +1 -1
- tinybird/tb/modules/shell.py +30 -35
- tinybird/tb/modules/test.py +41 -22
- tinybird/tb/modules/update.py +182 -0
- tinybird/tb/modules/watch.py +76 -1
- {tinybird-0.0.1.dev29.dist-info → tinybird-0.0.1.dev31.dist-info}/METADATA +2 -1
- {tinybird-0.0.1.dev29.dist-info → tinybird-0.0.1.dev31.dist-info}/RECORD +31 -30
- tinybird/tb/modules/build_server.py +0 -75
- {tinybird-0.0.1.dev29.dist-info → tinybird-0.0.1.dev31.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev29.dist-info → tinybird-0.0.1.dev31.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev29.dist-info → tinybird-0.0.1.dev31.dist-info}/top_level.txt +0 -0
tinybird/sql_template.py
CHANGED
|
@@ -12,7 +12,11 @@ from typing import Any, Dict, List, Optional, Tuple, Union
|
|
|
12
12
|
from tornado import escape
|
|
13
13
|
from tornado.util import ObjectDict, exec_in, unicode_type
|
|
14
14
|
|
|
15
|
-
from tinybird.context import
|
|
15
|
+
from tinybird.context import (
|
|
16
|
+
ff_column_json_backticks_circuit_breaker,
|
|
17
|
+
ff_preprocess_parameters_circuit_breaker,
|
|
18
|
+
ff_split_to_array_escape,
|
|
19
|
+
)
|
|
16
20
|
|
|
17
21
|
from .datatypes import testers
|
|
18
22
|
from .tornado_template import VALID_CUSTOM_FUNCTION_NAMES, SecurityException, Template
|
|
@@ -271,6 +275,8 @@ def columns(x, default=None, fn=None):
|
|
|
271
275
|
|
|
272
276
|
|
|
273
277
|
def column(x, default=None):
|
|
278
|
+
bypass_colunn_json_backticks = ff_column_json_backticks_circuit_breaker.get(False)
|
|
279
|
+
|
|
274
280
|
if x is None or isinstance(x, Placeholder):
|
|
275
281
|
if default is None:
|
|
276
282
|
raise SQLTemplateException(
|
|
@@ -278,7 +284,18 @@ def column(x, default=None):
|
|
|
278
284
|
documentation="/cli/advanced-templates.html#column",
|
|
279
285
|
)
|
|
280
286
|
x = default
|
|
281
|
-
|
|
287
|
+
|
|
288
|
+
quote = "`"
|
|
289
|
+
if bypass_colunn_json_backticks:
|
|
290
|
+
return Symbol(quote + sqlescape(x) + quote)
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
slices = x.split(".")
|
|
294
|
+
escaped_slices = [quote + sqlescape(s) + quote for s in slices]
|
|
295
|
+
escaped = ".".join(escaped_slices)
|
|
296
|
+
return Symbol(escaped)
|
|
297
|
+
except Exception: # in case there's a problem with .split
|
|
298
|
+
return Symbol(quote + sqlescape(x) + quote)
|
|
282
299
|
|
|
283
300
|
|
|
284
301
|
def symbol(x, quote="`"):
|
|
@@ -2109,6 +2126,8 @@ def render_sql_template(
|
|
|
2109
2126
|
('SELECT `foo`', {}, [])
|
|
2110
2127
|
>>> render_sql_template("SELECT {{column(agg)}}", {'agg': '"foo"'})
|
|
2111
2128
|
('SELECT `foo`', {}, [])
|
|
2129
|
+
>>> render_sql_template("SELECT {{column(agg)}}", {'agg': 'json.a'})
|
|
2130
|
+
('SELECT `json`.`a`', {}, [])
|
|
2112
2131
|
>>> render_sql_template('{% if not defined(test) %}error("This is an error"){% end %}', {})
|
|
2113
2132
|
('error("This is an error")', {}, [])
|
|
2114
2133
|
>>> render_sql_template('{% if not defined(test) %}custom_error({error: "This is an error"}){% end %}', {})
|
tinybird/tb/__cli__.py
CHANGED
|
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
|
|
|
4
4
|
__url__ = 'https://www.tinybird.co/docs/cli/introduction.html'
|
|
5
5
|
__author__ = 'Tinybird'
|
|
6
6
|
__author_email__ = 'support@tinybird.co'
|
|
7
|
-
__version__ = '0.0.1.
|
|
8
|
-
__revision__ = '
|
|
7
|
+
__version__ = '0.0.1.dev31'
|
|
8
|
+
__revision__ = '0e056e8'
|
tinybird/tb/cli.py
CHANGED
|
@@ -6,7 +6,7 @@ if sys.platform == "win32":
|
|
|
6
6
|
|
|
7
7
|
import tinybird.tb.modules.auth
|
|
8
8
|
import tinybird.tb.modules.build
|
|
9
|
-
import tinybird.tb.modules.
|
|
9
|
+
import tinybird.tb.modules.build_client
|
|
10
10
|
import tinybird.tb.modules.cli
|
|
11
11
|
import tinybird.tb.modules.common
|
|
12
12
|
import tinybird.tb.modules.connection
|
|
@@ -22,6 +22,7 @@ import tinybird.tb.modules.pipe
|
|
|
22
22
|
import tinybird.tb.modules.tag
|
|
23
23
|
import tinybird.tb.modules.test
|
|
24
24
|
import tinybird.tb.modules.token
|
|
25
|
+
import tinybird.tb.modules.update
|
|
25
26
|
import tinybird.tb.modules.workspace
|
|
26
27
|
import tinybird.tb.modules.workspace_members
|
|
27
28
|
|
tinybird/tb/modules/build.py
CHANGED
|
@@ -1,214 +1,101 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import
|
|
2
|
+
import glob
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
3
5
|
import threading
|
|
4
|
-
import time
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import List
|
|
7
8
|
|
|
8
9
|
import click
|
|
10
|
+
import requests
|
|
9
11
|
|
|
10
|
-
import tinybird.context as context
|
|
11
12
|
from tinybird.client import TinyB
|
|
12
|
-
from tinybird.config import FeatureFlags
|
|
13
13
|
from tinybird.tb.modules.cli import cli
|
|
14
|
-
from tinybird.tb.modules.common import push_data
|
|
15
|
-
from tinybird.tb.modules.datafile.build import folder_build
|
|
16
|
-
from tinybird.tb.modules.datafile.common import get_project_filenames, get_project_fixtures, has_internal_datafiles
|
|
17
|
-
from tinybird.tb.modules.datafile.exceptions import ParseException
|
|
18
|
-
from tinybird.tb.modules.datafile.fixture import build_fixture_name, get_fixture_dir
|
|
19
|
-
from tinybird.tb.modules.datafile.parse_datasource import parse_datasource
|
|
20
|
-
from tinybird.tb.modules.datafile.parse_pipe import parse_pipe
|
|
21
14
|
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
22
15
|
from tinybird.tb.modules.local_common import get_tinybird_local_client
|
|
23
|
-
from tinybird.tb.modules.shell import Shell
|
|
24
|
-
from tinybird.tb.modules.watch import
|
|
16
|
+
from tinybird.tb.modules.shell import Shell
|
|
17
|
+
from tinybird.tb.modules.watch import watch_project
|
|
25
18
|
|
|
26
19
|
|
|
27
20
|
@cli.command()
|
|
28
|
-
@click.option(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
)
|
|
35
|
-
@click.option(
|
|
36
|
-
"--watch",
|
|
37
|
-
is_flag=True,
|
|
38
|
-
help="Watch for changes in the files and rebuild them.",
|
|
39
|
-
)
|
|
40
|
-
def build(
|
|
41
|
-
folder: str,
|
|
42
|
-
watch: bool,
|
|
43
|
-
) -> None:
|
|
44
|
-
"""Build the project in Tinybird Local."""
|
|
45
|
-
ignore_sql_errors = FeatureFlags.ignore_sql_errors()
|
|
46
|
-
context.disable_template_security_validation.set(True)
|
|
47
|
-
is_internal = has_internal_datafiles(folder)
|
|
48
|
-
folder_path = os.path.abspath(folder)
|
|
49
|
-
tb_client = asyncio.run(get_tinybird_local_client(folder_path))
|
|
21
|
+
@click.option("--folder", type=str, default=".")
|
|
22
|
+
@click.option("--watch", is_flag=True, default=False, help="Watch for changes and rebuild automatically")
|
|
23
|
+
def build(folder: str, watch: bool) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Validate and build the project server side.
|
|
26
|
+
"""
|
|
50
27
|
|
|
51
|
-
|
|
52
|
-
parser_matrix = {".pipe": parse_pipe, ".datasource": parse_datasource}
|
|
53
|
-
incl_suffix = ".incl"
|
|
28
|
+
tb_client = asyncio.run(get_tinybird_local_client(folder))
|
|
54
29
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
check_filenames(filenames=get_project_filenames(filename))
|
|
30
|
+
def process() -> None:
|
|
31
|
+
build_project(folder, tb_client)
|
|
58
32
|
|
|
59
|
-
|
|
60
|
-
if file_suffix == incl_suffix:
|
|
61
|
-
continue
|
|
62
|
-
|
|
63
|
-
parser = parser_matrix.get(file_suffix)
|
|
64
|
-
if not parser:
|
|
65
|
-
raise ParseException(FeedbackManager.error_unsupported_datafile(extension=file_suffix))
|
|
66
|
-
|
|
67
|
-
parser(filename)
|
|
68
|
-
|
|
69
|
-
async def process(filenames: List[str], watch: bool = False):
|
|
70
|
-
datafiles = [f for f in filenames if f.endswith(".datasource") or f.endswith(".pipe")]
|
|
71
|
-
if len(datafiles) > 0:
|
|
72
|
-
check_filenames(filenames=datafiles)
|
|
73
|
-
await folder_build(
|
|
74
|
-
tb_client,
|
|
75
|
-
filenames=datafiles,
|
|
76
|
-
ignore_sql_errors=ignore_sql_errors,
|
|
77
|
-
is_internal=is_internal,
|
|
78
|
-
watch=watch,
|
|
79
|
-
folder=folder,
|
|
80
|
-
)
|
|
81
|
-
if len(filenames) > 0:
|
|
82
|
-
filename = filenames[0]
|
|
83
|
-
if filename.endswith(".ndjson"):
|
|
84
|
-
fixture_path = Path(filename)
|
|
85
|
-
datasources_path = Path(folder) / "datasources"
|
|
86
|
-
ds_name = fixture_path.stem
|
|
87
|
-
ds_path = datasources_path / f"{ds_name}.datasource"
|
|
88
|
-
|
|
89
|
-
if not ds_path.exists():
|
|
90
|
-
try:
|
|
91
|
-
ds_name = "_".join(fixture_path.stem.split("_")[:-1])
|
|
92
|
-
ds_path = datasources_path / f"{ds_name}.datasource"
|
|
93
|
-
except Exception:
|
|
94
|
-
pass
|
|
95
|
-
|
|
96
|
-
if ds_path.exists():
|
|
97
|
-
await append_datasource(tb_client, ds_name, str(fixture_path))
|
|
98
|
-
|
|
99
|
-
if watch:
|
|
100
|
-
if filename.endswith(".datasource"):
|
|
101
|
-
ds_path = Path(filename)
|
|
102
|
-
ds_name = ds_path.stem
|
|
103
|
-
name = build_fixture_name(filename, ds_name, ds_path.read_text())
|
|
104
|
-
fixture_folder = get_fixture_dir(folder)
|
|
105
|
-
fixture_path = fixture_folder / f"{name}.ndjson"
|
|
106
|
-
|
|
107
|
-
if not fixture_path.exists():
|
|
108
|
-
fixture_path = fixture_folder / f"{ds_name}.ndjson"
|
|
109
|
-
|
|
110
|
-
if fixture_path.exists():
|
|
111
|
-
await append_datasource(tb_client, ds_name, str(fixture_path))
|
|
112
|
-
|
|
113
|
-
if not filename.endswith(".ndjson"):
|
|
114
|
-
await build_and_print_resource(tb_client, filename)
|
|
115
|
-
|
|
116
|
-
datafiles = get_project_filenames(folder)
|
|
117
|
-
fixtures = get_project_fixtures(folder)
|
|
118
|
-
filenames = datafiles + fixtures
|
|
119
|
-
|
|
120
|
-
async def build_once(filenames: List[str]):
|
|
121
|
-
ok = False
|
|
122
|
-
try:
|
|
123
|
-
click.echo(FeedbackManager.highlight(message="» Building project...\n"))
|
|
124
|
-
time_start = time.time()
|
|
125
|
-
await process(filenames=filenames, watch=False)
|
|
126
|
-
time_end = time.time()
|
|
127
|
-
elapsed_time = time_end - time_start
|
|
128
|
-
for filename in filenames:
|
|
129
|
-
if filename.endswith(".datasource"):
|
|
130
|
-
ds_path = Path(filename)
|
|
131
|
-
ds_name = ds_path.stem
|
|
132
|
-
name = build_fixture_name(filename, ds_name, ds_path.read_text())
|
|
133
|
-
fixture_folder = get_fixture_dir(folder)
|
|
134
|
-
fixture_path = fixture_folder / f"{name}.ndjson"
|
|
135
|
-
|
|
136
|
-
if not fixture_path.exists():
|
|
137
|
-
fixture_path = fixture_folder / f"{ds_name}.ndjson"
|
|
138
|
-
|
|
139
|
-
if fixture_path.exists():
|
|
140
|
-
await append_datasource(tb_client, ds_name, str(fixture_path))
|
|
141
|
-
click.echo(FeedbackManager.success(message=f"\n✓ Build completed in {elapsed_time:.1f}s"))
|
|
142
|
-
ok = True
|
|
143
|
-
except Exception as e:
|
|
144
|
-
error_path = Path(".tb_error.txt")
|
|
145
|
-
if error_path.exists():
|
|
146
|
-
content = error_path.read_text()
|
|
147
|
-
content += f"\n\n{str(e)}"
|
|
148
|
-
error_path.write_text(content)
|
|
149
|
-
else:
|
|
150
|
-
error_path.write_text(str(e))
|
|
151
|
-
click.echo(FeedbackManager.error_exception(error=e))
|
|
152
|
-
ok = False
|
|
153
|
-
return ok
|
|
154
|
-
|
|
155
|
-
build_ok = asyncio.run(build_once(filenames))
|
|
33
|
+
process()
|
|
156
34
|
|
|
157
35
|
if watch:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def is_vendor(f: Path) -> bool:
|
|
161
|
-
return f.parts[0] == "vendor"
|
|
162
|
-
|
|
163
|
-
def get_vendor_workspace(f: Path) -> str:
|
|
164
|
-
return f.parts[1]
|
|
165
|
-
|
|
166
|
-
def is_endpoint(f: Path) -> bool:
|
|
167
|
-
return f.suffix == ".pipe" and not is_vendor(f) and f.parts[0] == "endpoints"
|
|
168
|
-
|
|
169
|
-
def is_pipe(f: Path) -> bool:
|
|
170
|
-
return f.suffix == ".pipe" and not is_vendor(f)
|
|
171
|
-
|
|
172
|
-
datasource_paths = [f for f in paths if f.suffix == ".datasource"]
|
|
173
|
-
datasources = [f.stem for f in datasource_paths if not is_vendor(f)]
|
|
174
|
-
shared_datasources = [f"{get_vendor_workspace(f)}.{f.stem}" for f in datasource_paths if is_vendor(f)]
|
|
175
|
-
pipes = [f.stem for f in paths if is_pipe(f) and not is_endpoint(f)]
|
|
176
|
-
endpoints = [f.stem for f in paths if is_endpoint(f)]
|
|
177
|
-
shell = Shell(
|
|
178
|
-
folder=folder,
|
|
179
|
-
client=tb_client,
|
|
180
|
-
datasources=datasources,
|
|
181
|
-
shared_datasources=shared_datasources,
|
|
182
|
-
pipes=pipes,
|
|
183
|
-
endpoints=endpoints,
|
|
184
|
-
)
|
|
36
|
+
shell = Shell(folder=folder, client=tb_client)
|
|
185
37
|
click.echo(FeedbackManager.gray(message="\nWatching for changes..."))
|
|
186
38
|
watcher_thread = threading.Thread(
|
|
187
|
-
target=
|
|
39
|
+
target=watch_project,
|
|
40
|
+
args=(shell, process, folder),
|
|
41
|
+
daemon=True,
|
|
188
42
|
)
|
|
189
43
|
watcher_thread.start()
|
|
190
44
|
shell.run()
|
|
191
45
|
|
|
192
46
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
47
|
+
def get_project_files(project_path: Path) -> List[str]:
|
|
48
|
+
project_file_extensions = ("datasource", "pipe")
|
|
49
|
+
project_files = []
|
|
50
|
+
for extension in project_file_extensions:
|
|
51
|
+
for project_file in glob.glob(f"{project_path}/**/*.{extension}", recursive=True):
|
|
52
|
+
logging.debug(f"Found project file: {project_file}")
|
|
53
|
+
project_files.append(project_file)
|
|
54
|
+
return project_files
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def build_project(folder: str, tb_client: TinyB) -> None:
|
|
58
|
+
MULTIPART_BOUNDARY_DATA_PROJECT = "data_project://"
|
|
59
|
+
DATAFILE_TYPE_TO_CONTENT_TYPE = {
|
|
60
|
+
".datasource": "text/plain",
|
|
61
|
+
".pipe": "text/plain",
|
|
62
|
+
}
|
|
63
|
+
TINYBIRD_API_URL = tb_client.host + "/v1/build"
|
|
64
|
+
TINYBIRD_API_KEY = tb_client.token
|
|
65
|
+
try:
|
|
66
|
+
files = [
|
|
67
|
+
("context://", ("cli-version", "1.0.0", "text/plain")),
|
|
68
|
+
]
|
|
69
|
+
fds = []
|
|
70
|
+
project_path = Path(folder)
|
|
71
|
+
project_files = get_project_files(project_path)
|
|
72
|
+
for file_path in project_files:
|
|
73
|
+
relative_path = str(Path(file_path).relative_to(project_path))
|
|
74
|
+
fd = open(file_path, "rb")
|
|
75
|
+
fds.append(fd)
|
|
76
|
+
content_type = DATAFILE_TYPE_TO_CONTENT_TYPE.get(Path(file_path).suffix, "application/unknown")
|
|
77
|
+
files.append((MULTIPART_BOUNDARY_DATA_PROJECT, (relative_path, fd.read().decode("utf-8"), content_type)))
|
|
78
|
+
HEADERS = {"Authorization": f"Bearer {TINYBIRD_API_KEY}"}
|
|
79
|
+
|
|
80
|
+
r = requests.post(TINYBIRD_API_URL, files=files, headers=HEADERS)
|
|
81
|
+
result = r.json()
|
|
82
|
+
|
|
83
|
+
logging.debug(json.dumps(result, indent=2))
|
|
84
|
+
|
|
85
|
+
build_result = result.get("result")
|
|
86
|
+
if build_result == "success":
|
|
87
|
+
click.echo(FeedbackManager.success(message="Build completed successfully"))
|
|
88
|
+
elif build_result == "failed":
|
|
89
|
+
click.echo(FeedbackManager.error(message="Build failed"))
|
|
90
|
+
build_errors = result.get("errors")
|
|
91
|
+
for build_error in build_errors:
|
|
92
|
+
filename_bit = f"{build_error.get('filename', '')}"
|
|
93
|
+
error_msg = (filename_bit + "\n\n") if filename_bit else "" + build_error.get("error")
|
|
94
|
+
click.echo(FeedbackManager.error(message=error_msg))
|
|
95
|
+
else:
|
|
96
|
+
click.echo(FeedbackManager.error(message=f"Unknown build result. Error: {result.get('error')}"))
|
|
97
|
+
except Exception as e:
|
|
98
|
+
click.echo(FeedbackManager.error_exception(error="Error building project: " + str(e)))
|
|
99
|
+
finally:
|
|
100
|
+
for fd in fds:
|
|
101
|
+
fd.close()
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
import tinybird.context as context
|
|
11
|
+
from tinybird.client import TinyB
|
|
12
|
+
from tinybird.config import FeatureFlags
|
|
13
|
+
from tinybird.tb.modules.cli import cli
|
|
14
|
+
from tinybird.tb.modules.common import push_data
|
|
15
|
+
from tinybird.tb.modules.datafile.build import folder_build
|
|
16
|
+
from tinybird.tb.modules.datafile.common import get_project_filenames, get_project_fixtures, has_internal_datafiles
|
|
17
|
+
from tinybird.tb.modules.datafile.exceptions import ParseException
|
|
18
|
+
from tinybird.tb.modules.datafile.fixture import build_fixture_name, get_fixture_dir
|
|
19
|
+
from tinybird.tb.modules.datafile.parse_datasource import parse_datasource
|
|
20
|
+
from tinybird.tb.modules.datafile.parse_pipe import parse_pipe
|
|
21
|
+
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
22
|
+
from tinybird.tb.modules.local_common import get_tinybird_local_client
|
|
23
|
+
from tinybird.tb.modules.shell import Shell, print_table_formatted
|
|
24
|
+
from tinybird.tb.modules.watch import watch_files
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_vendor(f: Path) -> bool:
|
|
28
|
+
return f.parts[0] == "vendor"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_vendor_workspace(f: Path) -> str:
|
|
32
|
+
return f.parts[1]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def is_endpoint(f: Path) -> bool:
|
|
36
|
+
return f.suffix == ".pipe" and not is_vendor(f) and f.parts[0] == "endpoints"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def is_pipe(f: Path) -> bool:
|
|
40
|
+
return f.suffix == ".pipe" and not is_vendor(f)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def check_filenames(filenames: List[str]):
|
|
44
|
+
parser_matrix = {".pipe": parse_pipe, ".datasource": parse_datasource}
|
|
45
|
+
incl_suffix = ".incl"
|
|
46
|
+
|
|
47
|
+
for filename in filenames:
|
|
48
|
+
if os.path.isdir(filename):
|
|
49
|
+
check_filenames(filenames=get_project_filenames(filename))
|
|
50
|
+
|
|
51
|
+
file_suffix = Path(filename).suffix
|
|
52
|
+
if file_suffix == incl_suffix:
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
parser = parser_matrix.get(file_suffix)
|
|
56
|
+
if not parser:
|
|
57
|
+
raise ParseException(FeedbackManager.error_unsupported_datafile(extension=file_suffix))
|
|
58
|
+
|
|
59
|
+
parser(filename)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@cli.command()
|
|
63
|
+
@click.option(
|
|
64
|
+
"--folder",
|
|
65
|
+
default=".",
|
|
66
|
+
help="Folder from where to execute the command. By default the current folder.",
|
|
67
|
+
hidden=True,
|
|
68
|
+
type=click.types.STRING,
|
|
69
|
+
)
|
|
70
|
+
@click.option(
|
|
71
|
+
"--watch",
|
|
72
|
+
is_flag=True,
|
|
73
|
+
help="Watch for changes in the files and rebuild them.",
|
|
74
|
+
)
|
|
75
|
+
def build_client(
|
|
76
|
+
folder: str,
|
|
77
|
+
watch: bool,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Build the project in Tinybird Local."""
|
|
80
|
+
ignore_sql_errors = FeatureFlags.ignore_sql_errors()
|
|
81
|
+
context.disable_template_security_validation.set(True)
|
|
82
|
+
is_internal = has_internal_datafiles(folder)
|
|
83
|
+
folder_path = os.path.abspath(folder)
|
|
84
|
+
tb_client = asyncio.run(get_tinybird_local_client(folder_path))
|
|
85
|
+
|
|
86
|
+
async def process(filenames: List[str], watch: bool = False):
|
|
87
|
+
datafiles = [f for f in filenames if f.endswith(".datasource") or f.endswith(".pipe")]
|
|
88
|
+
if len(datafiles) > 0:
|
|
89
|
+
check_filenames(filenames=datafiles)
|
|
90
|
+
await folder_build(
|
|
91
|
+
tb_client,
|
|
92
|
+
filenames=datafiles,
|
|
93
|
+
ignore_sql_errors=ignore_sql_errors,
|
|
94
|
+
is_internal=is_internal,
|
|
95
|
+
watch=watch,
|
|
96
|
+
folder=folder,
|
|
97
|
+
)
|
|
98
|
+
if len(filenames) > 0:
|
|
99
|
+
filename = filenames[0]
|
|
100
|
+
if filename.endswith(".ndjson"):
|
|
101
|
+
fixture_path = Path(filename)
|
|
102
|
+
datasources_path = Path(folder) / "datasources"
|
|
103
|
+
ds_name = fixture_path.stem
|
|
104
|
+
ds_path = datasources_path / f"{ds_name}.datasource"
|
|
105
|
+
|
|
106
|
+
if not ds_path.exists():
|
|
107
|
+
try:
|
|
108
|
+
ds_name = "_".join(fixture_path.stem.split("_")[:-1])
|
|
109
|
+
ds_path = datasources_path / f"{ds_name}.datasource"
|
|
110
|
+
except Exception:
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
if ds_path.exists():
|
|
114
|
+
await append_datasource(tb_client, ds_name, str(fixture_path))
|
|
115
|
+
|
|
116
|
+
if watch:
|
|
117
|
+
if filename.endswith(".datasource"):
|
|
118
|
+
ds_path = Path(filename)
|
|
119
|
+
ds_name = ds_path.stem
|
|
120
|
+
name = build_fixture_name(filename, ds_name, ds_path.read_text())
|
|
121
|
+
fixture_folder = get_fixture_dir(folder)
|
|
122
|
+
fixture_path = fixture_folder / f"{name}.ndjson"
|
|
123
|
+
|
|
124
|
+
if not fixture_path.exists():
|
|
125
|
+
fixture_path = fixture_folder / f"{ds_name}.ndjson"
|
|
126
|
+
|
|
127
|
+
if fixture_path.exists():
|
|
128
|
+
await append_datasource(tb_client, ds_name, str(fixture_path))
|
|
129
|
+
|
|
130
|
+
if not filename.endswith(".ndjson"):
|
|
131
|
+
await build_and_print_resource(tb_client, filename)
|
|
132
|
+
|
|
133
|
+
datafiles = get_project_filenames(folder)
|
|
134
|
+
fixtures = get_project_fixtures(folder)
|
|
135
|
+
filenames = datafiles + fixtures
|
|
136
|
+
|
|
137
|
+
async def build_once(filenames: List[str]):
|
|
138
|
+
ok = False
|
|
139
|
+
try:
|
|
140
|
+
click.echo(FeedbackManager.highlight(message="» Building project...\n"))
|
|
141
|
+
time_start = time.time()
|
|
142
|
+
await process(filenames=filenames, watch=False)
|
|
143
|
+
time_end = time.time()
|
|
144
|
+
elapsed_time = time_end - time_start
|
|
145
|
+
for filename in filenames:
|
|
146
|
+
if filename.endswith(".datasource"):
|
|
147
|
+
ds_path = Path(filename)
|
|
148
|
+
ds_name = ds_path.stem
|
|
149
|
+
name = build_fixture_name(filename, ds_name, ds_path.read_text())
|
|
150
|
+
fixture_folder = get_fixture_dir(folder)
|
|
151
|
+
fixture_path = fixture_folder / f"{name}.ndjson"
|
|
152
|
+
|
|
153
|
+
if not fixture_path.exists():
|
|
154
|
+
fixture_path = fixture_folder / f"{ds_name}.ndjson"
|
|
155
|
+
|
|
156
|
+
if fixture_path.exists():
|
|
157
|
+
await append_datasource(tb_client, ds_name, str(fixture_path))
|
|
158
|
+
click.echo(FeedbackManager.success(message=f"\n✓ Build completed in {elapsed_time:.1f}s"))
|
|
159
|
+
ok = True
|
|
160
|
+
except Exception as e:
|
|
161
|
+
error_path = Path(".tb_error.txt")
|
|
162
|
+
if error_path.exists():
|
|
163
|
+
content = error_path.read_text()
|
|
164
|
+
content += f"\n\n{str(e)}"
|
|
165
|
+
error_path.write_text(content)
|
|
166
|
+
else:
|
|
167
|
+
error_path.write_text(str(e))
|
|
168
|
+
click.echo(FeedbackManager.error_exception(error=e))
|
|
169
|
+
ok = False
|
|
170
|
+
return ok
|
|
171
|
+
|
|
172
|
+
build_ok = asyncio.run(build_once(filenames))
|
|
173
|
+
|
|
174
|
+
if watch:
|
|
175
|
+
paths = [Path(f) for f in get_project_filenames(folder, with_vendor=True)]
|
|
176
|
+
|
|
177
|
+
datasource_paths = [f for f in paths if f.suffix == ".datasource"]
|
|
178
|
+
datasources = [f.stem for f in datasource_paths if not is_vendor(f)]
|
|
179
|
+
shared_datasources = [f"{get_vendor_workspace(f)}.{f.stem}" for f in datasource_paths if is_vendor(f)]
|
|
180
|
+
pipes = [f.stem for f in paths if is_pipe(f) and not is_endpoint(f)]
|
|
181
|
+
endpoints = [f.stem for f in paths if is_endpoint(f)]
|
|
182
|
+
shell = Shell(
|
|
183
|
+
folder=folder,
|
|
184
|
+
client=tb_client,
|
|
185
|
+
datasources=datasources,
|
|
186
|
+
shared_datasources=shared_datasources,
|
|
187
|
+
pipes=pipes,
|
|
188
|
+
endpoints=endpoints,
|
|
189
|
+
)
|
|
190
|
+
click.echo(FeedbackManager.gray(message="\nWatching for changes..."))
|
|
191
|
+
watcher_thread = threading.Thread(
|
|
192
|
+
target=watch_files, args=(filenames, process, shell, folder, build_ok), daemon=True
|
|
193
|
+
)
|
|
194
|
+
watcher_thread.start()
|
|
195
|
+
shell.run()
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
async def build_and_print_resource(tb_client: TinyB, filename: str):
|
|
199
|
+
resource_path = Path(filename)
|
|
200
|
+
name = resource_path.stem
|
|
201
|
+
pipeline = name if filename.endswith(".pipe") else None
|
|
202
|
+
res = await tb_client.query(f"SELECT * FROM {name} FORMAT JSON", pipeline=pipeline)
|
|
203
|
+
print_table_formatted(res, name)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
async def append_datasource(
|
|
207
|
+
tb_client: TinyB,
|
|
208
|
+
datasource_name: str,
|
|
209
|
+
url: str,
|
|
210
|
+
):
|
|
211
|
+
await tb_client.datasource_truncate(datasource_name)
|
|
212
|
+
await push_data(
|
|
213
|
+
tb_client,
|
|
214
|
+
datasource_name,
|
|
215
|
+
url,
|
|
216
|
+
mode="append",
|
|
217
|
+
concurrency=1,
|
|
218
|
+
silent=True,
|
|
219
|
+
)
|