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/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
- elif "Unknown function tb_secret" in msg:
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
- elif "Unknown function tb_var" in msg:
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**15)
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
- sql = sql.replace("``.", "")
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
- else:
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
- else:
256
- while inspect.ismethod(func):
257
- func = func.__func__
258
- while isinstance(func, functools.partial):
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
- else:
664
- # Python 3.6
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
- else:
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, run_in_main=main)
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
 
@@ -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(
@@ -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
- else:
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
- else:
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
- elif current_ws.get("release") and not override_commit:
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
- to_run = await folder_push(
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
- to_run = await folder_push(
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
- else:
732
- response = job_response.get("result", {})
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
- else:
761
- return f"{round(metric, 3)} seconds"
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
- use_v1: bool = False,
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
- if use_v1 and parsed.scheme in ("http", "https"):
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 parsed.scheme in ("http", "https"):
1016
- res = await client.datasource_create_from_url(
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
- if use_v1:
1037
- job_id = res.get("id") or res.get("import_id")
1038
- if not isinstance(job_id, str):
1039
- raise CLIException("The v1 import response did not include a job ID.")
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
- output = await gather_with_concurrency(concurrency, *tasks)
1072
- if use_v1:
1073
- return list(output)
1074
- parser, total_rows, appended_rows = list(output)[-1]
1075
- except AuthNoTokenException:
1076
- raise
1077
- except OperationCanNotBePerformed as e:
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
- else:
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.keys():
1307
+ for key, value in params.items():
1362
1308
  try:
1363
- api_params[ConnectionReplacements._PARAMS_REPLACEMENTS[service][key]] = params[key]
1309
+ api_params[ConnectionReplacements._PARAMS_REPLACEMENTS[service][key]] = value
1364
1310
  except KeyError:
1365
- api_params[key] = 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
- else:
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: