tinybird-cli 1.1.1.dev0__tar.gz → 1.1.1.dev2__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.
Files changed (45) hide show
  1. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/PKG-INFO +17 -1
  2. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/__cli__.py +2 -2
  3. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/ch_utils/engine.py +10 -0
  4. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/client.py +16 -12
  5. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/config.py +2 -1
  6. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/datafile.py +8 -6
  7. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/feedback_manager.py +9 -2
  8. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/tb_cli_modules/branch.py +59 -11
  9. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/tb_cli_modules/cli.py +10 -13
  10. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/tb_cli_modules/common.py +20 -15
  11. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/tb_cli_modules/config.py +10 -0
  12. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/tb_cli_modules/datasource.py +50 -1
  13. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/tb_cli_modules/pipe.py +1 -11
  14. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird_cli.egg-info/PKG-INFO +17 -1
  15. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/setup.cfg +0 -0
  16. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/ch_utils/constants.py +0 -0
  17. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/check_pypi.py +0 -0
  18. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/connector_settings.py +0 -0
  19. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/connectors.py +0 -0
  20. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/context.py +0 -0
  21. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/datatypes.py +0 -0
  22. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/sql.py +0 -0
  23. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/sql_template.py +0 -0
  24. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/sql_template_fmt.py +0 -0
  25. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/sql_toolset.py +0 -0
  26. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/syncasync.py +0 -0
  27. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/tb_cli.py +0 -0
  28. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/tb_cli_modules/auth.py +0 -0
  29. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/tb_cli_modules/cicd.py +0 -0
  30. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/tb_cli_modules/connection.py +0 -0
  31. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/tb_cli_modules/exceptions.py +0 -0
  32. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/tb_cli_modules/job.py +0 -0
  33. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/tb_cli_modules/telemetry.py +0 -0
  34. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/tb_cli_modules/test.py +0 -0
  35. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  36. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
  37. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/tb_cli_modules/token.py +0 -0
  38. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/tb_cli_modules/workspace.py +0 -0
  39. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  40. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird/tornado_template.py +0 -0
  41. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird_cli.egg-info/SOURCES.txt +0 -0
  42. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird_cli.egg-info/dependency_links.txt +0 -0
  43. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird_cli.egg-info/entry_points.txt +0 -0
  44. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird_cli.egg-info/requires.txt +0 -0
  45. {tinybird-cli-1.1.1.dev0 → tinybird-cli-1.1.1.dev2}/tinybird_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tinybird-cli
3
- Version: 1.1.1.dev0
3
+ Version: 1.1.1.dev2
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://docs.tinybird.co/cli.html
6
6
  Author: Tinybird
@@ -19,10 +19,26 @@ Changelog
19
19
 
20
20
  ---------
21
21
 
22
+ 1.1.1.dev2
23
+ ************
24
+
25
+ - `Fixed` Do not print data branching summary in `tb env create <env_name>`
26
+
27
+ 1.1.1.dev1
28
+ ************
29
+
30
+ - `Changed` internal Releases management.
31
+
32
+ 1.1.1.dev0
33
+ ************
34
+
35
+ - `Changed` internal Releases management.
36
+
22
37
  1.1.0
23
38
  ************
24
39
 
25
40
  Released new version 1.1.0 with all these changes:
41
+
26
42
  - `Added` Support `skip` individual regression tests through the regression.yaml configuration file. CI/CD guide at https://www.tinybird.co/docs/guides/continuous-integration.html
27
43
  - `Added` Fail `tb env regression-tests` if no tests are run for any pipe, use `skip` to avoid the error
28
44
  - `Added` Skip POST requests by default in `tb env regression-tests` until it's supported
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
4
4
  __url__ = 'https://docs.tinybird.co/cli.html'
5
5
  __author__ = 'Tinybird'
6
6
  __author_email__ = 'support@tinybird.co'
7
- __version__ = '1.1.1dev0'
8
- __revision__ = '695ba55'
7
+ __version__ = '1.1.1.dev2'
8
+ __revision__ = '2438918'
@@ -64,6 +64,10 @@ class TableDetails:
64
64
  >>> ed = TableDetails({ "engine_full": "MergeTree() PARTITION BY toYear(timestamp) ORDER BY (timestamp, cityHash64(location)) SAMPLE BY cityHash64(location) SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1, merge_with_ttl_timeout = 1800 TTL toDate(timestamp) + INTERVAL 1 DAY"})
65
65
  >>> ed.engine_full
66
66
  'MergeTree() PARTITION BY toYear(timestamp) ORDER BY (timestamp, cityHash64(location)) SAMPLE BY cityHash64(location) SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1, merge_with_ttl_timeout = 1800 TTL toDate(timestamp) + INTERVAL 1 DAY'
67
+
68
+ >>> x = TableDetails({'database': 'd_01', 'name': 't_01', 'create_table_query': "CREATE TABLE d_01.t_01 (`project_id` String, `project_name` String, `project_repo` String, `owner_id` String, `updated_at` DateTime64(3)) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/{layer}-{shard}/d_test_1ad5e496b29246e1ade99117e9180f6f.t_1bac899a56b34b33921fbf468b4500f7', '{replica}', updated_at) PARTITION BY tuple() PRIMARY KEY project_id ORDER BY project_id SETTINGS index_granularity = 32", 'engine': 'ReplicatedReplacingMergeTree', 'partition_key': 'tuple()', 'sorting_key': 'project_id', 'primary_key': 'project_id', 'sampling_key': '', 'engine_full': "ReplicatedReplacingMergeTree('/clickhouse/tables/{layer}-{shard}/d_01.t_01', '{replica}', updated_at) PARTITION BY tuple() PRIMARY KEY project_id ORDER BY project_id SETTINGS index_granularity = 32", 'settings': 'index_granularity = 32', 'ttl': ''})
69
+ >>> x.primary_key
70
+
67
71
  """
68
72
 
69
73
  def __init__(self, details: Optional[Dict[str, Any]] = None):
@@ -144,6 +148,8 @@ class TableDetails:
144
148
  @property
145
149
  def primary_key(self) -> Optional[str]:
146
150
  _primary_key = self.details.get("primary_key", None)
151
+ # When querying `system.tables`, it will return the `sorting_key` as `primary_key` even if it was not specify
152
+ # So we need to ignore it
147
153
  if self.sorting_key == _primary_key:
148
154
  return None
149
155
  return _primary_key
@@ -615,6 +621,8 @@ def engine_full_from_dict(
615
621
  Traceback (most recent call last):
616
622
  ...
617
623
  ValueError: You can not use 'schema' and 'columns' at the same time
624
+ >>> engine_full_from_dict('ReplacingMergeTree', {'partition_key': 'tuple()', 'sorting_key': 'project_id', 'settings': 'index_granularity = 32', 'ver': 'updated_at'}, "`project_id` String, `project_name` String, `project_repo` String, `owner_id` String, `updated_at` DateTime64(3)")
625
+ 'ReplacingMergeTree(updated_at) PARTITION BY (tuple()) ORDER BY (project_id) SETTINGS index_granularity = 32'
618
626
  """
619
627
 
620
628
  if schema is not None and columns is not None:
@@ -653,6 +661,8 @@ def engine_params_from_engine_full(engine_full: str) -> Dict[str, Any]:
653
661
  {'ver': 'insert_date'}
654
662
  >>> engine_params_from_engine_full("ReplicatedVersionedCollapsingMergeTree('/clickhouse/tables/{layer}-{shard}/test.foo', '{replica}', sign_c,version_c) ORDER BY pk TTL toDate(local_timeplaced) + toIntervalDay(3) SETTINGS index_granularity = 8192")
655
663
  {'sign': 'sign_c', 'version': 'version_c'}
664
+ >>> engine_params_from_engine_full("ReplacingMergeTree(updated_at) PARTITION BY tuple() PRIMARY KEY project_id ORDER BY project_id SETTINGS index_granularity = 32")
665
+ {'ver': 'updated_at'}
656
666
  """
657
667
  engine_full = engine_replicated_to_local(engine_full)
658
668
  for engine, (params, _options) in ENABLED_ENGINES:
@@ -88,6 +88,7 @@ class TinyB(object):
88
88
  version: Optional[str] = None,
89
89
  disable_ssl_checks: bool = False,
90
90
  send_telemetry: bool = False,
91
+ semver: Optional[str] = None,
91
92
  ):
92
93
  ctx = ssl.create_default_context()
93
94
  ctx.check_hostname = False
@@ -98,6 +99,7 @@ class TinyB(object):
98
99
  self.version = version
99
100
  self.disable_ssl_checks = disable_ssl_checks
100
101
  self.send_telemetry = send_telemetry
102
+ self.semver = semver
101
103
 
102
104
  async def _req(
103
105
  self, endpoint: str, data=None, files=None, method: str = "GET", retries: int = LIMIT_RETRIES, **kwargs
@@ -108,6 +110,8 @@ class TinyB(object):
108
110
  url += ("&" if "?" in endpoint else "?") + "token=" + self.token
109
111
  if self.version:
110
112
  url += ("&" if "?" in url else "?") + "cli_version=" + quote(self.version)
113
+ if self.semver:
114
+ url += ("&" if "?" in url else "?") + "__tb__semver=" + self.semver
111
115
 
112
116
  verify_ssl = not self.disable_ssl_checks
113
117
  try:
@@ -466,7 +470,6 @@ class TinyB(object):
466
470
  populate_condition: Optional[str] = None,
467
471
  truncate: bool = True,
468
472
  unlink_on_populate_error: bool = False,
469
- semver: Optional[str] = None,
470
473
  ):
471
474
  params: Dict[str, Any] = {
472
475
  "truncate": "true" if truncate else "false",
@@ -476,8 +479,6 @@ class TinyB(object):
476
479
  params.update({"populate_subset": populate_subset})
477
480
  if populate_condition:
478
481
  params.update({"populate_condition": populate_condition})
479
- if semver:
480
- params.update({"semver": semver})
481
482
  response = await self._req(
482
483
  f"/v0/pipes/{pipe_name}/nodes/{node_name}/population?{urlencode(params)}", method="POST"
483
484
  )
@@ -583,12 +584,10 @@ class TinyB(object):
583
584
  params = {**params} if params else {}
584
585
  return await self._req(f"/v0/pipes/{pipe_name_or_id}/sink?{urlencode(params)}", method="POST")
585
586
 
586
- async def query(self, sql: str, pipeline: Optional[str] = None, semver: Optional[str] = None):
587
+ async def query(self, sql: str, pipeline: Optional[str] = None):
587
588
  params = {}
588
589
  if pipeline:
589
590
  params = {"pipeline": pipeline}
590
- if semver:
591
- params["semver"] = semver
592
591
 
593
592
  if len(sql) > TinyB.MAX_GET_LENGTH:
594
593
  return await self._req(f"/v0/sql?{urlencode(params)}", data=sql, method="POST")
@@ -782,14 +781,11 @@ class TinyB(object):
782
781
  backoff_seconds: float = 2.0,
783
782
  backoff_multiplier: float = 1,
784
783
  maximum_backoff_seconds: float = 2.0,
785
- semver: Optional[str] = None,
786
784
  ) -> Dict[str, Any]:
787
785
  res: Dict[str, Any] = {}
788
786
  done: bool = False
789
787
  while not done:
790
788
  params = {"debug": "blocks,block_log"}
791
- if semver:
792
- params["semver"] = semver
793
789
  res = await self._req(f"/v0/jobs/{job_id}?{urlencode(params)}")
794
790
 
795
791
  if res["status"] == "error":
@@ -1071,10 +1067,8 @@ class TinyB(object):
1071
1067
  async def regions(self):
1072
1068
  return await self._req("/v0/regions")
1073
1069
 
1074
- async def datasource_query_copy(self, datasource_name: str, sql_query: str, semver: str):
1070
+ async def datasource_query_copy(self, datasource_name: str, sql_query: str):
1075
1071
  params = {"copy_to": datasource_name}
1076
- if semver:
1077
- params["semver"] = semver
1078
1072
  return await self._req(f"/v0/sql_copy?{urlencode(params)}", data=sql_query, method="POST")
1079
1073
 
1080
1074
  async def workspace_commit_update(self, workspace_id: str, commit: str):
@@ -1082,6 +1076,16 @@ class TinyB(object):
1082
1076
  f"/v0/workspaces/{workspace_id}/releases/?commit={commit}&force=true", method="POST", data=""
1083
1077
  )
1084
1078
 
1079
+ async def release_new(self, workspace_id: str, semver: str, commit: str):
1080
+ params = {
1081
+ "commit": commit,
1082
+ "semver": semver,
1083
+ }
1084
+ return await self._req(f"/v0/workspaces/{workspace_id}/releases/?{urlencode(params)}", method="POST", data="")
1085
+
1086
+ async def release_preview(self, workspace_id: str, semver: str):
1087
+ return await self._req(f"/v0/workspaces/{workspace_id}/releases/{semver}?status=preview", method="PUT")
1088
+
1085
1089
  async def release_promote(self, workspace_id: str, semver: str):
1086
1090
  return await self._req(f"/v0/workspaces/{workspace_id}/releases/{semver}?status=live", method="PUT")
1087
1091
 
@@ -22,7 +22,7 @@ PROJECT_PATHS = ["datasources", "datasources/fixtures", "endpoints", "pipes", "t
22
22
  MIN_WORKSPACE_ID_LENGTH = 36
23
23
 
24
24
 
25
- async def get_config(host: str, token: Optional[str]) -> Dict[str, Any]:
25
+ async def get_config(host: str, token: Optional[str], semver: Optional[str] = None) -> Dict[str, Any]:
26
26
  if host:
27
27
  host = host.rstrip("/")
28
28
 
@@ -39,6 +39,7 @@ async def get_config(host: str, token: Optional[str]) -> Dict[str, Any]:
39
39
 
40
40
  config["token_passed"] = token
41
41
  config["token"] = token or config.get("token", None)
42
+ config["semver"] = semver or config.get("semver", None)
42
43
  config["host"] = host or config.get("host", DEFAULT_API_HOST)
43
44
  config["workspaces"] = config.get("workspaces", [])
44
45
  return config
@@ -488,8 +488,8 @@ class Deployment:
488
488
  def deploying(self):
489
489
  click.echo(FeedbackManager.info_deployment_deploying_release_header())
490
490
 
491
- async def update_release(self, commit: Optional[str] = None):
492
- if not self.is_git_release or self.dry_run:
491
+ async def update_release(self, commit: Optional[str] = None, has_semver: Optional[bool] = False):
492
+ if not self.is_git_release or self.dry_run or has_semver:
493
493
  return
494
494
  self.cli_git_release = self.cli_git_release or CLIGitRelease()
495
495
  release = await self.tb_client.workspace_commit_update(
@@ -3397,6 +3397,9 @@ async def folder_push(
3397
3397
  (workspace for workspace in workspaces if config and workspace.get("id", ".") == config.get("id", "..")), {}
3398
3398
  )
3399
3399
  is_environment = current_ws.get("is_branch", False)
3400
+ has_semver = False
3401
+ if config and config.get("semver"):
3402
+ has_semver = True
3400
3403
 
3401
3404
  deployment = Deployment(current_ws, git_release, tb_client, dry_run)
3402
3405
 
@@ -3454,7 +3457,7 @@ async def folder_push(
3454
3457
  workspace_lib_paths=workspace_lib_paths,
3455
3458
  current_ws=current_ws,
3456
3459
  changed=changed,
3457
- only_changes=only_changes,
3460
+ only_changes=only_changes or (deployment.is_git_release and has_semver),
3458
3461
  fork_downstream=fork_downstream,
3459
3462
  is_internal=is_internal,
3460
3463
  )
@@ -3483,7 +3486,7 @@ async def folder_push(
3483
3486
  workspace_lib_paths=workspace_lib_paths,
3484
3487
  current_ws=current_ws,
3485
3488
  changed=changed,
3486
- only_changes=only_changes,
3489
+ only_changes=only_changes or (deployment.is_git_release and has_semver),
3487
3490
  skip_connectors=is_environment,
3488
3491
  fork_downstream=fork_downstream,
3489
3492
  is_internal=is_internal,
@@ -3659,7 +3662,6 @@ async def folder_push(
3659
3662
  if not deployment.dry_run:
3660
3663
  deployment.deploying()
3661
3664
  await push_files(dry_run)
3662
-
3663
3665
  else:
3664
3666
  await push_files(dry_run)
3665
3667
 
@@ -3708,7 +3710,7 @@ async def folder_push(
3708
3710
  if verbose:
3709
3711
  click.echo(FeedbackManager.info_not_pushing_fixtures())
3710
3712
 
3711
- await deployment.update_release()
3713
+ await deployment.update_release(has_semver=has_semver)
3712
3714
 
3713
3715
  return to_run
3714
3716
 
@@ -78,6 +78,9 @@ class FeedbackManager:
78
78
  error_processing_data = error_message("{error} - FAIL")
79
79
  error_file_already_exists = error_message("{file} already exists, use --force to override")
80
80
  error_invalid_token_for_host = error_message("Invalid token for {host}")
81
+ error_invalid_release_for_workspace = error_message(
82
+ "There's no Release with semver {semver} for Workspace {workspace}"
83
+ )
81
84
  error_invalid_token = error_message(
82
85
  "Invalid token\n** Run 'tb auth --interactive' to select region. If you belong to a custom region, include your region host in the command:\n** tb auth --host https://<region>.tinybird.co"
83
86
  )
@@ -456,7 +459,7 @@ Ready? """
456
459
  "** Do you want to override {name} with the formatted version shown above?"
457
460
  )
458
461
  info_populate_job_url = info_message("** Populating job url {url}")
459
- info_data_branch_job_url = info_message("** Data Branch job url {url}")
462
+ info_data_branch_job_url = info_message("** Environment job url {url}")
460
463
  info_regression_tests_branch_job_url = info_message("** Environment regression tests job url {url}")
461
464
  info_merge_branch_job_url = info_message("** Merge Environment deployment job url {url}")
462
465
  info_copy_from_main_job_url = info_message("** Copy from 'main' Workspace to '{datasource_name}' job url {url}")
@@ -607,6 +610,7 @@ Ready? """
607
610
  success_test_endpoint = info_message(
608
611
  "** => Test endpoint with:\n** $ curl {host}/v0/pipes/{pipe}.json?token={token}"
609
612
  )
613
+ success_deployment_release = success_message("** => Release {semver} created in deployment status")
610
614
  success_test_endpoint_no_token = success_message("** => Test endpoint at {host}/v0/pipes/{pipe}.json")
611
615
  success_promote = success_message(
612
616
  "** Release has been promoted to Live. Run `tb release ls` to list Releases in the Workspace. To rollback to the previous Release run `tb release rollback`."
@@ -756,9 +760,12 @@ Ready? """
756
760
  "** Workspace '{workspace_name}' release initialized to commit '{release_commit}'.\n Now start working with git, pushing changes to pull requests and let the CI/CD work for you. More details in this guide: https://www.tinybird.co/docs/guides/working-with-git.html."
757
761
  )
758
762
  success_release_promote = success_message("** Release {semver} promoted to live")
763
+ success_release_preview = success_message("** Release {semver} in preview status")
759
764
  success_release_rollback = success_message("** Workspace rolled back to Release {semver}")
760
765
  success_release_delete = success_message("** Release {semver} deleted")
761
- success_release_delete_dry_run = success_message("** Release {semver} to delete (dry run)")
766
+ success_release_delete_dry_run = success_message(
767
+ "** Release {semver} delete (dry run). The following resources are not used in any other Release and would be deleted:"
768
+ )
762
769
 
763
770
  success_delete_token = success_message("** Token '{token}' removed successfully")
764
771
  success_refresh_token = success_message("** Token '{token}' refreshed successfully")
@@ -3,6 +3,7 @@
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
5
 
6
+ from os import getcwd
6
7
  from typing import Any, Dict, List, Tuple, Optional
7
8
  import click
8
9
  from click import Context
@@ -128,6 +129,31 @@ tb deploy
128
129
  click.echo(FeedbackManager.info_release_generated(semver=semver))
129
130
 
130
131
 
132
+ @release.command(name="create", short_help="Create a new Release in deploying status")
133
+ @click.option(
134
+ "--semver",
135
+ is_flag=False,
136
+ required=True,
137
+ type=str,
138
+ help="Semver of the new Release. Example: 1.0.0",
139
+ )
140
+ @click.pass_context
141
+ @coro
142
+ async def release_create(ctx: Context, semver: str) -> None:
143
+ client: TinyB = ctx.ensure_object(dict)["client"]
144
+ config = ctx.ensure_object(dict)["config"]
145
+ folder = getcwd()
146
+ cli_git_release = None
147
+ try:
148
+ cli_git_release = CLIGitRelease(path=folder)
149
+ commit = cli_git_release.head_commit()
150
+ except CLIGitReleaseException:
151
+ raise CLIGitReleaseException(FeedbackManager.error_no_git_repo_for_init(repo_path=folder))
152
+
153
+ await client.release_new(config["id"], semver, commit)
154
+ click.echo(FeedbackManager.success_deployment_release(semver=semver))
155
+
156
+
131
157
  @release.command(name="promote", short_help="Promotes to live status a preview Release")
132
158
  @click.option(
133
159
  "--semver",
@@ -161,6 +187,35 @@ async def release_promote(ctx: Context, semver: str) -> None:
161
187
  raise CLIReleaseException(FeedbackManager.error_exception(error=str(e)))
162
188
 
163
189
 
190
+ @release.command(name="preview", short_help="Updates the status of a deploying Release to preview")
191
+ @click.option(
192
+ "--semver", is_flag=False, required=True, type=str, help="Semver of a preview Release to preview. Example: 1.0.0"
193
+ )
194
+ @click.pass_context
195
+ @coro
196
+ async def release_preview(ctx: Context, semver: str) -> None:
197
+ obj: Dict[str, Any] = ctx.ensure_object(dict)
198
+ client: TinyB = obj["client"]
199
+ config = obj["config"]
200
+
201
+ if "id" not in config:
202
+ config = await _get_config(config["host"], config["token"], load_tb_file=False)
203
+
204
+ current_main_workspace = await get_current_main_workspace(client, config)
205
+ if not current_main_workspace:
206
+ raise CLIReleaseException(FeedbackManager.error_exception(error=str("ERROR!")))
207
+ # FIXME validate is not environment
208
+
209
+ if current_main_workspace["id"] != config["id"]:
210
+ client = _get_tb_client(current_main_workspace["token"], config["host"])
211
+
212
+ try:
213
+ await client.release_preview(current_main_workspace["id"], semver)
214
+ click.echo(FeedbackManager.success_release_preview(semver=semver))
215
+ except Exception as e:
216
+ raise CLIReleaseException(FeedbackManager.error_exception(error=str(e)))
217
+
218
+
164
219
  @release.command(name="rollback", short_help="Rollbacks to a previous Release")
165
220
  @click.pass_context
166
221
  @coro
@@ -484,7 +539,7 @@ async def data_branch(ctx: Context, last_partition: bool, all: bool, ignore_data
484
539
  job_id = response["job"]["job_id"]
485
540
  job_url = response["job"]["job_url"]
486
541
  click.echo(FeedbackManager.info_data_branch_job_url(url=job_url))
487
- job_response = await wait_job(client, job_id, job_url, "Data Branching")
542
+ job_response = await wait_job(client, job_id, job_url, "Environment creation")
488
543
  response = job_response["result"]
489
544
  is_job = False
490
545
  is_summary = "partitions" in response
@@ -494,7 +549,7 @@ async def data_branch(ctx: Context, last_partition: bool, all: bool, ignore_data
494
549
  else:
495
550
  if not is_job and not is_summary:
496
551
  FeedbackManager.warning_unknown_response(response=response)
497
- elif is_summary:
552
+ elif is_summary and (bool(last_partition) or bool(all)):
498
553
  await print_data_branch_summary(client, None, response)
499
554
  click.echo(FeedbackManager.success_workspace_data_branch())
500
555
 
@@ -973,17 +1028,10 @@ def datasource(ctx: Context) -> None:
973
1028
  required=False,
974
1029
  )
975
1030
  @click.option("--wait", is_flag=True, default=False, help="Wait for copy job to finish, disabled by default")
976
- @click.option(
977
- "--semver",
978
- is_flag=False,
979
- required=False,
980
- type=str,
981
- help="Release semver where to run the copy operation. Example: 1.0.0",
982
- )
983
1031
  @click.pass_context
984
1032
  @coro
985
1033
  async def datasource_copy_from_main(
986
- ctx: Context, datasource_name: str, sql: str, sql_from_main: bool, wait: bool, semver: str
1034
+ ctx: Context, datasource_name: str, sql: str, sql_from_main: bool, wait: bool
987
1035
  ) -> None:
988
1036
  """Copy data source from Main."""
989
1037
 
@@ -1007,7 +1055,7 @@ async def datasource_copy_from_main(
1007
1055
  return
1008
1056
 
1009
1057
  response = await client.datasource_query_copy(
1010
- datasource_name, sql if sql else f"SELECT * FROM main.{datasource_name}", semver
1058
+ datasource_name, sql if sql else f"SELECT * FROM main.{datasource_name}"
1011
1059
  )
1012
1060
  if "job" not in response:
1013
1061
  raise CLIBranchException(response)
@@ -83,6 +83,7 @@ DEFAULT_PATTERNS: List[Tuple[str, Union[str, Callable[[str], str]]]] = [
83
83
  )
84
84
  @click.option("--token", help="Use auth token, defaults to TB_TOKEN envvar, then to the .tinyb file")
85
85
  @click.option("--host", help="Use custom host, defaults to TB_HOST envvar, then to https://api.tinybird.co")
86
+ @click.option("--semver", help="Semver of a Release to run the command. Example: 1.0.0")
86
87
  @click.option("--gcp-project-id", help="The Google Cloud project ID", hidden=True)
87
88
  @click.option(
88
89
  "--gcs-bucket", help="The Google Cloud Storage bucket to write temp files when using the connectors", hidden=True
@@ -130,6 +131,7 @@ async def cli(
130
131
  debug: bool,
131
132
  token: str,
132
133
  host: str,
134
+ semver: str,
133
135
  gcp_project_id: str,
134
136
  gcs_bucket: str,
135
137
  google_application_credentials: str,
@@ -188,6 +190,8 @@ async def cli(
188
190
  config_temp.set_token(token)
189
191
  if host:
190
192
  config_temp.set_host(host)
193
+ if semver:
194
+ config_temp.set_semver(semver)
191
195
 
192
196
  # Overwrite token and host with env vars manually, without resorting to click.
193
197
  #
@@ -198,8 +202,10 @@ async def cli(
198
202
  token = os.environ.get("TB_TOKEN", "")
199
203
  if not host and "TB_HOST" in os.environ:
200
204
  host = os.environ.get("TB_HOST", "")
205
+ if not semver and "TB_SEMVER" in os.environ:
206
+ semver = os.environ.get("TB_SEMVER", "")
201
207
 
202
- config = await get_config(host, token)
208
+ config = await get_config(host, token, semver)
203
209
  client = _get_tb_client(config.get("token", None), config["host"])
204
210
 
205
211
  # If they have passed a token or host as paramter and it's different that record in .tinyb, refresh the workspace id
@@ -255,7 +261,7 @@ async def cli(
255
261
 
256
262
  logging.debug("debug enabled")
257
263
 
258
- ctx.ensure_object(dict)["client"] = client
264
+ ctx.ensure_object(dict)["client"] = _get_tb_client(config.get("token", None), config["host"], semver)
259
265
 
260
266
  for connector in SUPPORTED_CONNECTORS:
261
267
  load_connector_config(ctx, connector, debug, check_uninstalled=True)
@@ -917,14 +923,6 @@ async def diff(
917
923
  default="human",
918
924
  help="Output format",
919
925
  )
920
- @click.option(
921
- "--semver",
922
- is_flag=False,
923
- required=False,
924
- type=str,
925
- help="Semver of a preview Release to run the query. Example: 1.0.0",
926
- hidden=True,
927
- )
928
926
  @click.option("--stats/--no-stats", default=False, help="Show query stats")
929
927
  @click.pass_context
930
928
  @coro
@@ -936,7 +934,6 @@ async def sql(
936
934
  pipe: Optional[str],
937
935
  node: Optional[str],
938
936
  format_: str,
939
- semver: Optional[str],
940
937
  stats: bool,
941
938
  ) -> None:
942
939
  """Run SQL query over data sources and pipes."""
@@ -954,7 +951,7 @@ async def sql(
954
951
  click.echo(FeedbackManager.error_invalid_query())
955
952
  return
956
953
  res = await client.query(
957
- f"SELECT * FROM ({query}) LIMIT {rows_limit} FORMAT {req_format}", pipeline=pipeline, semver=semver
954
+ f"SELECT * FROM ({query}) LIMIT {rows_limit} FORMAT {req_format}", pipeline=pipeline
958
955
  )
959
956
  elif pipe and node:
960
957
  datasources: List[Dict[str, Any]] = await client.datasources()
@@ -1001,7 +998,7 @@ async def sql(
1001
998
  query = "".join(_node["sql"])
1002
999
  pipeline = pipe.split("/")[-1].split(".pipe")[0]
1003
1000
  res = await client.query(
1004
- f"SELECT * FROM ({query}) LIMIT {rows_limit} FORMAT {req_format}", pipeline=pipeline, semver=semver
1001
+ f"SELECT * FROM ({query}) LIMIT {rows_limit} FORMAT {req_format}", pipeline=pipeline
1005
1002
  )
1006
1003
  except Exception as e:
1007
1004
  click.echo(FeedbackManager.error_exception(error=str(e)))
@@ -274,15 +274,16 @@ def getenv_bool(key: str, default: bool) -> bool:
274
274
  return v.lower() == "true" or v == "1"
275
275
 
276
276
 
277
- def _get_tb_client(token: str, host: str) -> TinyB:
277
+ def _get_tb_client(token: str, host: str, semver: Optional[str] = None) -> TinyB:
278
278
  disable_ssl: bool = getenv_bool("TB_DISABLE_SSL_CHECKS", False)
279
- return TinyB(token, host, version=VERSION, disable_ssl_checks=disable_ssl, send_telemetry=True)
279
+ return TinyB(token, host, version=VERSION, disable_ssl_checks=disable_ssl, send_telemetry=True, semver=semver)
280
280
 
281
281
 
282
282
  def create_tb_client(ctx: Context) -> TinyB:
283
283
  token = ctx.ensure_object(dict)["config"].get("token", "")
284
284
  host = ctx.ensure_object(dict)["config"].get("host", DEFAULT_API_HOST)
285
- return _get_tb_client(token, host)
285
+ semver = ctx.ensure_object(dict)["config"].get("semver", "")
286
+ return _get_tb_client(token, host, semver=semver)
286
287
 
287
288
 
288
289
  async def _analyze(filename: str, client: TinyB, format: str, connector: Optional[Connector] = None):
@@ -488,7 +489,7 @@ async def configure_connector(connector):
488
489
  click.echo(FeedbackManager.success_connector_config(connector=connector, file_name=file_name))
489
490
 
490
491
 
491
- async def _get_config(host, token, load_tb_file=True):
492
+ async def _get_config(host: str, token: str, load_tb_file: bool = True, semver: Optional[str] = None):
492
493
  config = {}
493
494
 
494
495
  try:
@@ -497,6 +498,13 @@ async def _get_config(host, token, load_tb_file=True):
497
498
  except Exception:
498
499
  raise CLIAuthException(FeedbackManager.error_invalid_token_for_host(host=host))
499
500
 
501
+ if semver:
502
+ release_exists = any(release["semver"] == semver for release in response["releases"])
503
+ if not release_exists:
504
+ raise CLIAuthException(
505
+ FeedbackManager.error_invalid_release_for_workspace(semver=semver, workspace=response["name"])
506
+ )
507
+
500
508
  from_response = load_tb_file
501
509
 
502
510
  try:
@@ -953,7 +961,7 @@ async def create_workspace_branch(
953
961
  assert isinstance(job_id, str)
954
962
 
955
963
  # Await the job to finish and get the result dict
956
- job_response = await wait_job(client, job_id, job_url, "Data Branching")
964
+ job_response = await wait_job(client, job_id, job_url, "Environment creation")
957
965
  if job_response is None:
958
966
  raise CLIException(f"Empty job API response (job_id: {job_id}, job_url: {job_url})")
959
967
  else:
@@ -962,7 +970,7 @@ async def create_workspace_branch(
962
970
  is_summary = "partitions" in response
963
971
 
964
972
  await switch_workspace(ctx, branch_name, only_environments=True)
965
- if is_summary:
973
+ if is_summary and (bool(last_partition) or bool(all)):
966
974
  await print_data_branch_summary(client, None, response)
967
975
 
968
976
  except Exception as e:
@@ -1511,13 +1519,14 @@ def _get_setting_value(connection, setting, sensitive_settings):
1511
1519
  return connection.get(setting, "")
1512
1520
 
1513
1521
 
1514
- async def _get_config_or_load_tb_file(config):
1522
+ async def _get_config_or_load_tb_file(config, semver: Optional[str] = None):
1515
1523
  if "id" not in config:
1516
- config = await _get_config(config["host"], config["token"], load_tb_file=False)
1524
+ config = await _get_config(config["host"], config["token"], load_tb_file=False, semver=semver)
1517
1525
  else:
1518
1526
  config_file = Path(getcwd()) / ".tinyb"
1519
1527
  with open(config_file) as file:
1520
1528
  config = json.loads(file.read())
1529
+ config["semver"] = semver
1521
1530
  return config
1522
1531
 
1523
1532
 
@@ -1634,7 +1643,7 @@ async def print_current_branch(ctx):
1634
1643
 
1635
1644
  response = await client.user_workspaces_and_branches()
1636
1645
 
1637
- columns = ["name", "id", "main"]
1646
+ columns = ["name", "id", "workspace"]
1638
1647
  table = []
1639
1648
 
1640
1649
  for workspace in response["workspaces"]:
@@ -1909,7 +1918,6 @@ async def wait_job(
1909
1918
  label: str,
1910
1919
  timeout: Optional[int] = None,
1911
1920
  wait_observer: Optional[Callable[[Dict[str, Any], ProgressBar], None]] = None,
1912
- semver: Optional[str] = None,
1913
1921
  ) -> Dict[str, Any]:
1914
1922
  progress_bar: ProgressBar
1915
1923
  with click.progressbar(
@@ -1932,7 +1940,7 @@ async def wait_job(
1932
1940
 
1933
1941
  try:
1934
1942
  # TODO: Simplify this as it's not needed to use two functions for
1935
- result = await wait_job_no_ui(tb_client, job_id, timeout, progressbar_cb, semver)
1943
+ result = await wait_job_no_ui(tb_client, job_id, timeout, progressbar_cb)
1936
1944
  if result["status"] != "done":
1937
1945
  click.echo(FeedbackManager.error_while_running_job(error=result["error"]))
1938
1946
  return result
@@ -1949,12 +1957,9 @@ async def wait_job_no_ui(
1949
1957
  job_id: str,
1950
1958
  timeout: Optional[int] = None,
1951
1959
  status_callback: Optional[Callable[[Dict[str, Any]], None]] = None,
1952
- semver: Optional[str] = None,
1953
1960
  ) -> Dict[str, Any]:
1954
1961
  try:
1955
- result = await asyncio.wait_for(
1956
- tb_client.wait_for_job(job_id, status_callback=status_callback, semver=semver), timeout
1957
- )
1962
+ result = await asyncio.wait_for(tb_client.wait_for_job(job_id, status_callback=status_callback), timeout)
1958
1963
  if result["status"] != "done":
1959
1964
  raise JobException(result.get("error"))
1960
1965
  return result
@@ -73,6 +73,7 @@ class CLIConfig:
73
73
  "token": "TB_TOKEN",
74
74
  "user_token": "TB_USER_TOKEN",
75
75
  "host": "TB_HOST",
76
+ "semver": "TB_SEMVER",
76
77
  }
77
78
 
78
79
  DEFAULTS: Dict[str, str] = {"host": DEFAULT_API_HOST if not FeatureFlags.is_localhost() else DEFAULT_LOCALHOST}
@@ -178,6 +179,15 @@ class CLIConfig:
178
179
  except KeyError:
179
180
  return None
180
181
 
182
+ def set_semver(self, semver: Optional[str]) -> None:
183
+ self["semver"] = semver
184
+
185
+ def get_semver(self) -> Optional[str]:
186
+ try:
187
+ return self["semver"]
188
+ except KeyError:
189
+ return None
190
+
181
191
  def set_token_for_host(self, token: Optional[str], host: Optional[str]) -> None:
182
192
  """Sets the token for the specified host.
183
193
 
@@ -16,7 +16,7 @@ import humanfriendly
16
16
  from tinybird.client import CanNotBeDeletedException, DoesNotExistException, TinyB
17
17
  from tinybird.connectors import Connector
18
18
  from tinybird.feedback_manager import FeedbackManager
19
- from tinybird.datafile import get_name_tag_version
19
+ from tinybird.datafile import get_name_tag_version, wait_job
20
20
  from tinybird.tb_cli_modules.cli import cli
21
21
  from tinybird.tb_cli_modules.common import (
22
22
  _analyze,
@@ -747,3 +747,52 @@ async def datasource_exchange(ctx, datasource_a, datasource_b):
747
747
  raise CLIDatasourceException(FeedbackManager.error_exception(error=e))
748
748
 
749
749
  click.echo(FeedbackManager.success_exchange_datasources(datasource_a=datasource_a, datasource_b=datasource_b))
750
+
751
+
752
+ @datasource.command(name="copy")
753
+ @click.argument("datasource_name")
754
+ @click.option(
755
+ "--sql",
756
+ default=None,
757
+ help="Freeform SQL query to select what is copied from Main into the Environment Data Source",
758
+ required=False,
759
+ )
760
+ @click.option(
761
+ "--sql-from-main",
762
+ is_flag=True,
763
+ default=False,
764
+ help="SQL query selecting * from the same Data Source in Main",
765
+ required=False,
766
+ )
767
+ @click.option("--wait", is_flag=True, default=False, help="Wait for copy job to finish, disabled by default")
768
+ @click.pass_context
769
+ @coro
770
+ async def datasource_copy_from_main(
771
+ ctx: Context, datasource_name: str, sql: str, sql_from_main: bool, wait: bool
772
+ ) -> None:
773
+ """Copy data source from Main."""
774
+
775
+ client: TinyB = ctx.ensure_object(dict)["client"]
776
+
777
+ if sql and sql_from_main:
778
+ click.echo(FeedbackManager.error_exception(error="Use --sql or --sql-from-main but not both"))
779
+ return
780
+
781
+ if not sql and not sql_from_main:
782
+ click.echo(FeedbackManager.error_exception(error="Use --sql or --sql-from-main"))
783
+ return
784
+
785
+ response = await client.datasource_query_copy(
786
+ datasource_name, sql if sql else f"SELECT * FROM main.{datasource_name}"
787
+ )
788
+ if "job" not in response:
789
+ raise Exception(response)
790
+ job_id = response["job"]["job_id"]
791
+ job_url = response["job"]["job_url"]
792
+ if sql:
793
+ click.echo(FeedbackManager.info_copy_with_sql_job_url(sql=sql, datasource_name=datasource_name, url=job_url))
794
+ else:
795
+ click.echo(FeedbackManager.info_copy_from_main_job_url(datasource_name=datasource_name, url=job_url))
796
+ if wait:
797
+ base_msg = "Copy from Main Workspace" if sql_from_main else f"Copy from {sql}"
798
+ await wait_job(client, job_id, job_url, f"{base_msg} to {datasource_name}")
@@ -240,14 +240,6 @@ async def pipe_ls(ctx: Context, prefix: str, match: str, format_: str):
240
240
  default=False,
241
241
  help="Waits for populate jobs to finish, showing a progress bar. Disabled by default.",
242
242
  )
243
- @click.option(
244
- "--semver",
245
- is_flag=False,
246
- required=False,
247
- type=str,
248
- help="Semver of a preview Release to run the population. Example: 1.0.0",
249
- hidden=True,
250
- )
251
243
  @click.pass_context
252
244
  @coro
253
245
  async def pipe_populate(
@@ -258,7 +250,6 @@ async def pipe_populate(
258
250
  truncate: bool,
259
251
  unlink_on_populate_error: bool,
260
252
  wait: bool,
261
- semver: str,
262
253
  ):
263
254
  """Populate the result of a Materialized Node into the target Materialized View"""
264
255
  cl = create_tb_client(ctx)
@@ -268,7 +259,6 @@ async def pipe_populate(
268
259
  populate_condition=sql_condition,
269
260
  truncate=truncate,
270
261
  unlink_on_populate_error=unlink_on_populate_error,
271
- semver=semver,
272
262
  )
273
263
  if "job" not in response:
274
264
  raise CLIPipeException(response)
@@ -280,7 +270,7 @@ async def pipe_populate(
280
270
  else:
281
271
  click.echo(FeedbackManager.info_populate_job_url(url=job_url))
282
272
  if wait:
283
- await wait_job(cl, job_id, job_url, "Populating", semver=semver)
273
+ await wait_job(cl, job_id, job_url, "Populating")
284
274
 
285
275
 
286
276
  @pipe.command(name="new", hidden=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tinybird-cli
3
- Version: 1.1.1.dev0
3
+ Version: 1.1.1.dev2
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://docs.tinybird.co/cli.html
6
6
  Author: Tinybird
@@ -19,10 +19,26 @@ Changelog
19
19
 
20
20
  ---------
21
21
 
22
+ 1.1.1.dev2
23
+ ************
24
+
25
+ - `Fixed` Do not print data branching summary in `tb env create <env_name>`
26
+
27
+ 1.1.1.dev1
28
+ ************
29
+
30
+ - `Changed` internal Releases management.
31
+
32
+ 1.1.1.dev0
33
+ ************
34
+
35
+ - `Changed` internal Releases management.
36
+
22
37
  1.1.0
23
38
  ************
24
39
 
25
40
  Released new version 1.1.0 with all these changes:
41
+
26
42
  - `Added` Support `skip` individual regression tests through the regression.yaml configuration file. CI/CD guide at https://www.tinybird.co/docs/guides/continuous-integration.html
27
43
  - `Added` Fail `tb env regression-tests` if no tests are run for any pipe, use `skip` to avoid the error
28
44
  - `Added` Skip POST requests by default in `tb env regression-tests` until it's supported