tinybird-cli 6.5.6.dev0__py3-none-any.whl → 6.5.8.dev0__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/__cli__.py +2 -2
- tinybird/ch_utils/constants.py +1 -0
- tinybird/ch_utils/engine.py +90 -54
- tinybird/client.py +80 -89
- tinybird/config.py +0 -4
- tinybird/datafile_common.py +72 -45
- tinybird/datatypes.py +34 -12
- tinybird/feedback_manager.py +5 -0
- tinybird/git_settings.py +1 -3
- tinybird/sql.py +3 -10
- tinybird/sql_template.py +128 -98
- tinybird/sql_template_fmt.py +10 -15
- tinybird/sql_toolset.py +63 -11
- tinybird/syncasync.py +9 -54
- tinybird/tb_cli_modules/branch.py +4 -25
- tinybird/tb_cli_modules/cicd.py +2 -0
- tinybird/tb_cli_modules/cli.py +5 -9
- tinybird/tb_cli_modules/common.py +33 -87
- tinybird/tb_cli_modules/config.py +1 -2
- tinybird/tb_cli_modules/datasource.py +15 -55
- tinybird/tb_cli_modules/pipe.py +49 -34
- tinybird/tb_cli_modules/tinyunit/tinyunit.py +4 -4
- tinybird/tb_cli_modules/token.py +1 -2
- tinybird/tb_cli_modules/workspace.py +11 -13
- tinybird/tornado_template.py +227 -83
- tinybird/utils/__init__.py +0 -0
- tinybird/utils/bools.py +19 -0
- {tinybird_cli-6.5.6.dev0.dist-info → tinybird_cli-6.5.8.dev0.dist-info}/METADATA +15 -1
- tinybird_cli-6.5.8.dev0.dist-info/RECORD +45 -0
- tinybird_cli-6.5.6.dev0.dist-info/RECORD +0 -43
- {tinybird_cli-6.5.6.dev0.dist-info → tinybird_cli-6.5.8.dev0.dist-info}/WHEEL +0 -0
- {tinybird_cli-6.5.6.dev0.dist-info → tinybird_cli-6.5.8.dev0.dist-info}/entry_points.txt +0 -0
- {tinybird_cli-6.5.6.dev0.dist-info → tinybird_cli-6.5.8.dev0.dist-info}/top_level.txt +0 -0
tinybird/sql_toolset.py
CHANGED
|
@@ -314,9 +314,9 @@ def _sql_get_used_tables_impl(
|
|
|
314
314
|
msg = str(e)
|
|
315
315
|
if "is restricted. Contact support@tinybird.co" in msg:
|
|
316
316
|
raise InvalidFunction(msg=msg) from e
|
|
317
|
-
|
|
317
|
+
if "Unknown function tb_secret" in msg:
|
|
318
318
|
raise InvalidFunction(msg="Unknown function tb_secret. Usage: {{tb_secret('secret_name')}}") from e
|
|
319
|
-
|
|
319
|
+
if "Unknown function tb_var" in msg:
|
|
320
320
|
raise InvalidFunction(msg="Unknown function tb_var. Usage: {{tb_var('var_name')}}") from e
|
|
321
321
|
raise
|
|
322
322
|
# Do not cache this fallback result: the returned sql string can contain
|
|
@@ -487,13 +487,12 @@ def replacements_to_tuples(replacements: dict) -> dict:
|
|
|
487
487
|
return parsed_replacements
|
|
488
488
|
|
|
489
489
|
|
|
490
|
-
@lru_cache(maxsize=2**
|
|
490
|
+
@lru_cache(maxsize=2**13)
|
|
491
491
|
def replace_tables_chquery_cached(
|
|
492
492
|
sql: str,
|
|
493
493
|
sorted_replacements: Optional[tuple] = None,
|
|
494
494
|
default_database: str = "",
|
|
495
495
|
output_one_line: bool = False,
|
|
496
|
-
timestamp: Optional[datetime] = None,
|
|
497
496
|
function_allow_list: Optional[FrozenSet[str]] = None,
|
|
498
497
|
settings_allow_list: Optional[FrozenSet[str]] = None,
|
|
499
498
|
) -> str:
|
|
@@ -536,7 +535,6 @@ def replace_tables(
|
|
|
536
535
|
sql,
|
|
537
536
|
None,
|
|
538
537
|
output_one_line=output_one_line,
|
|
539
|
-
timestamp=timestamp,
|
|
540
538
|
function_allow_list=hashable_list,
|
|
541
539
|
settings_allow_list=hashable_settings_list,
|
|
542
540
|
)
|
|
@@ -610,7 +608,6 @@ def replace_tables(
|
|
|
610
608
|
sql,
|
|
611
609
|
None,
|
|
612
610
|
output_one_line=output_one_line,
|
|
613
|
-
timestamp=timestamp,
|
|
614
611
|
function_allow_list=hashable_list,
|
|
615
612
|
settings_allow_list=hashable_settings_list,
|
|
616
613
|
)
|
|
@@ -649,7 +646,6 @@ def replace_tables(
|
|
|
649
646
|
r,
|
|
650
647
|
default_database=default_database,
|
|
651
648
|
output_one_line=output_one_line,
|
|
652
|
-
timestamp=timestamp,
|
|
653
649
|
function_allow_list=hashable_list,
|
|
654
650
|
settings_allow_list=hashable_settings_list,
|
|
655
651
|
)
|
|
@@ -658,16 +654,13 @@ def replace_tables(
|
|
|
658
654
|
sql,
|
|
659
655
|
None,
|
|
660
656
|
output_one_line=output_one_line,
|
|
661
|
-
timestamp=timestamp,
|
|
662
657
|
function_allow_list=hashable_list,
|
|
663
658
|
settings_allow_list=hashable_settings_list,
|
|
664
659
|
)
|
|
665
660
|
|
|
666
661
|
# Fix for empty database names in JOINs - remove empty backticks like ``.table_name
|
|
667
662
|
# that are generated when chquery.replace_tables processes tuples with empty database names
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
return sql
|
|
663
|
+
return sql.replace("``.", "")
|
|
671
664
|
|
|
672
665
|
|
|
673
666
|
def is_invalid_resource(
|
|
@@ -706,3 +699,62 @@ def is_invalid_resource_from_current_workspace(
|
|
|
706
699
|
valid_tables: Optional[Set[Tuple[str, str]]],
|
|
707
700
|
) -> bool:
|
|
708
701
|
return bool(database == default_database and valid_tables and r not in valid_tables and r not in _replaced_with)
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def validate_ttl_expression(
|
|
705
|
+
columns: list[dict[str, str]],
|
|
706
|
+
ttl: Optional[str],
|
|
707
|
+
sorting_key: str,
|
|
708
|
+
primary_key: Optional[str] = None,
|
|
709
|
+
partition_key: Optional[str] = None,
|
|
710
|
+
) -> Optional[str]:
|
|
711
|
+
if not ttl:
|
|
712
|
+
return None
|
|
713
|
+
|
|
714
|
+
kwargs = {"partition_key": partition_key} if partition_key else {}
|
|
715
|
+
error = chquery.validate_ttl(
|
|
716
|
+
ttl=ttl,
|
|
717
|
+
columns=columns,
|
|
718
|
+
sorting_key=sorting_key,
|
|
719
|
+
primary_key=primary_key or sorting_key,
|
|
720
|
+
**kwargs,
|
|
721
|
+
)
|
|
722
|
+
if error is None:
|
|
723
|
+
return None
|
|
724
|
+
return error.removeprefix("DB::Exception: ")
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
TTL_ONLY_DROP_PARTS_SETTING = "ttl_only_drop_parts"
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def should_default_ttl_only_drop_parts(
|
|
731
|
+
columns: list[dict[str, str]],
|
|
732
|
+
ttl: Optional[str],
|
|
733
|
+
sorting_key: str,
|
|
734
|
+
partition_key: Optional[str],
|
|
735
|
+
primary_key: Optional[str] = None,
|
|
736
|
+
) -> bool:
|
|
737
|
+
"""Whether ``ttl_only_drop_parts = 1`` should be defaulted for this table."""
|
|
738
|
+
if not ttl or not partition_key:
|
|
739
|
+
return False
|
|
740
|
+
result = chquery.check_ttl_partition_compatibility(
|
|
741
|
+
columns=columns,
|
|
742
|
+
ttl=ttl,
|
|
743
|
+
sorting_key=sorting_key,
|
|
744
|
+
primary_key=primary_key or sorting_key,
|
|
745
|
+
partition_key=partition_key,
|
|
746
|
+
)
|
|
747
|
+
return result["checked"] and result["error"] is None
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def validate_partition_key_expression(
|
|
751
|
+
columns: list[dict[str, str]],
|
|
752
|
+
partition_key: Optional[str],
|
|
753
|
+
) -> Optional[str]:
|
|
754
|
+
if not partition_key:
|
|
755
|
+
return None
|
|
756
|
+
|
|
757
|
+
error = chquery.validate_partition_key(columns=columns, partition_key=partition_key)
|
|
758
|
+
if error is None:
|
|
759
|
+
return None
|
|
760
|
+
return error.removeprefix("DB::Exception: ")
|
tinybird/syncasync.py
CHANGED
|
@@ -142,8 +142,7 @@ class Local:
|
|
|
142
142
|
storage = self._get_storage()
|
|
143
143
|
if key in storage:
|
|
144
144
|
return storage[key]
|
|
145
|
-
|
|
146
|
-
raise AttributeError(f"{self!r} object has no attribute {key!r}")
|
|
145
|
+
raise AttributeError(f"{self!r} object has no attribute {key!r}")
|
|
147
146
|
|
|
148
147
|
def __setattr__(self, key, value):
|
|
149
148
|
if key in ("_context_refs", "_thread_critical", "_thread_lock", "_attr_name"):
|
|
@@ -151,6 +150,7 @@ class Local:
|
|
|
151
150
|
with self._thread_lock:
|
|
152
151
|
storage = self._get_storage()
|
|
153
152
|
storage[key] = value
|
|
153
|
+
return None
|
|
154
154
|
|
|
155
155
|
def __delattr__(self, key):
|
|
156
156
|
with self._thread_lock:
|
|
@@ -252,56 +252,12 @@ def _iscoroutinefunction_or_partial(func: Any) -> bool:
|
|
|
252
252
|
# this to exist. Code taken from CPython.
|
|
253
253
|
if sys.version_info >= (3, 8):
|
|
254
254
|
return asyncio.iscoroutinefunction(func)
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
func = func.func
|
|
260
|
-
|
|
261
|
-
return asyncio.iscoroutinefunction(func)
|
|
255
|
+
while inspect.ismethod(func):
|
|
256
|
+
func = func.__func__
|
|
257
|
+
while isinstance(func, functools.partial):
|
|
258
|
+
func = func.func
|
|
262
259
|
|
|
263
|
-
|
|
264
|
-
class ThreadSensitiveContext:
|
|
265
|
-
"""Async context manager to manage context for thread sensitive mode
|
|
266
|
-
This context manager controls which thread pool executor is used when in
|
|
267
|
-
thread sensitive mode. By default, a single thread pool executor is shared
|
|
268
|
-
within a process.
|
|
269
|
-
In Python 3.7+, the ThreadSensitiveContext() context manager may be used to
|
|
270
|
-
specify a thread pool per context.
|
|
271
|
-
In Python 3.6, usage of this context manager has no effect.
|
|
272
|
-
This context manager is re-entrant, so only the outer-most call to
|
|
273
|
-
ThreadSensitiveContext will set the context.
|
|
274
|
-
"""
|
|
275
|
-
|
|
276
|
-
def __init__(self):
|
|
277
|
-
self.token = None
|
|
278
|
-
|
|
279
|
-
if contextvars:
|
|
280
|
-
|
|
281
|
-
async def __aenter__(self):
|
|
282
|
-
try:
|
|
283
|
-
SyncToAsync.thread_sensitive_context.get()
|
|
284
|
-
except LookupError:
|
|
285
|
-
self.token = SyncToAsync.thread_sensitive_context.set(self)
|
|
286
|
-
|
|
287
|
-
return self
|
|
288
|
-
|
|
289
|
-
async def __aexit__(self, exc, value, tb):
|
|
290
|
-
if not self.token:
|
|
291
|
-
return
|
|
292
|
-
|
|
293
|
-
executor = SyncToAsync.context_to_thread_executor.pop(self, None)
|
|
294
|
-
if executor:
|
|
295
|
-
executor.shutdown()
|
|
296
|
-
SyncToAsync.thread_sensitive_context.reset(self.token)
|
|
297
|
-
|
|
298
|
-
else:
|
|
299
|
-
|
|
300
|
-
async def __aenter__(self):
|
|
301
|
-
return self
|
|
302
|
-
|
|
303
|
-
async def __aexit__(self, exc, value, tb):
|
|
304
|
-
pass
|
|
260
|
+
return asyncio.iscoroutinefunction(func)
|
|
305
261
|
|
|
306
262
|
|
|
307
263
|
class AsyncToSync:
|
|
@@ -660,9 +616,8 @@ class SyncToAsync:
|
|
|
660
616
|
if hasattr(asyncio, "current_task"):
|
|
661
617
|
# Python 3.7 and up
|
|
662
618
|
return asyncio.current_task()
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
return asyncio.Task.current_task()
|
|
619
|
+
# Python 3.6
|
|
620
|
+
return asyncio.Task.current_task()
|
|
666
621
|
except RuntimeError:
|
|
667
622
|
return None
|
|
668
623
|
|
|
@@ -264,8 +264,7 @@ async def release_rm(semver: str, oldest_rollback: bool, force: bool, yes: bool,
|
|
|
264
264
|
if not oldest_rollback_semver:
|
|
265
265
|
click.echo(FeedbackManager.info_release_no_rollback())
|
|
266
266
|
return
|
|
267
|
-
|
|
268
|
-
semver = oldest_rollback_semver
|
|
267
|
+
semver = oldest_rollback_semver
|
|
269
268
|
await print_release_summary(config, semver, info=True, dry_run=dry_run)
|
|
270
269
|
if dry_run or yes or click.confirm(FeedbackManager.warning_confirm_delete_release(semver=semver)):
|
|
271
270
|
try:
|
|
@@ -555,17 +554,9 @@ async def data_branch(last_partition: bool, all: bool, wait: bool) -> None:
|
|
|
555
554
|
default=False,
|
|
556
555
|
help="Flag to skip execution of regression tests. This is handy for CI branches where regression might be flaky",
|
|
557
556
|
)
|
|
558
|
-
@click.option(
|
|
559
|
-
"--main",
|
|
560
|
-
is_flag=True,
|
|
561
|
-
default=False,
|
|
562
|
-
help="Run regression tests in the main Branch. For this flag to work all the resources in the Branch pipe endpoints need to exist in the main Branch.",
|
|
563
|
-
)
|
|
564
557
|
@click.pass_context
|
|
565
558
|
@coro
|
|
566
|
-
async def regression_tests(
|
|
567
|
-
ctx, filename: str, wait: bool, skip_regression_tests: Optional[bool] = False, main: Optional[bool] = False
|
|
568
|
-
):
|
|
559
|
+
async def regression_tests(ctx, filename: str, wait: bool, skip_regression_tests: Optional[bool] = False):
|
|
569
560
|
"""Regression test commands for Branches"""
|
|
570
561
|
if skip_regression_tests:
|
|
571
562
|
click.echo(FeedbackManager.warning_regression_skipped())
|
|
@@ -591,9 +582,7 @@ async def regression_tests(
|
|
|
591
582
|
if current_main_workspace["id"] == config["id"]:
|
|
592
583
|
raise CLIException(FeedbackManager.error_not_allowed_in_main_branch())
|
|
593
584
|
try:
|
|
594
|
-
response = await client.branch_regression_tests_file(
|
|
595
|
-
config["id"], regression_tests_commands, run_in_main=main
|
|
596
|
-
)
|
|
585
|
+
response = await client.branch_regression_tests_file(config["id"], regression_tests_commands)
|
|
597
586
|
if "job" not in response:
|
|
598
587
|
raise CLIBranchException(str(response))
|
|
599
588
|
job_id = response["job"]["job_id"]
|
|
@@ -605,7 +594,7 @@ async def regression_tests(
|
|
|
605
594
|
except Exception as e:
|
|
606
595
|
raise CLIBranchException(FeedbackManager.error_exception(error=str(e)))
|
|
607
596
|
elif not ctx.invoked_subcommand:
|
|
608
|
-
await _run_regression(type="coverage", wait=wait
|
|
597
|
+
await _run_regression(type="coverage", wait=wait)
|
|
609
598
|
|
|
610
599
|
|
|
611
600
|
async def _run_regression(
|
|
@@ -621,7 +610,6 @@ async def _run_regression(
|
|
|
621
610
|
failfast: Optional[bool] = False,
|
|
622
611
|
wait: Optional[bool] = False,
|
|
623
612
|
skip: Optional[bool] = False,
|
|
624
|
-
run_in_main: Optional[bool] = False,
|
|
625
613
|
**kwargs,
|
|
626
614
|
):
|
|
627
615
|
if skip:
|
|
@@ -649,7 +637,6 @@ async def _run_regression(
|
|
|
649
637
|
assert_time_increase_percentage=assert_time_increase_percentage,
|
|
650
638
|
assert_bytes_read_increase_percentage=assert_bytes_read_increase_percentage,
|
|
651
639
|
assert_max_time=assert_max_time,
|
|
652
|
-
run_in_main=run_in_main,
|
|
653
640
|
**kwargs,
|
|
654
641
|
)
|
|
655
642
|
if "job" not in response:
|
|
@@ -729,12 +716,6 @@ async def _run_regression(
|
|
|
729
716
|
default=False,
|
|
730
717
|
help="Flag to skip execution of regression tests. This is handy for CI branches where regression might be flaky",
|
|
731
718
|
)
|
|
732
|
-
@click.option(
|
|
733
|
-
"--main",
|
|
734
|
-
is_flag=True,
|
|
735
|
-
default=False,
|
|
736
|
-
help="Run regression tests in the main Branch. For this flag to work all the resources in the Branch pipe endpoints need to exist in the main Branch.",
|
|
737
|
-
)
|
|
738
719
|
@coro
|
|
739
720
|
async def coverage(
|
|
740
721
|
pipe_name: str,
|
|
@@ -748,7 +729,6 @@ async def coverage(
|
|
|
748
729
|
failfast: bool,
|
|
749
730
|
wait: bool,
|
|
750
731
|
skip_regression_tests: Optional[bool] = False,
|
|
751
|
-
main: Optional[bool] = False,
|
|
752
732
|
):
|
|
753
733
|
await _run_regression(
|
|
754
734
|
"coverage",
|
|
@@ -763,7 +743,6 @@ async def coverage(
|
|
|
763
743
|
failfast,
|
|
764
744
|
wait,
|
|
765
745
|
skip_regression_tests,
|
|
766
|
-
run_in_main=main,
|
|
767
746
|
)
|
|
768
747
|
|
|
769
748
|
|
tinybird/tb_cli_modules/cicd.py
CHANGED
|
@@ -327,6 +327,7 @@ class CICDGeneratorBase:
|
|
|
327
327
|
return FeedbackManager.warning_for_cicd_file(
|
|
328
328
|
file_name=cicd_file.file_name, warning_message=cicd_file.warning_message.format(**params)
|
|
329
329
|
)
|
|
330
|
+
return None
|
|
330
331
|
|
|
331
332
|
def is_already_generated(self, path: str) -> bool:
|
|
332
333
|
for cicd_file in self.cicd_files:
|
|
@@ -396,6 +397,7 @@ def ask_provider_interactively():
|
|
|
396
397
|
FeedbackManager.error_git_provider_index(host_index=provider_index, available_options=available_options)
|
|
397
398
|
)
|
|
398
399
|
provider_index = -1
|
|
400
|
+
return None
|
|
399
401
|
|
|
400
402
|
|
|
401
403
|
async def init_cicd(
|
tinybird/tb_cli_modules/cli.py
CHANGED
|
@@ -261,8 +261,7 @@ async def init(
|
|
|
261
261
|
raise CLIGitReleaseException(
|
|
262
262
|
FeedbackManager.error_dottinyb_not_ignored(git_working_dir=f"{cli_git_release.working_dir()}/")
|
|
263
263
|
)
|
|
264
|
-
|
|
265
|
-
click.echo(FeedbackManager.info_dottinyb_already_ignored())
|
|
264
|
+
click.echo(FeedbackManager.info_dottinyb_already_ignored())
|
|
266
265
|
|
|
267
266
|
if (
|
|
268
267
|
os.path.exists(f"{cli_git_release.working_dir()}/.diff_tmp")
|
|
@@ -271,14 +270,13 @@ async def init(
|
|
|
271
270
|
raise CLIGitReleaseException(
|
|
272
271
|
FeedbackManager.error_dotdiff_not_ignored(git_working_dir=f"{cli_git_release.working_dir()}/")
|
|
273
272
|
)
|
|
274
|
-
|
|
275
|
-
click.echo(FeedbackManager.info_dotdifftemp_already_ignored())
|
|
273
|
+
click.echo(FeedbackManager.info_dotdifftemp_already_ignored())
|
|
276
274
|
|
|
277
275
|
if "release" not in current_ws:
|
|
278
276
|
raise CLIGitReleaseException(FeedbackManager.error_no_correct_token_for_init())
|
|
279
277
|
|
|
280
278
|
# If we have a release and we are not overriding the commit, we check if we have a release already
|
|
281
|
-
|
|
279
|
+
if current_ws.get("release") and not override_commit:
|
|
282
280
|
final_response = FeedbackManager.error_release_already_set(
|
|
283
281
|
workspace=current_ws["name"], commit=current_ws["release"]["commit"]
|
|
284
282
|
)
|
|
@@ -1034,7 +1032,7 @@ async def materialize(
|
|
|
1034
1032
|
pass
|
|
1035
1033
|
|
|
1036
1034
|
filename = str(f.absolute())
|
|
1037
|
-
|
|
1035
|
+
return await folder_push(
|
|
1038
1036
|
cl,
|
|
1039
1037
|
filenames=[filename],
|
|
1040
1038
|
push_deps=push_deps,
|
|
@@ -1043,7 +1041,6 @@ async def materialize(
|
|
|
1043
1041
|
no_versions=no_versions,
|
|
1044
1042
|
verbose=verbose,
|
|
1045
1043
|
)
|
|
1046
|
-
return to_run
|
|
1047
1044
|
|
|
1048
1045
|
def _save_local_pipe(pipe_file_name, pipe_datafile, pipe):
|
|
1049
1046
|
base = Path("pipes")
|
|
@@ -1075,7 +1072,7 @@ async def materialize(
|
|
|
1075
1072
|
check = option == 1
|
|
1076
1073
|
|
|
1077
1074
|
filename = str(f_pipe.absolute())
|
|
1078
|
-
|
|
1075
|
+
return await folder_push(
|
|
1079
1076
|
cl,
|
|
1080
1077
|
filenames=[filename],
|
|
1081
1078
|
dry_run=False,
|
|
@@ -1090,7 +1087,6 @@ async def materialize(
|
|
|
1090
1087
|
run_tests=False,
|
|
1091
1088
|
verbose=verbose,
|
|
1092
1089
|
)
|
|
1093
|
-
return to_run
|
|
1094
1090
|
|
|
1095
1091
|
async def _populate(pipe, node_name, f_pipe):
|
|
1096
1092
|
if force_populate or click.confirm(FeedbackManager.prompt_populate(file=f_pipe)):
|
|
@@ -35,7 +35,6 @@ from packaging.version import Version
|
|
|
35
35
|
from tinybird.client import (
|
|
36
36
|
AuthException,
|
|
37
37
|
AuthNoTokenException,
|
|
38
|
-
DoesNotExistException,
|
|
39
38
|
JobException,
|
|
40
39
|
OperationCanNotBePerformed,
|
|
41
40
|
TinyB,
|
|
@@ -194,6 +193,7 @@ class AliasedGroup(click.Group):
|
|
|
194
193
|
cm = click.Group.get_command(self, ctx, cmd_name)
|
|
195
194
|
if cm is not None:
|
|
196
195
|
return cm
|
|
196
|
+
return None
|
|
197
197
|
|
|
198
198
|
def resolve_command(self, ctx, args):
|
|
199
199
|
# always return the command's name, not the alias
|
|
@@ -454,6 +454,7 @@ def ask_for_region_interactively(regions):
|
|
|
454
454
|
available_options = ", ".join(map(str, range(1, len(regions) + 1)))
|
|
455
455
|
click.echo(FeedbackManager.error_region_index(host_index=region_index, available_options=available_options))
|
|
456
456
|
region_index = -1
|
|
457
|
+
return None
|
|
457
458
|
|
|
458
459
|
|
|
459
460
|
def get_region_info(ctx, region=None):
|
|
@@ -728,9 +729,8 @@ async def create_workspace_branch(
|
|
|
728
729
|
job_response = await wait_job(config.get_client(), job_id, job_url, "Branch creation")
|
|
729
730
|
if job_response is None:
|
|
730
731
|
raise CLIException(f"Empty job API response (job_id: {job_id}, job_url: {job_url})")
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
is_summary = "partitions" in response
|
|
732
|
+
response = job_response.get("result", {})
|
|
733
|
+
is_summary = "partitions" in response
|
|
734
734
|
|
|
735
735
|
await switch_workspace(config, branch_name, only_environments=True)
|
|
736
736
|
if is_summary and (bool(last_partition) or bool(all)):
|
|
@@ -757,10 +757,8 @@ async def print_branch_regression_tests_summary(client, job_id, host, response=N
|
|
|
757
757
|
if isinstance(metric, float):
|
|
758
758
|
if is_percentage:
|
|
759
759
|
return f"{round(metric, 3):+} %"
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
else:
|
|
763
|
-
return metric
|
|
760
|
+
return f"{round(metric, 3)} seconds"
|
|
761
|
+
return metric
|
|
764
762
|
|
|
765
763
|
failed = False
|
|
766
764
|
response = await client.job(job_id) if job_id else response or {"progress": []}
|
|
@@ -970,7 +968,7 @@ async def push_data(
|
|
|
970
968
|
sql_condition: Optional[str] = None,
|
|
971
969
|
replace_options=None,
|
|
972
970
|
concurrency: int = 1,
|
|
973
|
-
|
|
971
|
+
wait: bool = True,
|
|
974
972
|
) -> Optional[list[str]]:
|
|
975
973
|
if url and type(url) is tuple:
|
|
976
974
|
url = url[0]
|
|
@@ -979,24 +977,6 @@ async def push_data(
|
|
|
979
977
|
if not url:
|
|
980
978
|
raise CLIException(FeedbackManager.error_missing_url(datasource=datasource_name))
|
|
981
979
|
|
|
982
|
-
def cb(res):
|
|
983
|
-
if cb.First: # type: ignore[attr-defined]
|
|
984
|
-
blocks_to_process = len([x for x in res["block_log"] if x["status"] == "idle"])
|
|
985
|
-
if blocks_to_process:
|
|
986
|
-
cb.bar = click.progressbar(label=FeedbackManager.info_progress_blocks(), length=blocks_to_process) # type: ignore[attr-defined]
|
|
987
|
-
cb.bar.update(0) # type: ignore[attr-defined]
|
|
988
|
-
cb.First = False # type: ignore[attr-defined]
|
|
989
|
-
cb.blocks_to_process = blocks_to_process # type: ignore[attr-defined]
|
|
990
|
-
else:
|
|
991
|
-
done = len([x for x in res["block_log"] if x["status"] == "done"])
|
|
992
|
-
if done * 2 > cb.blocks_to_process: # type: ignore[attr-defined]
|
|
993
|
-
cb.bar.label = FeedbackManager.info_progress_current_blocks() # type: ignore[attr-defined]
|
|
994
|
-
cb.bar.update(done - cb.prev_done) # type: ignore[attr-defined]
|
|
995
|
-
cb.prev_done = done # type: ignore[attr-defined]
|
|
996
|
-
|
|
997
|
-
cb.First = True # type: ignore[attr-defined]
|
|
998
|
-
cb.prev_done = 0 # type: ignore[attr-defined]
|
|
999
|
-
|
|
1000
980
|
click.echo(FeedbackManager.info_starting_import_process())
|
|
1001
981
|
|
|
1002
982
|
if isinstance(url, list):
|
|
@@ -1006,18 +986,16 @@ async def push_data(
|
|
|
1006
986
|
|
|
1007
987
|
async def process_url(
|
|
1008
988
|
datasource_name: str, url: str, mode: str, sql_condition: Optional[str], replace_options: Optional[Set[str]]
|
|
1009
|
-
):
|
|
989
|
+
) -> str:
|
|
1010
990
|
parsed = urlparse(url)
|
|
1011
|
-
|
|
1012
|
-
raise CLIException("--experimental=use_v1 only supports local files.")
|
|
991
|
+
is_remote_url = parsed.scheme in ("http", "https")
|
|
1013
992
|
# poor man's format detection
|
|
1014
993
|
_format = get_format_from_filename_or_url(url)
|
|
1015
|
-
if
|
|
1016
|
-
res = await client.
|
|
994
|
+
if is_remote_url:
|
|
995
|
+
res = await client.datasource_append_url(
|
|
1017
996
|
datasource_name,
|
|
1018
997
|
url,
|
|
1019
998
|
mode=mode,
|
|
1020
|
-
status_callback=cb,
|
|
1021
999
|
sql_condition=sql_condition,
|
|
1022
1000
|
format=_format,
|
|
1023
1001
|
replace_options=replace_options,
|
|
@@ -1030,66 +1008,35 @@ async def push_data(
|
|
|
1030
1008
|
sql_condition=sql_condition,
|
|
1031
1009
|
format=_format,
|
|
1032
1010
|
replace_options=replace_options,
|
|
1033
|
-
use_v1=use_v1,
|
|
1034
1011
|
)
|
|
1035
1012
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
return job_id
|
|
1041
|
-
|
|
1042
|
-
datasource_name = res["datasource"]["name"]
|
|
1043
|
-
try:
|
|
1044
|
-
datasource = await client.get_datasource(datasource_name)
|
|
1045
|
-
except DoesNotExistException:
|
|
1046
|
-
raise CLIException(FeedbackManager.error_datasource_does_not_exist(datasource=datasource_name))
|
|
1047
|
-
except Exception as e:
|
|
1048
|
-
raise CLIException(FeedbackManager.error_exception(error=str(e)))
|
|
1049
|
-
|
|
1050
|
-
total_rows = (datasource.get("statistics", {}) or {}).get("row_count", 0)
|
|
1051
|
-
appended_rows = 0
|
|
1052
|
-
parser = None
|
|
1053
|
-
|
|
1054
|
-
if res.get("error"):
|
|
1055
|
-
raise CLIException(FeedbackManager.error_exception(error=res["error"]))
|
|
1056
|
-
if res.get("errors"):
|
|
1057
|
-
raise CLIException(FeedbackManager.error_exception(error=res["errors"]))
|
|
1058
|
-
if res.get("blocks"):
|
|
1059
|
-
for block in res["blocks"]:
|
|
1060
|
-
if "process_return" in block and block["process_return"] is not None:
|
|
1061
|
-
process_return = block["process_return"][0]
|
|
1062
|
-
parser = process_return["parser"] if process_return.get("parser") else parser
|
|
1063
|
-
if parser and parser != "clickhouse":
|
|
1064
|
-
parser = process_return["parser"]
|
|
1065
|
-
appended_rows += process_return["lines"]
|
|
1066
|
-
|
|
1067
|
-
return parser, total_rows, appended_rows
|
|
1013
|
+
job_id = res.get("id") or res.get("import_id")
|
|
1014
|
+
if not isinstance(job_id, str):
|
|
1015
|
+
raise CLIException("We couldn't confirm that your import started. Please try again.")
|
|
1016
|
+
return job_id
|
|
1068
1017
|
|
|
1069
1018
|
try:
|
|
1070
1019
|
tasks = [process_url(datasource_name, url, mode, sql_condition, replace_options) for url in urls]
|
|
1071
|
-
|
|
1072
|
-
if
|
|
1073
|
-
return
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
raise CLIException(FeedbackManager.error_operation_can_not_be_performed(error=e))
|
|
1079
|
-
except Exception as e:
|
|
1080
|
-
raise CLIException(FeedbackManager.error_exception(error=e))
|
|
1081
|
-
else:
|
|
1082
|
-
click.echo(FeedbackManager.success_progress_blocks())
|
|
1083
|
-
if mode == "append" and parser and parser != "clickhouse":
|
|
1084
|
-
click.echo(FeedbackManager.success_appended_rows(appended_rows=appended_rows))
|
|
1085
|
-
|
|
1020
|
+
job_ids = list(await gather_with_concurrency(concurrency, *tasks))
|
|
1021
|
+
if not wait:
|
|
1022
|
+
return job_ids
|
|
1023
|
+
for job_id in job_ids:
|
|
1024
|
+
await wait_job_no_ui(client, job_id)
|
|
1025
|
+
datasource = await client.get_datasource(datasource_name)
|
|
1026
|
+
total_rows = (datasource.get("statistics", {}) or {}).get("row_count", 0)
|
|
1086
1027
|
click.echo(FeedbackManager.success_total_rows(datasource=datasource_name, total_rows=total_rows))
|
|
1087
|
-
|
|
1088
1028
|
if mode == "replace":
|
|
1089
1029
|
click.echo(FeedbackManager.success_replaced_datasource(datasource=datasource_name))
|
|
1090
1030
|
else:
|
|
1091
1031
|
click.echo(FeedbackManager.success_appended_datasource(datasource=datasource_name))
|
|
1032
|
+
click.echo(FeedbackManager.success_progress_blocks())
|
|
1092
1033
|
click.echo(FeedbackManager.info_data_pushed(datasource=datasource_name))
|
|
1034
|
+
except AuthNoTokenException:
|
|
1035
|
+
raise
|
|
1036
|
+
except OperationCanNotBePerformed as e:
|
|
1037
|
+
raise CLIException(FeedbackManager.error_operation_can_not_be_performed(error=e))
|
|
1038
|
+
except Exception as e:
|
|
1039
|
+
raise CLIException(FeedbackManager.error_exception(error=e))
|
|
1093
1040
|
|
|
1094
1041
|
return None
|
|
1095
1042
|
|
|
@@ -1230,8 +1177,7 @@ async def switch_workspace(config: CLIConfig, workspace_name_or_id: str, only_en
|
|
|
1230
1177
|
if not workspace:
|
|
1231
1178
|
if only_environments:
|
|
1232
1179
|
raise CLIException(FeedbackManager.error_branch(branch=workspace_name_or_id, cli="tb"))
|
|
1233
|
-
|
|
1234
|
-
raise CLIException(FeedbackManager.error_workspace(workspace=workspace_name_or_id, cli="tb"))
|
|
1180
|
+
raise CLIException(FeedbackManager.error_workspace(workspace=workspace_name_or_id, cli="tb"))
|
|
1235
1181
|
|
|
1236
1182
|
config.set_token(workspace["token"])
|
|
1237
1183
|
config.set_token_for_host(workspace["token"], config.get_host())
|
|
@@ -1358,11 +1304,11 @@ class ConnectionReplacements:
|
|
|
1358
1304
|
"""Maps prompt parameters to API parameters."""
|
|
1359
1305
|
|
|
1360
1306
|
api_params = {}
|
|
1361
|
-
for key in params.
|
|
1307
|
+
for key, value in params.items():
|
|
1362
1308
|
try:
|
|
1363
|
-
api_params[ConnectionReplacements._PARAMS_REPLACEMENTS[service][key]] =
|
|
1309
|
+
api_params[ConnectionReplacements._PARAMS_REPLACEMENTS[service][key]] = value
|
|
1364
1310
|
except KeyError:
|
|
1365
|
-
api_params[key] =
|
|
1311
|
+
api_params[key] = value
|
|
1366
1312
|
|
|
1367
1313
|
api_params["service"] = service
|
|
1368
1314
|
return api_params
|
|
@@ -121,8 +121,7 @@ class CLIConfig:
|
|
|
121
121
|
return self._values[key].origin
|
|
122
122
|
if self._parent:
|
|
123
123
|
return self._parent.get_value_origin(key)
|
|
124
|
-
|
|
125
|
-
return ConfigValueOrigin.NONE
|
|
124
|
+
return ConfigValueOrigin.NONE
|
|
126
125
|
|
|
127
126
|
def persist_to_file(self, override_with_values: Optional["CLIConfig"] = None) -> None:
|
|
128
127
|
if not self._path:
|