tinybird 0.0.1.dev291__py3-none-any.whl → 1.0.5__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/ch_utils/constants.py +5 -0
- tinybird/connectors.py +1 -7
- tinybird/context.py +3 -3
- tinybird/datafile/common.py +10 -8
- tinybird/datafile/parse_pipe.py +2 -2
- tinybird/feedback_manager.py +3 -0
- tinybird/prompts.py +1 -0
- tinybird/service_datasources.py +223 -0
- tinybird/sql_template.py +26 -11
- tinybird/sql_template_fmt.py +14 -4
- tinybird/tb/__cli__.py +2 -2
- tinybird/tb/cli.py +1 -0
- tinybird/tb/client.py +104 -26
- tinybird/tb/config.py +24 -0
- tinybird/tb/modules/agent/agent.py +103 -67
- tinybird/tb/modules/agent/banner.py +15 -15
- tinybird/tb/modules/agent/explore_agent.py +5 -0
- tinybird/tb/modules/agent/mock_agent.py +5 -1
- tinybird/tb/modules/agent/models.py +6 -2
- tinybird/tb/modules/agent/prompts.py +49 -2
- tinybird/tb/modules/agent/tools/deploy.py +1 -1
- tinybird/tb/modules/agent/tools/execute_query.py +15 -18
- tinybird/tb/modules/agent/tools/request_endpoint.py +1 -1
- tinybird/tb/modules/agent/tools/run_command.py +9 -0
- tinybird/tb/modules/agent/utils.py +38 -48
- tinybird/tb/modules/branch.py +150 -0
- tinybird/tb/modules/build.py +58 -13
- tinybird/tb/modules/build_common.py +209 -25
- tinybird/tb/modules/cli.py +129 -16
- tinybird/tb/modules/common.py +172 -146
- tinybird/tb/modules/connection.py +125 -194
- tinybird/tb/modules/connection_kafka.py +382 -0
- tinybird/tb/modules/copy.py +3 -1
- tinybird/tb/modules/create.py +83 -150
- tinybird/tb/modules/datafile/build.py +27 -38
- tinybird/tb/modules/datafile/build_datasource.py +21 -25
- tinybird/tb/modules/datafile/diff.py +1 -1
- tinybird/tb/modules/datafile/format_pipe.py +46 -7
- tinybird/tb/modules/datafile/playground.py +59 -68
- tinybird/tb/modules/datafile/pull.py +2 -3
- tinybird/tb/modules/datasource.py +477 -308
- tinybird/tb/modules/deployment.py +2 -0
- tinybird/tb/modules/deployment_common.py +84 -44
- tinybird/tb/modules/deprecations.py +4 -4
- tinybird/tb/modules/dev_server.py +33 -12
- tinybird/tb/modules/exceptions.py +14 -0
- tinybird/tb/modules/feedback_manager.py +1 -1
- tinybird/tb/modules/info.py +69 -12
- tinybird/tb/modules/infra.py +4 -5
- tinybird/tb/modules/job_common.py +15 -0
- tinybird/tb/modules/local.py +143 -23
- tinybird/tb/modules/local_common.py +347 -19
- tinybird/tb/modules/local_logs.py +209 -0
- tinybird/tb/modules/login.py +21 -2
- tinybird/tb/modules/login_common.py +254 -12
- tinybird/tb/modules/mock.py +5 -54
- tinybird/tb/modules/mock_common.py +0 -54
- tinybird/tb/modules/open.py +10 -5
- tinybird/tb/modules/project.py +14 -5
- tinybird/tb/modules/shell.py +15 -7
- tinybird/tb/modules/sink.py +3 -1
- tinybird/tb/modules/telemetry.py +11 -3
- tinybird/tb/modules/test.py +13 -9
- tinybird/tb/modules/test_common.py +13 -87
- tinybird/tb/modules/tinyunit/tinyunit.py +0 -14
- tinybird/tb/modules/tinyunit/tinyunit_lib.py +0 -6
- tinybird/tb/modules/watch.py +5 -3
- tinybird/tb_cli_modules/common.py +2 -2
- tinybird/tb_cli_modules/telemetry.py +1 -1
- tinybird/tornado_template.py +6 -7
- {tinybird-0.0.1.dev291.dist-info → tinybird-1.0.5.dist-info}/METADATA +32 -6
- tinybird-1.0.5.dist-info/RECORD +132 -0
- {tinybird-0.0.1.dev291.dist-info → tinybird-1.0.5.dist-info}/WHEEL +1 -1
- tinybird-0.0.1.dev291.dist-info/RECORD +0 -128
- {tinybird-0.0.1.dev291.dist-info → tinybird-1.0.5.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev291.dist-info → tinybird-1.0.5.dist-info}/top_level.txt +0 -0
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
|
+
import re
|
|
3
4
|
import time
|
|
5
|
+
from copy import deepcopy
|
|
4
6
|
from pathlib import Path
|
|
5
|
-
from typing import Optional
|
|
7
|
+
from typing import Any, Optional
|
|
6
8
|
from urllib.parse import urlencode, urljoin
|
|
7
9
|
|
|
8
10
|
import click
|
|
9
11
|
import requests
|
|
10
12
|
|
|
13
|
+
from tinybird.datafile.parse_datasource import parse_datasource
|
|
11
14
|
from tinybird.tb.client import TinyB
|
|
12
15
|
from tinybird.tb.modules.common import push_data, sys_exit
|
|
13
16
|
from tinybird.tb.modules.datafile.fixture import FixtureExtension, get_fixture_dir, persist_fixture
|
|
14
17
|
from tinybird.tb.modules.dev_server import BuildStatus
|
|
15
18
|
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
19
|
+
from tinybird.tb.modules.local_common import get_local_tokens
|
|
16
20
|
from tinybird.tb.modules.project import Project
|
|
17
21
|
from tinybird.tb.modules.shell import print_table_formatted
|
|
18
22
|
|
|
@@ -21,14 +25,26 @@ def process(
|
|
|
21
25
|
project: Project,
|
|
22
26
|
tb_client: TinyB,
|
|
23
27
|
watch: bool,
|
|
28
|
+
config: dict[str, Any],
|
|
24
29
|
file_changed: Optional[str] = None,
|
|
25
30
|
diff: Optional[str] = None,
|
|
26
31
|
silent: bool = False,
|
|
27
32
|
build_status: Optional[BuildStatus] = None,
|
|
28
33
|
exit_on_error: bool = True,
|
|
29
34
|
load_fixtures: bool = True,
|
|
35
|
+
project_with_vendors: Optional[Project] = None,
|
|
36
|
+
is_branch: bool = False,
|
|
30
37
|
) -> Optional[str]:
|
|
31
38
|
time_start = time.time()
|
|
39
|
+
|
|
40
|
+
# Build vendored workspaces before build
|
|
41
|
+
if not project_with_vendors and not is_branch:
|
|
42
|
+
build_vendored_workspaces(project=project, tb_client=tb_client, config=config)
|
|
43
|
+
|
|
44
|
+
# Ensure SHARED_WITH workspaces exist before build
|
|
45
|
+
if not is_branch:
|
|
46
|
+
build_shared_with_workspaces(project=project, tb_client=tb_client, config=config)
|
|
47
|
+
|
|
32
48
|
build_failed = False
|
|
33
49
|
build_error: Optional[str] = None
|
|
34
50
|
build_result: Optional[bool] = None
|
|
@@ -37,7 +53,7 @@ def process(
|
|
|
37
53
|
return build_status.error
|
|
38
54
|
else:
|
|
39
55
|
build_status.building = True
|
|
40
|
-
if file_changed and
|
|
56
|
+
if file_changed and file_changed.endswith((FixtureExtension.NDJSON, FixtureExtension.CSV)):
|
|
41
57
|
rebuild_fixture(project, tb_client, file_changed)
|
|
42
58
|
if build_status:
|
|
43
59
|
build_status.building = False
|
|
@@ -47,13 +63,19 @@ def process(
|
|
|
47
63
|
if build_status:
|
|
48
64
|
build_status.building = False
|
|
49
65
|
build_status.error = None
|
|
50
|
-
elif file_changed and
|
|
66
|
+
elif file_changed and file_changed.endswith((".env.local", ".env")):
|
|
51
67
|
if build_status:
|
|
52
68
|
build_status.building = False
|
|
53
69
|
build_status.error = None
|
|
54
70
|
else:
|
|
55
71
|
try:
|
|
56
|
-
build_result = build_project(
|
|
72
|
+
build_result = build_project(
|
|
73
|
+
project,
|
|
74
|
+
tb_client,
|
|
75
|
+
silent,
|
|
76
|
+
load_fixtures,
|
|
77
|
+
project_with_vendors=project_with_vendors,
|
|
78
|
+
)
|
|
57
79
|
if build_status:
|
|
58
80
|
build_status.building = False
|
|
59
81
|
build_status.error = None
|
|
@@ -82,12 +104,12 @@ def process(
|
|
|
82
104
|
build_status.error = build_error
|
|
83
105
|
build_status.building = False
|
|
84
106
|
return build_error
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
107
|
+
|
|
108
|
+
if not silent:
|
|
109
|
+
if build_result == False: # noqa: E712
|
|
110
|
+
click.echo(FeedbackManager.info(message="No changes. Build skipped."))
|
|
111
|
+
else:
|
|
112
|
+
click.echo(FeedbackManager.success(message=f"\n✓ {rebuild_str} completed in {elapsed_time:.1f}s"))
|
|
91
113
|
|
|
92
114
|
return None
|
|
93
115
|
|
|
@@ -182,7 +204,11 @@ def show_data(tb_client: TinyB, filename: str, diff: Optional[str] = None):
|
|
|
182
204
|
|
|
183
205
|
|
|
184
206
|
def build_project(
|
|
185
|
-
project: Project,
|
|
207
|
+
project: Project,
|
|
208
|
+
tb_client: TinyB,
|
|
209
|
+
silent: bool = False,
|
|
210
|
+
load_fixtures: bool = True,
|
|
211
|
+
project_with_vendors: Optional[Project] = None,
|
|
186
212
|
) -> Optional[bool]:
|
|
187
213
|
MULTIPART_BOUNDARY_DATA_PROJECT = "data_project://"
|
|
188
214
|
DATAFILE_TYPE_TO_CONTENT_TYPE = {
|
|
@@ -194,6 +220,7 @@ def build_project(
|
|
|
194
220
|
logging.debug(TINYBIRD_API_URL)
|
|
195
221
|
TINYBIRD_API_KEY = tb_client.token
|
|
196
222
|
error: Optional[str] = None
|
|
223
|
+
|
|
197
224
|
try:
|
|
198
225
|
files = [
|
|
199
226
|
("context://", ("cli-version", "1.0.0", "text/plain")),
|
|
@@ -208,9 +235,15 @@ def build_project(
|
|
|
208
235
|
relative_path = Path(file_path).relative_to(project_path).as_posix()
|
|
209
236
|
with open(file_path, "rb") as fd:
|
|
210
237
|
content_type = DATAFILE_TYPE_TO_CONTENT_TYPE.get(Path(file_path).suffix, "application/unknown")
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
238
|
+
content = fd.read().decode("utf-8")
|
|
239
|
+
if project_with_vendors:
|
|
240
|
+
# Replace 'SHARED_WITH' and everything that comes after, including new lines, with 'SHARED_WITH Tinybird_Local_Test_'
|
|
241
|
+
content = replace_shared_with(
|
|
242
|
+
content,
|
|
243
|
+
[project_with_vendors.workspace_name],
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
files.append((MULTIPART_BOUNDARY_DATA_PROJECT, (relative_path, content, content_type)))
|
|
214
247
|
HEADERS = {"Authorization": f"Bearer {TINYBIRD_API_KEY}"}
|
|
215
248
|
|
|
216
249
|
r = requests.post(TINYBIRD_API_URL, files=files, headers=HEADERS)
|
|
@@ -249,17 +282,16 @@ def build_project(
|
|
|
249
282
|
)
|
|
250
283
|
if no_changes:
|
|
251
284
|
return False
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
echo_changes(project, deleted_connections, ".connection", "deleted")
|
|
285
|
+
if not silent:
|
|
286
|
+
echo_changes(project, new_datasources, ".datasource", "created")
|
|
287
|
+
echo_changes(project, changed_datasources, ".datasource", "changed")
|
|
288
|
+
echo_changes(project, deleted_datasources, ".datasource", "deleted")
|
|
289
|
+
echo_changes(project, new_pipes, ".pipe", "created")
|
|
290
|
+
echo_changes(project, changed_pipes, ".pipe", "changed")
|
|
291
|
+
echo_changes(project, deleted_pipes, ".pipe", "deleted")
|
|
292
|
+
echo_changes(project, new_connections, ".connection", "created")
|
|
293
|
+
echo_changes(project, changed_connections, ".connection", "changed")
|
|
294
|
+
echo_changes(project, deleted_connections, ".connection", "deleted")
|
|
263
295
|
if load_fixtures:
|
|
264
296
|
try:
|
|
265
297
|
for filename in project_files:
|
|
@@ -320,3 +352,155 @@ def echo_changes(project: Project, changes: list[str], extension: str, status: s
|
|
|
320
352
|
if path_str:
|
|
321
353
|
path_str = path_str.replace(f"{project.folder}/", "")
|
|
322
354
|
click.echo(FeedbackManager.info(message=f"✓ {path_str} {status}"))
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def find_workspace_or_create(user_client: TinyB, workspace_name: str) -> Optional[str]:
|
|
358
|
+
# Get a client scoped to the vendored workspace using the user token
|
|
359
|
+
ws_token = None
|
|
360
|
+
org_id = None
|
|
361
|
+
try:
|
|
362
|
+
# Fetch org id and workspaces with tokens
|
|
363
|
+
info = user_client.user_workspaces_with_organization(version="v1")
|
|
364
|
+
org_id = info.get("organization_id")
|
|
365
|
+
workspaces = info.get("workspaces", [])
|
|
366
|
+
found = next((w for w in workspaces if w.get("name") == workspace_name), None)
|
|
367
|
+
if found:
|
|
368
|
+
ws_token = found.get("token")
|
|
369
|
+
# If still not found, try the generic listing
|
|
370
|
+
if not ws_token:
|
|
371
|
+
workspaces_full = user_client.user_workspaces_and_branches(version="v1")
|
|
372
|
+
created_ws = next(
|
|
373
|
+
(w for w in workspaces_full.get("workspaces", []) if w.get("name") == workspace_name), None
|
|
374
|
+
)
|
|
375
|
+
if created_ws:
|
|
376
|
+
ws_token = created_ws.get("token")
|
|
377
|
+
except Exception:
|
|
378
|
+
ws_token = None
|
|
379
|
+
|
|
380
|
+
# If workspace doesn't exist, try to create it and fetch its token
|
|
381
|
+
if not ws_token:
|
|
382
|
+
try:
|
|
383
|
+
user_client.create_workspace(workspace_name, assign_to_organization_id=org_id, version="v1")
|
|
384
|
+
# Fetch token for newly created workspace
|
|
385
|
+
info_after = user_client.user_workspaces_and_branches(version="v1")
|
|
386
|
+
created = next((w for w in info_after.get("workspaces", []) if w.get("name") == workspace_name), None)
|
|
387
|
+
ws_token = created.get("token") if created else None
|
|
388
|
+
except Exception as e:
|
|
389
|
+
click.echo(
|
|
390
|
+
FeedbackManager.warning(
|
|
391
|
+
message=(f"Skipping vendored workspace '{workspace_name}': unable to create or resolve token ({e})")
|
|
392
|
+
)
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
return ws_token
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def build_vendored_workspaces(project: Project, tb_client: TinyB, config: dict[str, Any]) -> None:
|
|
399
|
+
"""Build each vendored workspace under project.vendor_path if present.
|
|
400
|
+
|
|
401
|
+
Directory structure expected: vendor/<workspace_name>/<data_project_inside>
|
|
402
|
+
Each top-level directory under vendor is treated as a separate workspace
|
|
403
|
+
whose project files will be built using that workspace's token.
|
|
404
|
+
"""
|
|
405
|
+
try:
|
|
406
|
+
vendor_root = Path(project.vendor_path)
|
|
407
|
+
|
|
408
|
+
if not vendor_root.exists() or not vendor_root.is_dir():
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
tokens = get_local_tokens()
|
|
412
|
+
user_token = tokens["user_token"]
|
|
413
|
+
user_client = deepcopy(tb_client)
|
|
414
|
+
user_client.token = user_token
|
|
415
|
+
|
|
416
|
+
# Iterate over vendored workspace folders
|
|
417
|
+
for ws_dir in sorted([p for p in vendor_root.iterdir() if p.is_dir()]):
|
|
418
|
+
workspace_name = ws_dir.name
|
|
419
|
+
ws_token = find_workspace_or_create(user_client, workspace_name)
|
|
420
|
+
|
|
421
|
+
if not ws_token:
|
|
422
|
+
click.echo(
|
|
423
|
+
FeedbackManager.warning(
|
|
424
|
+
message=f"Skipping vendored workspace '{workspace_name}': could not resolve token after creation"
|
|
425
|
+
)
|
|
426
|
+
)
|
|
427
|
+
continue
|
|
428
|
+
|
|
429
|
+
# Build using a client scoped to the vendor workspace token
|
|
430
|
+
vendor_client = deepcopy(tb_client)
|
|
431
|
+
vendor_client.token = ws_token
|
|
432
|
+
vendor_project = Project(folder=str(ws_dir), workspace_name=workspace_name, max_depth=project.max_depth)
|
|
433
|
+
workspace_info = tb_client.workspace_info(version="v1")
|
|
434
|
+
project.workspace_name = workspace_info.get("name", "")
|
|
435
|
+
# Do not exit on error to allow main project to continue
|
|
436
|
+
process(
|
|
437
|
+
project=vendor_project,
|
|
438
|
+
tb_client=vendor_client,
|
|
439
|
+
watch=False,
|
|
440
|
+
silent=True,
|
|
441
|
+
exit_on_error=True,
|
|
442
|
+
load_fixtures=True,
|
|
443
|
+
config=config,
|
|
444
|
+
project_with_vendors=project,
|
|
445
|
+
)
|
|
446
|
+
except Exception as e:
|
|
447
|
+
# Never break the main build due to vendored build errors
|
|
448
|
+
click.echo(FeedbackManager.error_exception(error=e))
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def build_shared_with_workspaces(project: Project, tb_client: TinyB, config: dict[str, Any]) -> None:
|
|
452
|
+
"""Scan project for .datasource files and ensure SHARED_WITH workspaces exist."""
|
|
453
|
+
|
|
454
|
+
try:
|
|
455
|
+
# Gather SHARED_WITH workspace names from all .datasource files
|
|
456
|
+
datasource_files = project.get_datasource_files()
|
|
457
|
+
shared_ws_names = set()
|
|
458
|
+
|
|
459
|
+
for filename in datasource_files:
|
|
460
|
+
try:
|
|
461
|
+
doc = parse_datasource(filename).datafile
|
|
462
|
+
for ws_name in doc.shared_with or []:
|
|
463
|
+
shared_ws_names.add(ws_name)
|
|
464
|
+
except Exception:
|
|
465
|
+
# Ignore parse errors here; they'll be handled during the main process()
|
|
466
|
+
continue
|
|
467
|
+
|
|
468
|
+
if not shared_ws_names:
|
|
469
|
+
return
|
|
470
|
+
|
|
471
|
+
# Need a user token to list/create workspaces
|
|
472
|
+
tokens = get_local_tokens()
|
|
473
|
+
user_token = tokens.get("user_token")
|
|
474
|
+
if not user_token:
|
|
475
|
+
click.echo(FeedbackManager.info_skipping_shared_with_entry())
|
|
476
|
+
return
|
|
477
|
+
|
|
478
|
+
user_client = deepcopy(tb_client)
|
|
479
|
+
user_client.token = user_token
|
|
480
|
+
|
|
481
|
+
# Ensure each SHARED_WITH workspace exists
|
|
482
|
+
for ws_name in sorted(shared_ws_names):
|
|
483
|
+
find_workspace_or_create(user_client, ws_name)
|
|
484
|
+
except Exception as e:
|
|
485
|
+
click.echo(FeedbackManager.error_exception(error=e))
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def replace_shared_with(text: str, new_workspaces: list[str]) -> str:
|
|
489
|
+
replacement = ", ".join(new_workspaces)
|
|
490
|
+
|
|
491
|
+
# 1) Formato multilinea:
|
|
492
|
+
# SHARED_WITH >
|
|
493
|
+
# workspace1, workspace2
|
|
494
|
+
#
|
|
495
|
+
# Solo sustituimos la LÍNEA de workspaces (grupo 3), no usamos DOTALL.
|
|
496
|
+
pat_multiline = re.compile(r"(?m)^(SHARED_WITH\s*>\s*)\n([ \t]*)([^\n]*)$")
|
|
497
|
+
if pat_multiline.search(text):
|
|
498
|
+
return pat_multiline.sub(lambda m: f"{m.group(1)}\n{m.group(2)}{replacement}", text)
|
|
499
|
+
|
|
500
|
+
# 2) Formato inline:
|
|
501
|
+
# SHARED_WITH workspace1, workspace2
|
|
502
|
+
pat_inline = re.compile(r"(?m)^(SHARED_WITH\s+)([^\n]*)$")
|
|
503
|
+
if pat_inline.search(text):
|
|
504
|
+
return pat_inline.sub(lambda m: f"{m.group(1)}{replacement}", text)
|
|
505
|
+
|
|
506
|
+
return text
|
tinybird/tb/modules/cli.py
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
#
|
|
3
3
|
# - If it makes sense and only when strictly necessary, you can create utility functions in this file.
|
|
4
4
|
# - But please, **do not** interleave utility functions and command definitions.
|
|
5
|
-
|
|
6
5
|
import json
|
|
7
6
|
import logging
|
|
8
7
|
import os
|
|
@@ -11,9 +10,11 @@ import sys
|
|
|
11
10
|
from os import environ, getcwd
|
|
12
11
|
from pathlib import Path
|
|
13
12
|
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
13
|
+
from urllib.parse import urlencode
|
|
14
14
|
|
|
15
15
|
import click
|
|
16
16
|
import humanfriendly
|
|
17
|
+
import requests
|
|
17
18
|
from click import Context
|
|
18
19
|
|
|
19
20
|
from tinybird.tb import __cli__
|
|
@@ -21,7 +22,9 @@ from tinybird.tb.check_pypi import CheckPypi
|
|
|
21
22
|
from tinybird.tb.client import (
|
|
22
23
|
AuthException,
|
|
23
24
|
AuthNoTokenException,
|
|
25
|
+
TinyB,
|
|
24
26
|
)
|
|
27
|
+
from tinybird.tb.config import get_clickhouse_host
|
|
25
28
|
from tinybird.tb.modules.agent import run_agent
|
|
26
29
|
from tinybird.tb.modules.common import (
|
|
27
30
|
CatchAuthExceptions,
|
|
@@ -36,8 +39,10 @@ from tinybird.tb.modules.common import (
|
|
|
36
39
|
from tinybird.tb.modules.config import CURRENT_VERSION, CLIConfig
|
|
37
40
|
from tinybird.tb.modules.datafile.build import build_graph
|
|
38
41
|
from tinybird.tb.modules.datafile.pull import folder_pull
|
|
42
|
+
from tinybird.tb.modules.exceptions import CLIChException
|
|
39
43
|
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
40
44
|
from tinybird.tb.modules.local_common import get_tinybird_local_client
|
|
45
|
+
from tinybird.tb.modules.login_common import check_current_folder_in_sessions
|
|
41
46
|
from tinybird.tb.modules.project import Project
|
|
42
47
|
|
|
43
48
|
__old_click_echo = click.echo
|
|
@@ -72,6 +77,7 @@ VERSION = f"{__cli__.__version__} (rev {__cli__.__revision__})"
|
|
|
72
77
|
)
|
|
73
78
|
@click.option("--show-tokens", is_flag=True, default=False, help="Enable the output of tokens.")
|
|
74
79
|
@click.option("--cloud/--local", is_flag=True, default=False, help="Run against cloud or local.")
|
|
80
|
+
@click.option("--branch", help="Run against a branch.")
|
|
75
81
|
@click.option("--staging", is_flag=True, default=False, help="Run against a staging deployment.")
|
|
76
82
|
@click.option(
|
|
77
83
|
"--output", type=click.Choice(["human", "json", "csv"], case_sensitive=False), default="human", help="Output format"
|
|
@@ -99,6 +105,7 @@ def cli(
|
|
|
99
105
|
version_warning: bool,
|
|
100
106
|
show_tokens: bool,
|
|
101
107
|
cloud: bool,
|
|
108
|
+
branch: Optional[str],
|
|
102
109
|
staging: bool,
|
|
103
110
|
output: str,
|
|
104
111
|
max_depth: int,
|
|
@@ -106,17 +113,16 @@ def cli(
|
|
|
106
113
|
prompt: Optional[str] = None,
|
|
107
114
|
) -> None:
|
|
108
115
|
"""
|
|
109
|
-
|
|
116
|
+
Run just `tb` to use Tinybird Code to interact with your project.
|
|
110
117
|
"""
|
|
111
118
|
|
|
112
119
|
# We need to unpatch for our tests not to break
|
|
113
120
|
if output != "human":
|
|
114
121
|
__hide_click_output()
|
|
122
|
+
elif show_tokens or not cloud or ctx.invoked_subcommand == "build":
|
|
123
|
+
__unpatch_click_output()
|
|
115
124
|
else:
|
|
116
|
-
|
|
117
|
-
__unpatch_click_output()
|
|
118
|
-
else:
|
|
119
|
-
__patch_click_output()
|
|
125
|
+
__patch_click_output()
|
|
120
126
|
|
|
121
127
|
if getenv_bool("TB_DISABLE_SSL_CHECKS", False):
|
|
122
128
|
click.echo(FeedbackManager.warning_disabled_ssl_checks())
|
|
@@ -197,17 +203,36 @@ def cli(
|
|
|
197
203
|
return
|
|
198
204
|
|
|
199
205
|
ctx.ensure_object(dict)["project"] = project
|
|
200
|
-
client = create_ctx_client(
|
|
206
|
+
client = create_ctx_client(
|
|
207
|
+
ctx,
|
|
208
|
+
config,
|
|
209
|
+
cloud or bool(branch),
|
|
210
|
+
staging,
|
|
211
|
+
project=project,
|
|
212
|
+
show_warnings=version_warning,
|
|
213
|
+
branch=branch,
|
|
214
|
+
)
|
|
201
215
|
|
|
202
216
|
if client:
|
|
203
217
|
ctx.ensure_object(dict)["client"] = client
|
|
204
218
|
|
|
205
|
-
ctx.ensure_object(dict)["env"] = get_target_env(cloud)
|
|
219
|
+
ctx.ensure_object(dict)["env"] = get_target_env(cloud, branch)
|
|
220
|
+
ctx.ensure_object(dict)["branch"] = branch
|
|
206
221
|
ctx.ensure_object(dict)["output"] = output
|
|
207
222
|
|
|
223
|
+
# Check if current folder is tracked from previous sessions
|
|
224
|
+
check_current_folder_in_sessions(ctx)
|
|
225
|
+
|
|
208
226
|
is_prompt_mode = prompt is not None
|
|
209
227
|
|
|
210
228
|
if is_agent_mode or is_prompt_mode:
|
|
229
|
+
if any(arg in sys.argv for arg in ["--cloud", "--local", "--branch"]):
|
|
230
|
+
raise CLIException(
|
|
231
|
+
FeedbackManager.error(
|
|
232
|
+
message="Tinybird Code does not support --cloud, --local or --branch flags. It will choose the correct environment based on your prompts."
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
|
|
211
236
|
run_agent(config, project, dangerously_skip_permissions, prompt=prompt)
|
|
212
237
|
|
|
213
238
|
|
|
@@ -298,7 +323,7 @@ def sql(
|
|
|
298
323
|
)
|
|
299
324
|
|
|
300
325
|
query = ""
|
|
301
|
-
for
|
|
326
|
+
for elem in dependencies_graph.to_run.values():
|
|
302
327
|
for _node in elem["nodes"]:
|
|
303
328
|
if _node["params"]["name"].lower() == node.lower():
|
|
304
329
|
query = "".join(_node["sql"])
|
|
@@ -336,6 +361,88 @@ def sql(
|
|
|
336
361
|
click.echo(FeedbackManager.info_no_rows())
|
|
337
362
|
|
|
338
363
|
|
|
364
|
+
@cli.command(
|
|
365
|
+
name="ch",
|
|
366
|
+
context_settings=dict(
|
|
367
|
+
ignore_unknown_options=True,
|
|
368
|
+
allow_extra_args=True,
|
|
369
|
+
),
|
|
370
|
+
)
|
|
371
|
+
@click.option(
|
|
372
|
+
"--query",
|
|
373
|
+
type=str,
|
|
374
|
+
default=None,
|
|
375
|
+
required=False,
|
|
376
|
+
help="The query to run against ClickHouse.",
|
|
377
|
+
)
|
|
378
|
+
@click.option(
|
|
379
|
+
"--user",
|
|
380
|
+
required=False,
|
|
381
|
+
help="User field is not used for authentication but helps identify the connection.",
|
|
382
|
+
)
|
|
383
|
+
@click.option(
|
|
384
|
+
"--password",
|
|
385
|
+
required=False,
|
|
386
|
+
help="Your Tinybird Auth Token. If not provided, the token will be your current workspace token.",
|
|
387
|
+
)
|
|
388
|
+
@click.option(
|
|
389
|
+
"-m",
|
|
390
|
+
"--multiline",
|
|
391
|
+
is_flag=True,
|
|
392
|
+
default=False,
|
|
393
|
+
help="Enable multiline mode - read the query from multiple lines until a semicolon.",
|
|
394
|
+
)
|
|
395
|
+
@click.pass_context
|
|
396
|
+
def ch(ctx: Context, query: str, user: Optional[str], password: Optional[str], multiline: bool) -> None:
|
|
397
|
+
"""Run a query against ClickHouse native HTTP interface."""
|
|
398
|
+
try:
|
|
399
|
+
query_arg = next((arg for arg in ctx.args if not arg.startswith("--param_")), None)
|
|
400
|
+
if query_arg and not query:
|
|
401
|
+
query = query_arg
|
|
402
|
+
|
|
403
|
+
if not query and not sys.stdin.isatty(): # Check if there's piped input
|
|
404
|
+
query = sys.stdin.read().strip()
|
|
405
|
+
|
|
406
|
+
if not query:
|
|
407
|
+
click.echo(FeedbackManager.warning(message="Nothing to do. No query provided"))
|
|
408
|
+
return
|
|
409
|
+
|
|
410
|
+
if multiline:
|
|
411
|
+
queries = [query.strip() for query in query.split(";") if query.strip()]
|
|
412
|
+
else:
|
|
413
|
+
queries = [query]
|
|
414
|
+
|
|
415
|
+
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
416
|
+
config = ctx.ensure_object(dict)["config"]
|
|
417
|
+
password = password or client.token
|
|
418
|
+
user = user or config.get("name", None)
|
|
419
|
+
ch_host = get_clickhouse_host(client.host)
|
|
420
|
+
headers = {"X-ClickHouse-Key": password}
|
|
421
|
+
if user:
|
|
422
|
+
headers["X-ClickHouse-User"] = user
|
|
423
|
+
|
|
424
|
+
params = {}
|
|
425
|
+
|
|
426
|
+
for param in ctx.args:
|
|
427
|
+
if param.startswith("--param_"):
|
|
428
|
+
param_name = param.split("=")[0].replace("--", "")
|
|
429
|
+
param_value = param.split("=")[1]
|
|
430
|
+
params[param_name] = param_value
|
|
431
|
+
|
|
432
|
+
for query in queries:
|
|
433
|
+
query_params = {**params, "query": query}
|
|
434
|
+
url = f"{ch_host}?{urlencode(query_params)}"
|
|
435
|
+
res = requests.get(url=url, headers=headers)
|
|
436
|
+
|
|
437
|
+
if res.status_code != 200:
|
|
438
|
+
raise Exception(res.text)
|
|
439
|
+
|
|
440
|
+
click.echo(res.text)
|
|
441
|
+
|
|
442
|
+
except Exception as e:
|
|
443
|
+
raise CLIChException(FeedbackManager.error(message=str(e)))
|
|
444
|
+
|
|
445
|
+
|
|
339
446
|
def __patch_click_output():
|
|
340
447
|
import re
|
|
341
448
|
|
|
@@ -392,7 +499,13 @@ def __hide_click_output() -> None:
|
|
|
392
499
|
|
|
393
500
|
|
|
394
501
|
def create_ctx_client(
|
|
395
|
-
ctx: Context,
|
|
502
|
+
ctx: Context,
|
|
503
|
+
config: Dict[str, Any],
|
|
504
|
+
cloud: bool,
|
|
505
|
+
staging: bool,
|
|
506
|
+
project: Project,
|
|
507
|
+
show_warnings: bool = True,
|
|
508
|
+
branch: Optional[str] = None,
|
|
396
509
|
):
|
|
397
510
|
commands_without_ctx_client = [
|
|
398
511
|
"auth",
|
|
@@ -402,11 +515,11 @@ def create_ctx_client(
|
|
|
402
515
|
"logout",
|
|
403
516
|
"update",
|
|
404
517
|
"upgrade",
|
|
405
|
-
"create",
|
|
406
518
|
"info",
|
|
407
519
|
"tag",
|
|
408
520
|
"push",
|
|
409
521
|
"branch",
|
|
522
|
+
"environment",
|
|
410
523
|
"diff",
|
|
411
524
|
"fmt",
|
|
412
525
|
"init",
|
|
@@ -415,8 +528,8 @@ def create_ctx_client(
|
|
|
415
528
|
if not command or command in commands_without_ctx_client:
|
|
416
529
|
return None
|
|
417
530
|
|
|
418
|
-
commands_always_cloud = ["infra"]
|
|
419
|
-
commands_always_local = ["
|
|
531
|
+
commands_always_cloud = ["infra", "branch", "environment"]
|
|
532
|
+
commands_always_local = ["create"]
|
|
420
533
|
command_always_test = ["test"]
|
|
421
534
|
|
|
422
535
|
if (
|
|
@@ -439,7 +552,7 @@ def create_ctx_client(
|
|
|
439
552
|
if method and show_warnings:
|
|
440
553
|
click.echo(FeedbackManager.gray(message=f"Authentication method: {method}"))
|
|
441
554
|
|
|
442
|
-
return _get_tb_client(config.get("token", ""), config["host"], staging=staging)
|
|
555
|
+
return _get_tb_client(config.get("token", ""), config["host"], staging=staging, branch=branch)
|
|
443
556
|
local = command in commands_always_local
|
|
444
557
|
test = command in command_always_test
|
|
445
558
|
if show_warnings and not local and command not in commands_always_local and command:
|
|
@@ -447,8 +560,8 @@ def create_ctx_client(
|
|
|
447
560
|
return get_tinybird_local_client(config, test=test, staging=staging)
|
|
448
561
|
|
|
449
562
|
|
|
450
|
-
def get_target_env(cloud: bool) -> str:
|
|
451
|
-
if cloud:
|
|
563
|
+
def get_target_env(cloud: bool, branch: Optional[str]) -> str:
|
|
564
|
+
if cloud or bool(branch):
|
|
452
565
|
return "cloud"
|
|
453
566
|
return "local"
|
|
454
567
|
|