tinybird 0.0.1.dev29__py3-none-any.whl → 0.0.1.dev30__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/client.py +1 -1
- tinybird/prompts.py +218 -325
- tinybird/tb/__cli__.py +2 -2
- tinybird/tb/cli.py +2 -1
- tinybird/tb/modules/build.py +61 -201
- tinybird/tb/modules/build_client.py +219 -0
- tinybird/tb/modules/cli.py +52 -16
- tinybird/tb/modules/common.py +1 -26
- tinybird/tb/modules/config.py +0 -8
- tinybird/tb/modules/create.py +20 -1
- 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 +19 -4
- tinybird/tb/modules/local.py +1 -1
- tinybird/tb/modules/login.py +7 -2
- tinybird/tb/modules/shell.py +2 -2
- tinybird/tb/modules/test.py +41 -22
- tinybird/tb/modules/update.py +182 -0
- {tinybird-0.0.1.dev29.dist-info → tinybird-0.0.1.dev30.dist-info}/METADATA +2 -1
- {tinybird-0.0.1.dev29.dist-info → tinybird-0.0.1.dev30.dist-info}/RECORD +24 -23
- tinybird/tb/modules/build_server.py +0 -75
- {tinybird-0.0.1.dev29.dist-info → tinybird-0.0.1.dev30.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev29.dist-info → tinybird-0.0.1.dev30.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev29.dist-info → tinybird-0.0.1.dev30.dist-info}/top_level.txt +0 -0
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,74 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
2
|
+
import glob
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import List
|
|
7
7
|
|
|
8
8
|
import click
|
|
9
|
+
import requests
|
|
9
10
|
|
|
10
|
-
import tinybird.context as context
|
|
11
|
-
from tinybird.client import TinyB
|
|
12
|
-
from tinybird.config import FeatureFlags
|
|
13
11
|
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
12
|
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
22
13
|
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
14
|
|
|
26
15
|
|
|
27
|
-
|
|
28
|
-
|
|
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))
|
|
50
|
-
|
|
51
|
-
def check_filenames(filenames: List[str]):
|
|
52
|
-
parser_matrix = {".pipe": parse_pipe, ".datasource": parse_datasource}
|
|
53
|
-
incl_suffix = ".incl"
|
|
54
|
-
|
|
55
|
-
for filename in filenames:
|
|
56
|
-
if os.path.isdir(filename):
|
|
57
|
-
check_filenames(filenames=get_project_filenames(filename))
|
|
58
|
-
|
|
59
|
-
file_suffix = Path(filename).suffix
|
|
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
|
|
16
|
+
def project_files(project_path: Path) -> List[str]:
|
|
17
|
+
project_file_extensions = ("datasource", "pipe")
|
|
18
|
+
project_files = []
|
|
19
|
+
for extension in project_file_extensions:
|
|
20
|
+
for project_file in glob.glob(f"{project_path}/**/*.{extension}", recursive=True):
|
|
21
|
+
logging.debug(f"Found project file: {project_file}")
|
|
22
|
+
project_files.append(project_file)
|
|
23
|
+
return project_files
|
|
119
24
|
|
|
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
25
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
click.echo(FeedbackManager.gray(message="\nWatching for changes..."))
|
|
186
|
-
watcher_thread = threading.Thread(
|
|
187
|
-
target=watch_files, args=(filenames, process, shell, folder, build_ok), daemon=True
|
|
188
|
-
)
|
|
189
|
-
watcher_thread.start()
|
|
190
|
-
shell.run()
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
async def build_and_print_resource(tb_client: TinyB, filename: str):
|
|
194
|
-
resource_path = Path(filename)
|
|
195
|
-
name = resource_path.stem
|
|
196
|
-
pipeline = name if filename.endswith(".pipe") else None
|
|
197
|
-
res = await tb_client.query(f"SELECT * FROM {name} FORMAT JSON", pipeline=pipeline)
|
|
198
|
-
print_table_formatted(res, name)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
async def append_datasource(
|
|
202
|
-
tb_client: TinyB,
|
|
203
|
-
datasource_name: str,
|
|
204
|
-
url: str,
|
|
205
|
-
):
|
|
206
|
-
await tb_client.datasource_truncate(datasource_name)
|
|
207
|
-
await push_data(
|
|
208
|
-
tb_client,
|
|
209
|
-
datasource_name,
|
|
210
|
-
url,
|
|
211
|
-
mode="append",
|
|
212
|
-
concurrency=1,
|
|
213
|
-
silent=True,
|
|
214
|
-
)
|
|
26
|
+
@cli.command()
|
|
27
|
+
@click.argument("project_path", type=click.Path(exists=True), default=Path.cwd())
|
|
28
|
+
def build(project_path: Path) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Validate and build the project server side.
|
|
31
|
+
"""
|
|
32
|
+
MULTIPART_BOUNDARY_DATA_PROJECT = "data_project://"
|
|
33
|
+
DATAFILE_TYPE_TO_CONTENT_TYPE = {
|
|
34
|
+
".datasource": "text/plain",
|
|
35
|
+
".pipe": "text/plain",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
tb_client = asyncio.run(get_tinybird_local_client(str(project_path)))
|
|
39
|
+
TINYBIRD_API_URL = tb_client.host + "/v1/build"
|
|
40
|
+
TINYBIRD_API_KEY = tb_client.token
|
|
41
|
+
|
|
42
|
+
files = [
|
|
43
|
+
("context://", ("cli-version", "1.0.0", "text/plain")),
|
|
44
|
+
]
|
|
45
|
+
fds = []
|
|
46
|
+
for file_path in project_files(project_path):
|
|
47
|
+
relative_path = str(Path(file_path).relative_to(project_path))
|
|
48
|
+
fd = open(file_path, "rb")
|
|
49
|
+
fds.append(fd)
|
|
50
|
+
content_type = DATAFILE_TYPE_TO_CONTENT_TYPE.get(Path(file_path).suffix, "application/unknown")
|
|
51
|
+
files.append((MULTIPART_BOUNDARY_DATA_PROJECT, (relative_path, fd.read().decode("utf-8"), content_type)))
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
HEADERS = {"Authorization": f"Bearer {TINYBIRD_API_KEY}"}
|
|
55
|
+
|
|
56
|
+
r = requests.post(TINYBIRD_API_URL, files=files, headers=HEADERS)
|
|
57
|
+
result = r.json()
|
|
58
|
+
logging.debug(json.dumps(result, indent=2))
|
|
59
|
+
|
|
60
|
+
build_result = result.get("result")
|
|
61
|
+
if build_result == "success":
|
|
62
|
+
click.echo(FeedbackManager.success(message="Build completed successfully"))
|
|
63
|
+
elif build_result == "failed":
|
|
64
|
+
click.echo(FeedbackManager.error(message="Build failed"))
|
|
65
|
+
build_errors = result.get("errors")
|
|
66
|
+
for build_error in build_errors:
|
|
67
|
+
filename_bit = f"{build_error.get('filename', '')}"
|
|
68
|
+
error_msg = (filename_bit + "\n\n") if filename_bit else "" + build_error.get("error")
|
|
69
|
+
click.echo(FeedbackManager.error(message=error_msg))
|
|
70
|
+
else:
|
|
71
|
+
click.echo(FeedbackManager.error(message=f"Unknown build result. Error: {result.get('error')}"))
|
|
72
|
+
finally:
|
|
73
|
+
for fd in fds:
|
|
74
|
+
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
|
+
)
|
tinybird/tb/modules/cli.py
CHANGED
|
@@ -9,7 +9,7 @@ import os
|
|
|
9
9
|
import pprint
|
|
10
10
|
from os import getcwd
|
|
11
11
|
from pathlib import Path
|
|
12
|
-
from typing import Any, Callable, Iterable, List, Optional, Tuple, Union
|
|
12
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
|
|
13
13
|
|
|
14
14
|
import click
|
|
15
15
|
import humanfriendly
|
|
@@ -20,7 +20,8 @@ from tinybird.client import (
|
|
|
20
20
|
AuthNoTokenException,
|
|
21
21
|
TinyB,
|
|
22
22
|
)
|
|
23
|
-
from tinybird.config import
|
|
23
|
+
from tinybird.config import get_config
|
|
24
|
+
from tinybird.prompts import ask_prompt
|
|
24
25
|
from tinybird.tb import __cli__
|
|
25
26
|
from tinybird.tb.modules.common import (
|
|
26
27
|
CatchAuthExceptions,
|
|
@@ -30,7 +31,6 @@ from tinybird.tb.modules.common import (
|
|
|
30
31
|
echo_safe_format_table,
|
|
31
32
|
get_current_main_workspace,
|
|
32
33
|
getenv_bool,
|
|
33
|
-
load_connector_config,
|
|
34
34
|
try_update_config_with_remote,
|
|
35
35
|
)
|
|
36
36
|
from tinybird.tb.modules.config import CLIConfig
|
|
@@ -94,7 +94,7 @@ VERSION = f"{__cli__.__version__} (rev {__cli__.__revision__})"
|
|
|
94
94
|
"--with-headers", help="Flag to enable connector to export with headers", is_flag=True, default=False, hidden=True
|
|
95
95
|
)
|
|
96
96
|
@click.option("--show-tokens", is_flag=True, default=False, help="Enable the output of tokens")
|
|
97
|
-
@click.option("--
|
|
97
|
+
@click.option("--prod", is_flag=True, default=False, help="Run against production")
|
|
98
98
|
@click.version_option(version=VERSION)
|
|
99
99
|
@click.pass_context
|
|
100
100
|
@coro
|
|
@@ -117,14 +117,13 @@ async def cli(
|
|
|
117
117
|
sf_stage,
|
|
118
118
|
with_headers: bool,
|
|
119
119
|
show_tokens: bool,
|
|
120
|
-
|
|
120
|
+
prod: bool,
|
|
121
121
|
) -> None:
|
|
122
122
|
"""
|
|
123
123
|
Use `OBFUSCATE_REGEX_PATTERN` and `OBFUSCATE_PATTERN_SEPARATOR` environment variables to define a regex pattern and a separator (in case of a single string with multiple regex) to obfuscate secrets in the CLI output.
|
|
124
124
|
"""
|
|
125
|
-
|
|
126
125
|
# We need to unpatch for our tests not to break
|
|
127
|
-
if show_tokens or
|
|
126
|
+
if show_tokens or not prod or ctx.invoked_subcommand == "build":
|
|
128
127
|
__unpatch_click_output()
|
|
129
128
|
else:
|
|
130
129
|
__patch_click_output()
|
|
@@ -211,12 +210,10 @@ async def cli(
|
|
|
211
210
|
|
|
212
211
|
logging.debug("debug enabled")
|
|
213
212
|
|
|
214
|
-
|
|
215
|
-
await get_tinybird_local_client() if local else _get_tb_client(config.get("token", None), config["host"])
|
|
216
|
-
)
|
|
213
|
+
client = await create_ctx_client(config, prod)
|
|
217
214
|
|
|
218
|
-
|
|
219
|
-
|
|
215
|
+
if client:
|
|
216
|
+
ctx.ensure_object(dict)["client"] = client
|
|
220
217
|
|
|
221
218
|
|
|
222
219
|
@cli.command(hidden=True)
|
|
@@ -489,11 +486,10 @@ async def sql(
|
|
|
489
486
|
|
|
490
487
|
@cli.command(hidden=True)
|
|
491
488
|
@click.argument("prompt")
|
|
492
|
-
@click.option("--model", default="gpt-4o", help="The model to use for the LLM")
|
|
493
489
|
@click.option("--folder", default=".", help="The folder to use for the project")
|
|
494
490
|
@coro
|
|
495
|
-
async def
|
|
496
|
-
"""
|
|
491
|
+
async def ask(prompt: str, folder: str) -> None:
|
|
492
|
+
"""Ask things about your data project."""
|
|
497
493
|
try:
|
|
498
494
|
config = CLIConfig.get_project_config(folder)
|
|
499
495
|
user_token = config.get_user_token()
|
|
@@ -501,9 +497,39 @@ async def llm(prompt: str, model: str, folder: str) -> None:
|
|
|
501
497
|
if not user_token:
|
|
502
498
|
raise CLIException("This action requires authentication. Run 'tb login' first.")
|
|
503
499
|
|
|
500
|
+
datasource_paths = [
|
|
501
|
+
Path(folder) / "datasources" / f
|
|
502
|
+
for f in os.listdir(Path(folder) / "datasources")
|
|
503
|
+
if f.endswith(".datasource")
|
|
504
|
+
]
|
|
505
|
+
pipe_folders = ["endpoints", "pipes", "copies", "materializations", "sinks"]
|
|
506
|
+
pipe_paths = [
|
|
507
|
+
Path(folder) / pipe_folder / f
|
|
508
|
+
for pipe_folder in pipe_folders
|
|
509
|
+
if (Path(folder) / pipe_folder).exists()
|
|
510
|
+
for f in os.listdir(Path(folder) / pipe_folder)
|
|
511
|
+
if f.endswith(".pipe")
|
|
512
|
+
]
|
|
513
|
+
resources_xml = "\n".join(
|
|
514
|
+
[
|
|
515
|
+
f"<resource><type>{resource_type}</type><name>{resource_name}</name><content>{resource_content}</content></resource>"
|
|
516
|
+
for resource_type, resource_name, resource_content in [
|
|
517
|
+
("datasource", ds.stem, ds.read_text()) for ds in datasource_paths
|
|
518
|
+
]
|
|
519
|
+
+ [
|
|
520
|
+
(
|
|
521
|
+
"pipe",
|
|
522
|
+
pipe.stem,
|
|
523
|
+
pipe.read_text(),
|
|
524
|
+
)
|
|
525
|
+
for pipe in pipe_paths
|
|
526
|
+
]
|
|
527
|
+
]
|
|
528
|
+
)
|
|
529
|
+
|
|
504
530
|
client = config.get_client()
|
|
505
531
|
llm = LLM(user_token=user_token, client=client)
|
|
506
|
-
click.echo(await llm.ask(prompt,
|
|
532
|
+
click.echo(await llm.ask(prompt, system_prompt=ask_prompt(resources_xml)))
|
|
507
533
|
except Exception as e:
|
|
508
534
|
raise CLIException(FeedbackManager.error_exception(error=e))
|
|
509
535
|
|
|
@@ -543,3 +569,13 @@ def __patch_click_output():
|
|
|
543
569
|
def __unpatch_click_output():
|
|
544
570
|
click.echo = __old_click_echo
|
|
545
571
|
click.secho = __old_click_secho
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
async def create_ctx_client(config: Dict[str, Any], prod: bool):
|
|
575
|
+
if prod:
|
|
576
|
+
return _get_tb_client(config.get("token", None), config["host"])
|
|
577
|
+
|
|
578
|
+
try:
|
|
579
|
+
return await get_tinybird_local_client()
|
|
580
|
+
except Exception:
|
|
581
|
+
return None
|