tinybird 4.6.12.dev0__tar.gz → 4.6.15.dev0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/PKG-INFO +13 -1
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/client.py +34 -10
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/datafile/common.py +20 -10
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/datafile/parse_pipe.py +22 -5
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/feedback_manager.py +3 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/sql_template.py +40 -3
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/sql_toolset.py +26 -7
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/__cli__.py +2 -2
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/client.py +25 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/common.py +15 -1
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/config.py +1 -1
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datafile/build.py +1 -1
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datafile/pipe_checker.py +2 -2
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datafile/playground.py +1 -1
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datasource.py +72 -4
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/common.py +15 -1
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/config.py +1 -1
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/datasource.py +68 -4
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tornado_template.py +5 -5
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird.egg-info/PKG-INFO +13 -1
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/setup.cfg +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/__cli__.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/ch_utils/constants.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/ch_utils/engine.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/check_pypi.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/config.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/context.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/datafile/exceptions.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/datafile/parse_connection.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/datafile/parse_datasource.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/datatypes.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/git_settings.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/iterating/__init__.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/iterating/data_branch_modes.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/prompts.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/service_datasources.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/sql.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/sql_template_fmt.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/syncasync.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/check_pypi.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/cli.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/config.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/branch.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/build.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/build_common.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/cicd.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/cli.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/connection.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/connection_dynamodb.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/connection_kafka.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/connection_s3.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/copy.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/create.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datafile/build_common.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datafile/build_datasource.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datafile/build_pipe.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datafile/diff.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datafile/fixture.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datafile/format_common.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datafile/format_connection.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datafile/format_datasource.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datafile/format_pipe.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datafile/pull.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/deployment.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/deployment_common.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/deprecations.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/endpoint.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/exceptions.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/feedback_manager.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/fmt.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/info.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/infra.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/job.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/job_common.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/llm.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/llm_utils.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/local.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/local_common.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/local_logs.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/login.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/login_common.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/logout.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/logs.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/materialization.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/open.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/pipe.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/preview.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/project.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/project_commands.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/py_project.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/query_output.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/regions.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/secret.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/secret_common.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/sink.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/table.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/telemetry.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/test.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/test_common.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/tinyunit/tinyunit.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/token.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/ts_project.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/watch.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/workspace.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/workspace_members.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/auth.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/branch.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/cicd.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/cli.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/connection.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/exceptions.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/fmt.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/job.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/pipe.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/regions.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/tag.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/telemetry.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/test.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/workspace.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/workspace_members.py +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird.egg-info/SOURCES.txt +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird.egg-info/dependency_links.txt +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird.egg-info/entry_points.txt +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird.egg-info/requires.txt +0 -0
- {tinybird-4.6.12.dev0 → tinybird-4.6.15.dev0}/tinybird.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: tinybird
|
|
3
|
-
Version: 4.6.
|
|
3
|
+
Version: 4.6.15.dev0
|
|
4
4
|
Summary: Tinybird Command Line Tool
|
|
5
5
|
Home-page: https://www.tinybird.co/docs/forward/commands
|
|
6
6
|
Author: Tinybird
|
|
@@ -52,6 +52,18 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
|
|
|
52
52
|
Changelog
|
|
53
53
|
----------
|
|
54
54
|
|
|
55
|
+
4.6.13
|
|
56
|
+
********
|
|
57
|
+
|
|
58
|
+
- `Added` ``tb datasource append`` and ``tb datasource replace`` support the experimental ``--experimental=use_v1`` option for local CSV, NDJSON, and Parquet files. It submits the import through the v1 endpoint and prints the queued job ID; pass ``--wait`` to wait for completion.
|
|
59
|
+
|
|
60
|
+
4.6.12
|
|
61
|
+
********
|
|
62
|
+
|
|
63
|
+
- `Added` Forward sink pipes support ``{{ tb_secret(...) }}`` in ``EXPORT_*`` metadata (for example ``EXPORT_BUCKET_URI``). Secrets are resolved at deploy time and baked into the sink settings; changing a secret requires a redeploy.
|
|
64
|
+
- `Fixed` Forward deploy now raises an error when a secret used in sink metadata resolves to an empty value.
|
|
65
|
+
- `Fixed` Forward deploy now rejects ``{{ tb_secret(...) }}`` in ``EXPORT_SCHEDULE`` and ``COPY_SCHEDULE``. Use a literal cron expression (for example ``0 * * * *``) or ``@on-demand``.
|
|
66
|
+
|
|
55
67
|
4.6.11
|
|
56
68
|
********
|
|
57
69
|
|
|
@@ -441,6 +441,7 @@ class TinyB:
|
|
|
441
441
|
sql_condition: Optional[str] = None,
|
|
442
442
|
format: str = "csv",
|
|
443
443
|
replace_options: Optional[Set[str]] = None,
|
|
444
|
+
use_v1: bool = False,
|
|
444
445
|
):
|
|
445
446
|
params = {"name": datasource_name, "mode": mode, "format": format, "debug": "blocks_block_log"}
|
|
446
447
|
|
|
@@ -452,20 +453,43 @@ class TinyB:
|
|
|
452
453
|
|
|
453
454
|
async with aiofiles.open(file, "rb") as content:
|
|
454
455
|
file_content = await content.read()
|
|
455
|
-
if format == "csv":
|
|
456
|
-
files = {"csv": ("csv", file_content)}
|
|
457
|
-
else:
|
|
458
|
-
files = {"ndjson": ("ndjson", file_content)}
|
|
459
456
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
457
|
+
if use_v1:
|
|
458
|
+
v1_params = {"format": format}
|
|
459
|
+
if sql_condition:
|
|
460
|
+
v1_params["replace_condition"] = sql_condition
|
|
461
|
+
if replace_options:
|
|
462
|
+
for option in list(replace_options):
|
|
463
|
+
v1_params[option] = "true"
|
|
464
|
+
content_types = {
|
|
465
|
+
"csv": "text/csv",
|
|
466
|
+
"ndjson": "application/x-ndjson",
|
|
467
|
+
"parquet": "application/vnd.apache.parquet",
|
|
468
|
+
}
|
|
469
|
+
headers = {"Content-Type": content_types[format]}
|
|
470
|
+
if str(file).endswith(".gz"):
|
|
471
|
+
headers["Content-Encoding"] = "gzip"
|
|
472
|
+
return await self._req(
|
|
473
|
+
f"/v1/datasources/{quote(datasource_name, safe='')}/{mode}?{urlencode(v1_params, safe='')}",
|
|
474
|
+
data=file_content,
|
|
475
|
+
headers=headers,
|
|
463
476
|
method="POST",
|
|
464
477
|
)
|
|
465
|
-
if status_callback:
|
|
466
|
-
status_callback(res)
|
|
467
478
|
|
|
468
|
-
|
|
479
|
+
if format == "csv":
|
|
480
|
+
files = {"csv": ("csv", file_content)}
|
|
481
|
+
else:
|
|
482
|
+
files = {"ndjson": ("ndjson", file_content)}
|
|
483
|
+
|
|
484
|
+
res = await self._req(
|
|
485
|
+
f"v0/datasources?{urlencode(params, safe='')}",
|
|
486
|
+
files=files,
|
|
487
|
+
method="POST",
|
|
488
|
+
)
|
|
489
|
+
if status_callback:
|
|
490
|
+
status_callback(res)
|
|
491
|
+
|
|
492
|
+
return res
|
|
469
493
|
|
|
470
494
|
async def datasource_truncate(self, datasource_name: str):
|
|
471
495
|
return await self._req(f"/v0/datasources/{datasource_name}/truncate", method="POST", data="")
|
|
@@ -401,12 +401,15 @@ class Datafile:
|
|
|
401
401
|
if node.get("mode") and node["mode"] not in ["append", "replace"]:
|
|
402
402
|
raise DatafileValidationError("COPY node mode must be append or replace")
|
|
403
403
|
# copy schedule must be @on-demand or a cron-expression
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
and
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
404
|
+
copy_schedule = node.get("copy_schedule")
|
|
405
|
+
if copy_schedule and copy_schedule != ON_DEMAND:
|
|
406
|
+
if isinstance(copy_schedule, str) and "tb_secret" in copy_schedule:
|
|
407
|
+
raise DatafileValidationError(
|
|
408
|
+
"COPY_SCHEDULE does not support { tb_secret(...) }. "
|
|
409
|
+
'Use a literal cron expression (for example "0 * * * *") or @on-demand.'
|
|
410
|
+
)
|
|
411
|
+
if not croniter.is_valid(copy_schedule):
|
|
412
|
+
raise DatafileValidationError("COPY node schedule must be @on-demand or a valid cron expression.")
|
|
410
413
|
for key in node.keys():
|
|
411
414
|
if key not in CopyParameters.valid_params():
|
|
412
415
|
raise DatafileValidationError(
|
|
@@ -503,10 +506,16 @@ class Datafile:
|
|
|
503
506
|
|
|
504
507
|
# Validate schedule format (common for both Kafka and S3/GCS)
|
|
505
508
|
export_schedule = node.get("export_schedule")
|
|
506
|
-
if export_schedule and export_schedule != ON_DEMAND
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
509
|
+
if export_schedule and export_schedule != ON_DEMAND:
|
|
510
|
+
if isinstance(export_schedule, str) and "tb_secret" in export_schedule:
|
|
511
|
+
raise DatafileValidationError(
|
|
512
|
+
f"Sink node {repr(node['name'])}: EXPORT_SCHEDULE does not support {{ tb_secret(...) }}. "
|
|
513
|
+
'Use a literal cron expression (for example "0 * * * *") or @on-demand.'
|
|
514
|
+
)
|
|
515
|
+
if not croniter.is_valid(export_schedule):
|
|
516
|
+
raise DatafileValidationError(
|
|
517
|
+
f"Sink node {repr(node['name'])} has invalid export_schedule '{export_schedule}'. Must be @on-demand or a valid cron expression."
|
|
518
|
+
)
|
|
510
519
|
|
|
511
520
|
def validate(self):
|
|
512
521
|
if self.kind == DatafileKind.pipe:
|
|
@@ -2082,6 +2091,7 @@ def parse(
|
|
|
2082
2091
|
"sql": sql("sql"),
|
|
2083
2092
|
"version": version,
|
|
2084
2093
|
"deployment_method": assign_var("deployment_method", allowed_values={"alter"}),
|
|
2094
|
+
"export_service": export_service, # Deprecated
|
|
2085
2095
|
"export_connection_name": assign_var("export_connection_name"),
|
|
2086
2096
|
"export_schedule": assign_var("export_schedule"),
|
|
2087
2097
|
"export_bucket_uri": assign_var("export_bucket_uri"),
|
|
@@ -52,20 +52,35 @@ def parse_pipe(
|
|
|
52
52
|
if "type" in node:
|
|
53
53
|
node["type"] = node["type"].lower()
|
|
54
54
|
sql = node.get("sql", "")
|
|
55
|
-
|
|
55
|
+
source_sql = sql.strip()
|
|
56
|
+
if not source_sql:
|
|
57
|
+
raise click.ClickException(
|
|
58
|
+
FeedbackManager.error_parsing_node(node=node["name"], pipe=filename, error="Empty query")
|
|
59
|
+
)
|
|
60
|
+
if source_sql.startswith("%"):
|
|
56
61
|
secrets_list: Optional[List[str]] = None
|
|
57
62
|
if secrets:
|
|
58
63
|
secrets_list = list(secrets.keys())
|
|
59
64
|
# Setting test_mode=True to ignore errors on required parameters and
|
|
60
65
|
# secrets_in_test_mode=False to raise errors on missing secrets
|
|
61
66
|
sql, _, variable_warnings = render_sql_template(
|
|
62
|
-
|
|
67
|
+
source_sql[1:],
|
|
63
68
|
name=node["name"],
|
|
64
69
|
secrets=secrets_list,
|
|
65
70
|
test_mode=True,
|
|
66
71
|
secrets_in_test_mode=ignore_secrets,
|
|
67
72
|
)
|
|
68
73
|
doc.warnings = variable_warnings
|
|
74
|
+
if not sql.strip():
|
|
75
|
+
raise click.ClickException(
|
|
76
|
+
FeedbackManager.error_parsing_node(
|
|
77
|
+
node=node["name"],
|
|
78
|
+
pipe=filename,
|
|
79
|
+
error="Template renders to an empty query without parameters",
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
sql = source_sql
|
|
69
84
|
# it'll fail with a ModuleNotFoundError when the toolset is not available but it returns the parsed doc
|
|
70
85
|
from tinybird.sql_toolset import format_sql as toolset_format_sql
|
|
71
86
|
|
|
@@ -79,13 +94,15 @@ def parse_pipe(
|
|
|
79
94
|
)
|
|
80
95
|
)
|
|
81
96
|
except ValueError as e:
|
|
82
|
-
|
|
97
|
+
source_sql = node.get("sql", "")
|
|
98
|
+
source_sql_stripped = source_sql.strip()
|
|
99
|
+
t, template_variables, _ = get_template_and_variables(source_sql, name=node["name"])
|
|
83
100
|
|
|
84
|
-
if
|
|
101
|
+
if not source_sql_stripped.startswith("%") and len(template_variables) > 0:
|
|
85
102
|
raise click.ClickException(FeedbackManager.error_template_start(filename=filename))
|
|
86
103
|
raise click.ClickException(
|
|
87
104
|
FeedbackManager.error_parsing_file(
|
|
88
|
-
filename=filename, lineno="", error=f"{str(e)} + SQL(value error): {
|
|
105
|
+
filename=filename, lineno="", error=f"{str(e)} + SQL(value error): {source_sql}"
|
|
89
106
|
)
|
|
90
107
|
)
|
|
91
108
|
except UnClosedIfError as e:
|
|
@@ -864,6 +864,9 @@ class FeedbackManager:
|
|
|
864
864
|
success_total_rows = success_message("** Total rows in {datasource}: {total_rows}")
|
|
865
865
|
success_appended_datasource = success_message("** Data appended to Data Source '{datasource}' successfully!")
|
|
866
866
|
success_replaced_datasource = success_message("** Data replaced in Data Source '{datasource}' successfully!")
|
|
867
|
+
success_import_job_queued = success_message("** {operation} import job queued: {job_id}")
|
|
868
|
+
success_import_job_completed = success_message("** {operation} import completed: {job_id}")
|
|
869
|
+
info_import_job_status = info_message("Check status: tb job details {job_id}")
|
|
867
870
|
success_auth = success_message(
|
|
868
871
|
"** Auth successful! \n** Configuration written to .tinyb file, consider adding it to .gitignore"
|
|
869
872
|
)
|
|
@@ -1396,7 +1396,7 @@ _namespace = {
|
|
|
1396
1396
|
}
|
|
1397
1397
|
|
|
1398
1398
|
|
|
1399
|
-
reserved_vars =
|
|
1399
|
+
reserved_vars = {"_tt_tmp", "_tt_append", "isinstance", "str", "error", "custom_error", *list(vars(builtins))}
|
|
1400
1400
|
for p in DEFAULT_PARAM_NAMES: # we handle these in an specific manner
|
|
1401
1401
|
reserved_vars.discard(p) # `format` is part of builtins
|
|
1402
1402
|
# Allow 'id' to be used as a template parameter - https://gitlab.com/tinybird/analytics/-/issues/19119
|
|
@@ -2911,13 +2911,21 @@ def extract_variables_from_sql(sql: str, params: List[Dict[str, Any]]) -> Dict[s
|
|
|
2911
2911
|
return defaults
|
|
2912
2912
|
|
|
2913
2913
|
|
|
2914
|
-
def render_template_with_secrets(
|
|
2914
|
+
def render_template_with_secrets(
|
|
2915
|
+
name: str,
|
|
2916
|
+
content: str,
|
|
2917
|
+
secrets: Optional[Dict[str, str]] = None,
|
|
2918
|
+
empty_secret_raises: bool = False,
|
|
2919
|
+
) -> str:
|
|
2915
2920
|
"""Renders a template with secrets, allowing for default values.
|
|
2916
2921
|
|
|
2917
2922
|
Args:
|
|
2918
2923
|
name: The name of the template
|
|
2919
2924
|
content: The template content
|
|
2920
2925
|
secrets: A dictionary mapping secret names to their values
|
|
2926
|
+
empty_secret_raises: When True, empty secret values or empty defaults raise
|
|
2927
|
+
instead of returning '""'. Used for sink metadata (Forward deploy) where
|
|
2928
|
+
an empty bucket path must not be baked as s3://"".
|
|
2921
2929
|
|
|
2922
2930
|
Returns:
|
|
2923
2931
|
The rendered template
|
|
@@ -2959,6 +2967,26 @@ def render_template_with_secrets(name: str, content: str, secrets: Optional[Dict
|
|
|
2959
2967
|
Traceback (most recent call last):
|
|
2960
2968
|
...
|
|
2961
2969
|
tinybird.sql_template.SQLTemplateException: Template Syntax Error: Cannot access secret 'MISSING_SECRET'. Check the secret exists in the Workspace and the token has the required scope.
|
|
2970
|
+
|
|
2971
|
+
>>> render_template_with_secrets(
|
|
2972
|
+
... "sink_bucket",
|
|
2973
|
+
... "s3://{{ tb_secret('MISSING_SECRET', '') }}",
|
|
2974
|
+
... secrets = {},
|
|
2975
|
+
... empty_secret_raises=True,
|
|
2976
|
+
... )
|
|
2977
|
+
Traceback (most recent call last):
|
|
2978
|
+
...
|
|
2979
|
+
tinybird.sql_template.SQLTemplateException: Template Syntax Error: Secret 'MISSING_SECRET' resolves to an empty value and cannot be used in sink metadata.
|
|
2980
|
+
|
|
2981
|
+
>>> render_template_with_secrets(
|
|
2982
|
+
... "sink_bucket",
|
|
2983
|
+
... "s3://{{ tb_secret('EMPTY_SECRET', 'fallback') }}",
|
|
2984
|
+
... secrets = {'EMPTY_SECRET': ''},
|
|
2985
|
+
... empty_secret_raises=True,
|
|
2986
|
+
... )
|
|
2987
|
+
Traceback (most recent call last):
|
|
2988
|
+
...
|
|
2989
|
+
tinybird.sql_template.SQLTemplateException: Template Syntax Error: Secret 'EMPTY_SECRET' resolves to an empty value and cannot be used in sink metadata.
|
|
2962
2990
|
"""
|
|
2963
2991
|
if not secrets:
|
|
2964
2992
|
secrets = {}
|
|
@@ -2974,15 +3002,24 @@ def render_template_with_secrets(name: str, content: str, secrets: Optional[Dict
|
|
|
2974
3002
|
The secret value or default
|
|
2975
3003
|
|
|
2976
3004
|
Raises:
|
|
2977
|
-
SQLTemplateException: If the secret is not found and no default is provided
|
|
3005
|
+
SQLTemplateException: If the secret is not found and no default is provided,
|
|
3006
|
+
or if empty_secret_raises is True and the resolved value is empty
|
|
2978
3007
|
"""
|
|
2979
3008
|
if secret_name in secrets:
|
|
2980
3009
|
value = secrets[secret_name]
|
|
2981
3010
|
if isinstance(value, str) and len(value) == 0:
|
|
3011
|
+
if empty_secret_raises:
|
|
3012
|
+
raise SQLTemplateException(
|
|
3013
|
+
f"Secret '{secret_name}' resolves to an empty value and cannot be used in sink metadata."
|
|
3014
|
+
)
|
|
2982
3015
|
return '""'
|
|
2983
3016
|
return value
|
|
2984
3017
|
elif default is not None:
|
|
2985
3018
|
if isinstance(default, str) and len(default) == 0:
|
|
3019
|
+
if empty_secret_raises:
|
|
3020
|
+
raise SQLTemplateException(
|
|
3021
|
+
f"Secret '{secret_name}' resolves to an empty value and cannot be used in sink metadata."
|
|
3022
|
+
)
|
|
2986
3023
|
return '""'
|
|
2987
3024
|
return default
|
|
2988
3025
|
else:
|
|
@@ -158,7 +158,20 @@ def has_unoptimized_join(sql: str, left_table: Optional[Union[Tuple[str, str], T
|
|
|
158
158
|
raise UnoptimizedJoinException(sql)
|
|
159
159
|
|
|
160
160
|
|
|
161
|
-
def
|
|
161
|
+
def _format_fragment_for_mutation_command(fragment: str) -> str:
|
|
162
|
+
"""Normalize a SQL fragment the way CH stores it in the
|
|
163
|
+
`system.mutations.command` column: format the expression with CH's own
|
|
164
|
+
formatter, then escape it as it appears inside the serialized command
|
|
165
|
+
(string literals keep backslash-escaped quotes)."""
|
|
166
|
+
formatted = chquery.format(f"""SELECT {fragment}""").split("SELECT ")[1]
|
|
167
|
+
formatted = formatted.replace("\\", "\\\\").replace("'", "''")
|
|
168
|
+
quoted = chquery.format(f"SELECT '{formatted}'").split("SELECT ")[1]
|
|
169
|
+
return quoted[1:-1]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def format_where_for_mutation_command(
|
|
173
|
+
where_clause: str, lightweight: bool = False, partition: Optional[str] = None
|
|
174
|
+
) -> str:
|
|
162
175
|
"""
|
|
163
176
|
>>> format_where_for_mutation_command("numnights = 99")
|
|
164
177
|
'DELETE WHERE numnights = 99'
|
|
@@ -172,12 +185,18 @@ def format_where_for_mutation_command(where_clause: str, lightweight: bool = Fal
|
|
|
172
185
|
"DELETE WHERE reservationid = \\\\'\\\\\\\\\\\\'foo\\\\'"
|
|
173
186
|
>>> format_where_for_mutation_command("number < 3", lightweight=True)
|
|
174
187
|
'UPDATE _row_exists = 0 WHERE number < 3'
|
|
188
|
+
>>> format_where_for_mutation_command("number < 3", lightweight=True, partition="201901")
|
|
189
|
+
'UPDATE _row_exists = 0 IN PARTITION 201901 WHERE number < 3'
|
|
190
|
+
>>> format_where_for_mutation_command("number < 3", lightweight=True, partition="'2019-01-01'")
|
|
191
|
+
"UPDATE _row_exists = 0 IN PARTITION \\\\'2019-01-01\\\\' WHERE number < 3"
|
|
175
192
|
"""
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
193
|
+
quoted_condition = _format_fragment_for_mutation_command(where_clause)
|
|
194
|
+
prefix = "UPDATE _row_exists = 0" if lightweight else "DELETE"
|
|
195
|
+
# Partition-scoped mutations store the partition expression in the
|
|
196
|
+
# command (`… IN PARTITION <expr> WHERE <cond>`), normalized by the same
|
|
197
|
+
# CH formatter, so it must be part of the match.
|
|
198
|
+
partition_clause = f" IN PARTITION {_format_fragment_for_mutation_command(partition)}" if partition else ""
|
|
199
|
+
return f"{prefix}{partition_clause} WHERE {quoted_condition}"
|
|
181
200
|
|
|
182
201
|
|
|
183
202
|
# Functions that take table/dictionary names as string literal arguments.
|
|
@@ -624,7 +643,7 @@ def replace_tables(
|
|
|
624
643
|
|
|
625
644
|
if current_replacements:
|
|
626
645
|
# We need to transform the dictionary into something cacheable, so a sorted tuple of tuples it is
|
|
627
|
-
r = tuple(sorted(
|
|
646
|
+
r = tuple(sorted(current_replacements.items()))
|
|
628
647
|
sql = replace_tables_chquery_cached(
|
|
629
648
|
sql,
|
|
630
649
|
r,
|
|
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
|
|
|
4
4
|
__url__ = 'https://www.tinybird.co/docs/forward/commands'
|
|
5
5
|
__author__ = 'Tinybird'
|
|
6
6
|
__author_email__ = 'support@tinybird.co'
|
|
7
|
-
__version__ = '4.6.
|
|
8
|
-
__revision__ = '
|
|
7
|
+
__version__ = '4.6.15.dev0'
|
|
8
|
+
__revision__ = '8311ae9'
|
|
@@ -465,6 +465,7 @@ class TinyB:
|
|
|
465
465
|
sql_condition: Optional[str] = None,
|
|
466
466
|
format: str = "csv",
|
|
467
467
|
replace_options: Optional[Set[str]] = None,
|
|
468
|
+
use_v1: bool = False,
|
|
468
469
|
):
|
|
469
470
|
params = {"name": datasource_name, "mode": mode, "format": format, "debug": "blocks_block_log"}
|
|
470
471
|
|
|
@@ -476,6 +477,30 @@ class TinyB:
|
|
|
476
477
|
|
|
477
478
|
with open(file, "rb") as content:
|
|
478
479
|
file_content = content.read()
|
|
480
|
+
content_types = {
|
|
481
|
+
"csv": "text/csv",
|
|
482
|
+
"ndjson": "application/x-ndjson",
|
|
483
|
+
"parquet": "application/vnd.apache.parquet",
|
|
484
|
+
}
|
|
485
|
+
headers = {"Content-Type": content_types[format]}
|
|
486
|
+
if str(file).endswith(".gz"):
|
|
487
|
+
headers["Content-Encoding"] = "gzip"
|
|
488
|
+
if use_v1:
|
|
489
|
+
v1_params = {"format": format}
|
|
490
|
+
if sql_condition:
|
|
491
|
+
v1_params["replace_condition"] = sql_condition
|
|
492
|
+
if replace_options:
|
|
493
|
+
for option in list(replace_options):
|
|
494
|
+
v1_params[option] = "true"
|
|
495
|
+
res = self._req(
|
|
496
|
+
f"/v1/datasources/{quote(datasource_name, safe='')}/{mode}?{urlencode(v1_params, safe='')}",
|
|
497
|
+
data=file_content,
|
|
498
|
+
headers=headers,
|
|
499
|
+
method="POST",
|
|
500
|
+
)
|
|
501
|
+
if "error" in res:
|
|
502
|
+
raise Exception(res["error"])
|
|
503
|
+
return res
|
|
479
504
|
if format == "csv":
|
|
480
505
|
files = {"csv": ("csv", file_content)}
|
|
481
506
|
else:
|
|
@@ -925,7 +925,8 @@ def push_data(
|
|
|
925
925
|
replace_options=None,
|
|
926
926
|
concurrency: int = 1,
|
|
927
927
|
silent: bool = False,
|
|
928
|
-
|
|
928
|
+
use_v1: bool = False,
|
|
929
|
+
) -> Optional[list[str]]:
|
|
929
930
|
if url and type(url) is tuple:
|
|
930
931
|
url = url[0]
|
|
931
932
|
|
|
@@ -962,6 +963,8 @@ def push_data(
|
|
|
962
963
|
datasource_name: str, url: str, mode: str, sql_condition: Optional[str], replace_options: Optional[Set[str]]
|
|
963
964
|
):
|
|
964
965
|
parsed = urlparse(url)
|
|
966
|
+
if use_v1 and parsed.scheme in ("http", "https"):
|
|
967
|
+
raise CLIException("--experimental=use_v1 only supports local files.")
|
|
965
968
|
# poor man's format detection
|
|
966
969
|
_format = get_format_from_filename_or_url(url)
|
|
967
970
|
if parsed.scheme in ("http", "https"):
|
|
@@ -982,8 +985,15 @@ def push_data(
|
|
|
982
985
|
sql_condition=sql_condition,
|
|
983
986
|
format=_format,
|
|
984
987
|
replace_options=replace_options,
|
|
988
|
+
use_v1=use_v1,
|
|
985
989
|
)
|
|
986
990
|
|
|
991
|
+
if use_v1:
|
|
992
|
+
job_id = res.get("id") or res.get("import_id")
|
|
993
|
+
if not isinstance(job_id, str):
|
|
994
|
+
raise CLIException("The v1 import response did not include a job ID.")
|
|
995
|
+
return job_id
|
|
996
|
+
|
|
987
997
|
datasource_name = res["datasource"]["name"]
|
|
988
998
|
try:
|
|
989
999
|
datasource = client.get_datasource(datasource_name)
|
|
@@ -1014,6 +1024,8 @@ def push_data(
|
|
|
1014
1024
|
try:
|
|
1015
1025
|
tasks = [process_url(datasource_name, url, mode, sql_condition, replace_options) for url in urls]
|
|
1016
1026
|
output = gather_with_concurrency(concurrency, *tasks)
|
|
1027
|
+
if use_v1:
|
|
1028
|
+
return list(output)
|
|
1017
1029
|
parser, total_rows, appended_rows = list(output)[-1]
|
|
1018
1030
|
except AuthNoTokenException:
|
|
1019
1031
|
raise
|
|
@@ -1031,6 +1043,8 @@ def push_data(
|
|
|
1031
1043
|
|
|
1032
1044
|
click.echo(FeedbackManager.success_progress_blocks())
|
|
1033
1045
|
|
|
1046
|
+
return None
|
|
1047
|
+
|
|
1034
1048
|
|
|
1035
1049
|
def sync_data(ctx, datasource_name: str, yes: bool):
|
|
1036
1050
|
client: TinyB = ctx.obj["client"]
|
|
@@ -85,7 +85,7 @@ class CLIConfig:
|
|
|
85
85
|
def to_dict(self) -> Dict[str, Any]:
|
|
86
86
|
"""Helper to ease"""
|
|
87
87
|
result: Dict[str, Any] = {}
|
|
88
|
-
result.update(
|
|
88
|
+
result.update({v.name: deepcopy(v.value) for v in self._values.values()})
|
|
89
89
|
return result
|
|
90
90
|
|
|
91
91
|
def __getitem__(self, key) -> Any:
|
|
@@ -162,12 +162,12 @@ class PipeChecker(unittest.TestCase):
|
|
|
162
162
|
|
|
163
163
|
if self.ignore_order:
|
|
164
164
|
current_data = (
|
|
165
|
-
sorted(normalize_array(current_data), key=itemgetter(*
|
|
165
|
+
sorted(normalize_array(current_data), key=itemgetter(*list(current_data[0].keys())))
|
|
166
166
|
if len(current_data) > 0
|
|
167
167
|
else current_data
|
|
168
168
|
)
|
|
169
169
|
checker_data = (
|
|
170
|
-
sorted(normalize_array(checker_data), key=itemgetter(*
|
|
170
|
+
sorted(normalize_array(checker_data), key=itemgetter(*list(checker_data[0].keys())))
|
|
171
171
|
if len(checker_data) > 0
|
|
172
172
|
else checker_data
|
|
173
173
|
)
|