tinybird 0.0.1.dev306__py3-none-any.whl → 1.0.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. tinybird/datafile/common.py +4 -1
  2. tinybird/feedback_manager.py +3 -0
  3. tinybird/service_datasources.py +57 -8
  4. tinybird/sql_template.py +1 -1
  5. tinybird/sql_template_fmt.py +14 -4
  6. tinybird/tb/__cli__.py +2 -2
  7. tinybird/tb/cli.py +1 -0
  8. tinybird/tb/client.py +104 -22
  9. tinybird/tb/modules/agent/tools/execute_query.py +1 -1
  10. tinybird/tb/modules/agent/tools/request_endpoint.py +1 -1
  11. tinybird/tb/modules/branch.py +150 -0
  12. tinybird/tb/modules/build.py +51 -10
  13. tinybird/tb/modules/build_common.py +4 -2
  14. tinybird/tb/modules/cli.py +32 -10
  15. tinybird/tb/modules/common.py +161 -134
  16. tinybird/tb/modules/connection.py +125 -194
  17. tinybird/tb/modules/connection_kafka.py +382 -0
  18. tinybird/tb/modules/copy.py +3 -1
  19. tinybird/tb/modules/create.py +11 -0
  20. tinybird/tb/modules/datafile/build.py +1 -1
  21. tinybird/tb/modules/datafile/format_pipe.py +44 -5
  22. tinybird/tb/modules/datafile/playground.py +1 -1
  23. tinybird/tb/modules/datasource.py +475 -324
  24. tinybird/tb/modules/deployment.py +2 -0
  25. tinybird/tb/modules/deployment_common.py +81 -43
  26. tinybird/tb/modules/deprecations.py +4 -4
  27. tinybird/tb/modules/dev_server.py +33 -12
  28. tinybird/tb/modules/info.py +50 -7
  29. tinybird/tb/modules/job_common.py +15 -0
  30. tinybird/tb/modules/local.py +91 -21
  31. tinybird/tb/modules/local_common.py +320 -13
  32. tinybird/tb/modules/local_logs.py +209 -0
  33. tinybird/tb/modules/login.py +3 -2
  34. tinybird/tb/modules/login_common.py +252 -9
  35. tinybird/tb/modules/open.py +10 -5
  36. tinybird/tb/modules/project.py +14 -5
  37. tinybird/tb/modules/shell.py +14 -6
  38. tinybird/tb/modules/sink.py +3 -1
  39. tinybird/tb/modules/telemetry.py +7 -3
  40. tinybird/tb_cli_modules/telemetry.py +1 -1
  41. {tinybird-0.0.1.dev306.dist-info → tinybird-1.0.5.dist-info}/METADATA +29 -4
  42. {tinybird-0.0.1.dev306.dist-info → tinybird-1.0.5.dist-info}/RECORD +45 -41
  43. {tinybird-0.0.1.dev306.dist-info → tinybird-1.0.5.dist-info}/WHEEL +1 -1
  44. {tinybird-0.0.1.dev306.dist-info → tinybird-1.0.5.dist-info}/entry_points.txt +0 -0
  45. {tinybird-0.0.1.dev306.dist-info → tinybird-1.0.5.dist-info}/top_level.txt +0 -0
@@ -2127,6 +2127,9 @@ def parse(
2127
2127
  return ParseResult(datafile=doc, warnings=warnings)
2128
2128
 
2129
2129
 
2130
+ # TODO: This class is duplicated in tinybird/datafile_common.py with a slightly different
2131
+ # _REPLACEMENTS tuple. The duplication happened during the CLI/server code split (commit
2132
+ # f86d02cdd7). Consider extracting shared code into a common module that both files can import.
2130
2133
  class ImportReplacements:
2131
2134
  _REPLACEMENTS: Tuple[Tuple[str, str, Optional[str]], ...] = (
2132
2135
  ("import_strategy", "mode", "replace"),
@@ -2146,7 +2149,7 @@ class ImportReplacements:
2146
2149
  return [x[0] for x in ImportReplacements._REPLACEMENTS]
2147
2150
 
2148
2151
  @staticmethod
2149
- def get_api_param_for_datafile_param(connector_service: str, key: str) -> Tuple[Optional[str], Optional[str]]:
2152
+ def get_api_param_for_datafile_param(key: str) -> Tuple[Optional[str], Optional[str]]:
2150
2153
  """Returns the API parameter name and default value for a given
2151
2154
  datafile parameter.
2152
2155
  """
@@ -506,6 +506,9 @@ Ready? """
506
506
  prompt_init_git_release_force = prompt_message(
507
507
  "You are going to manually update workspace commit reference manually, this is just for special occasions. Do you want to update current commit reference '{current_commit}' to '{new_commit}'?"
508
508
  )
509
+ prompt_init_git_release_new = prompt_message(
510
+ "This workspace does not have any release yet. Do you want to create one with commit '{commit}'? This will enable 'tb deploy' to work."
511
+ )
509
512
 
510
513
  warning_exchange = warning_message(
511
514
  "Warning: Do you want to exchange Data Source {datasource_a} by Data Source {datasource_b}?"
@@ -5,6 +5,7 @@ This module provides access to predefined service datasources and their schemas
5
5
  for both Tinybird and Organization scopes.
6
6
  """
7
7
 
8
+ from functools import lru_cache
8
9
  from typing import Any, Dict, List, Optional
9
10
 
10
11
 
@@ -490,6 +491,7 @@ def get_tinybird_service_datasources() -> List[Dict[str, Any]]:
490
491
  "engine": "ReplacingMergeTree",
491
492
  "sorting_key": "event_time, organization_id, query_id",
492
493
  "partition_key": "toStartOfDay(event_time)",
494
+ "ttl": "toDate(event_time) + INTERVAL 30 DAY",
493
495
  },
494
496
  "columns": [
495
497
  {"name": "event_time", "type": "DateTime"},
@@ -516,18 +518,46 @@ def get_tinybird_service_datasources() -> List[Dict[str, Any]]:
516
518
  {
517
519
  "name": "tinybird.vcpu_time",
518
520
  "description": "vCPU time metrics from your workspace.",
519
- "dateColumn": "minute_slot",
521
+ "dateColumn": "second_slot",
520
522
  "engine": {
521
523
  "engine": "AggregatingMergeTree",
522
- "sorting_key": "organization_id, minute_slot",
523
- "partition_key": "toStartOfDay(minute_slot)",
524
+ "sorting_key": "organization_id, second_slot",
525
+ "partition_key": "toStartOfDay(second_slot)",
526
+ "ttl": "toDate(second_slot) + INTERVAL 30 DAY",
524
527
  },
525
528
  "columns": [
526
- {"name": "minute_slot", "type": "DateTime"},
529
+ {"name": "second_slot", "type": "DateTime"},
527
530
  {"name": "organization_id", "type": "String"},
528
531
  {"name": "vcpu_time", "type": "Float64"},
529
532
  ],
530
533
  },
534
+ {
535
+ "name": "tinybird.query_validator_log",
536
+ "description": "Log of failed queries executions in the next available ClickHouse version and their results.",
537
+ "dateColumn": "run_validation",
538
+ "engine": {
539
+ "engine": "MergeTree",
540
+ "sorting_key": "database, host, run_validation, query_hash",
541
+ "partition_key": "toYYYYMM(run_validation)",
542
+ },
543
+ "columns": [
544
+ {"name": "host", "type": "LowCardinality(String)"},
545
+ {"name": "version", "type": "LowCardinality(String)"},
546
+ {"name": "stable_version", "type": "LowCardinality(String)"},
547
+ {"name": "query_hash", "type": "UInt64"},
548
+ {"name": "query_last_execution", "type": "DateTime"},
549
+ {"name": "region", "type": "String"},
550
+ {"name": "workspace", "type": "String"},
551
+ {"name": "database", "type": "String"},
552
+ {"name": "pipe_name", "type": "String"},
553
+ {"name": "query_id", "type": "String"},
554
+ {"name": "query", "type": "String"},
555
+ {"name": "error_code", "type": "Int16"},
556
+ {"name": "error", "type": "String"},
557
+ {"name": "fix_suggestion", "type": "String"},
558
+ {"name": "run_validation", "type": "DateTime"},
559
+ ],
560
+ },
531
561
  ]
532
562
 
533
563
 
@@ -906,6 +936,22 @@ def get_organization_service_datasources() -> List[Dict[str, Any]]:
906
936
  {"name": "active_minutes", "type": "Float64"},
907
937
  ],
908
938
  },
939
+ {
940
+ "name": "organization.shared_infra_active_seconds",
941
+ "description": "Contains information about vCPU active seconds consumption aggregated by second for all Organization workspaces. Only available for Developer and Enterprise plans in shared infrastructure.",
942
+ "dateColumn": "second",
943
+ "engine": {
944
+ "engine": "AggregatingMergeTree",
945
+ "sorting_key": "second_slot, organization_id",
946
+ "partition_key": "toYYYYMM(second_slot)",
947
+ },
948
+ "columns": [
949
+ {"name": "second_slot", "type": "DateTime"},
950
+ {"name": "organization_id", "type": "String"},
951
+ {"name": "organization_name", "type": "String"},
952
+ {"name": "cpu_time", "type": "Float64"},
953
+ ],
954
+ },
909
955
  {
910
956
  "name": "organization.shared_infra_qps_overages",
911
957
  "description": "Contains information about QPS consumption and overages aggregated by second for all Organization workspaces. Only available for Developer and Enterprise plans in shared infrastructure.",
@@ -1016,6 +1062,7 @@ def get_organization_service_datasources() -> List[Dict[str, Any]]:
1016
1062
  "engine": "ReplacingMergeTree",
1017
1063
  "sorting_key": "event_time, organization_id, query_id",
1018
1064
  "partition_key": "toStartOfDay(event_time)",
1065
+ "ttl": "toDate(event_time) + INTERVAL 30 DAY",
1019
1066
  },
1020
1067
  "columns": [
1021
1068
  {"name": "event_time", "type": "DateTime"},
@@ -1042,14 +1089,15 @@ def get_organization_service_datasources() -> List[Dict[str, Any]]:
1042
1089
  {
1043
1090
  "name": "organization.vcpu_time",
1044
1091
  "description": "vCPU time metrics from your workspace.",
1045
- "dateColumn": "minute_slot",
1092
+ "dateColumn": "second_slot",
1046
1093
  "engine": {
1047
1094
  "engine": "AggregatingMergeTree",
1048
- "sorting_key": "organization_id, minute_slot",
1049
- "partition_key": "toStartOfDay(minute_slot)",
1095
+ "sorting_key": "organization_id, second_slot",
1096
+ "partition_key": "toStartOfDay(second_slot)",
1097
+ "ttl": "toDate(second_slot) + INTERVAL 30 DAY",
1050
1098
  },
1051
1099
  "columns": [
1052
- {"name": "minute_slot", "type": "DateTime"},
1100
+ {"name": "second_slot", "type": "DateTime"},
1053
1101
  {"name": "organization_id", "type": "String"},
1054
1102
  {"name": "workspace_id", "type": "String"},
1055
1103
  {"name": "vcpu_time", "type": "Float64"},
@@ -1058,6 +1106,7 @@ def get_organization_service_datasources() -> List[Dict[str, Any]]:
1058
1106
  ]
1059
1107
 
1060
1108
 
1109
+ @lru_cache(maxsize=1)
1061
1110
  def get_service_datasources() -> List[Dict[str, Any]]:
1062
1111
  """
1063
1112
  Get the list of all Tinybird and Organization service datasources.
tinybird/sql_template.py CHANGED
@@ -1841,7 +1841,7 @@ def get_var_names_and_types(t, node_id=None):
1841
1841
  raise SQLTemplateException(e)
1842
1842
 
1843
1843
 
1844
- @lru_cache(maxsize=256)
1844
+ @lru_cache(maxsize=512)
1845
1845
  def get_var_names_and_types_cached(t: Template):
1846
1846
  return get_var_names_and_types(t)
1847
1847
 
@@ -203,13 +203,15 @@ class TinybirdDialect(ClickHouse):
203
203
  Rule(
204
204
  name="jinja_if_block_end",
205
205
  priority=203,
206
- pattern=group(r"\{%-?\s*end\s*-?%\}"),
206
+ # Accept both Tornado-style {% end %} and {% end if %}
207
+ pattern=group(r"\{%-?\s*end(\s+if)?\s*-?%\}"),
207
208
  action=actions.raise_sqlfmt_bracket_error,
208
209
  ),
209
210
  Rule(
210
211
  name="jinja_for_block_end",
211
212
  priority=211,
212
- pattern=group(r"\{%-?\s*end\s*-?%\}"),
213
+ # Accept both Tornado-style {% end %} and {% end for %}
214
+ pattern=group(r"\{%-?\s*end(\s+for)?\s*-?%\}"),
213
215
  action=actions.raise_sqlfmt_bracket_error,
214
216
  ),
215
217
  ],
@@ -261,7 +263,13 @@ def _calc_str(self) -> str:
261
263
  Comment._calc_str = property(_calc_str)
262
264
 
263
265
 
264
- def format_sql_template(sql: str, line_length: Optional[int] = None, lower_keywords: bool = False) -> str:
266
+ def format_sql_template(
267
+ sql: str,
268
+ line_length: Optional[int] = None,
269
+ lower_keywords: bool = False,
270
+ resource_name: Optional[str] = None,
271
+ resource_source: Optional[str] = None,
272
+ ) -> str:
265
273
  try:
266
274
  # https://github.com/tconbeer/sqlfmt/blob/c11775b92d8a45f0e91d871b81a88a894d620bec/src/sqlfmt/mode.py#L16-L29
267
275
  config: Dict[str, Any] = {
@@ -277,5 +285,7 @@ def format_sql_template(sql: str, line_length: Optional[int] = None, lower_keywo
277
285
  else api.format_string(sql, mode=mode).strip()
278
286
  )
279
287
  except Exception as e:
280
- logging.warning(f"sqlfmt error: {str(e)}")
288
+ resource_info = f" in '{resource_name}'" if resource_name else ""
289
+ source_info = f" ({resource_source})" if resource_source else ""
290
+ logging.warning(f"sqlfmt error{resource_info}{source_info}: {str(e)}")
281
291
  return sql
tinybird/tb/__cli__.py CHANGED
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
4
4
  __url__ = 'https://www.tinybird.co/docs/forward/commands'
5
5
  __author__ = 'Tinybird'
6
6
  __author_email__ = 'support@tinybird.co'
7
- __version__ = '0.0.1.dev306'
8
- __revision__ = 'dcef65b'
7
+ __version__ = '1.0.5'
8
+ __revision__ = '60ae688'
tinybird/tb/cli.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import tinybird.tb.modules.agent
2
+ import tinybird.tb.modules.branch
2
3
  import tinybird.tb.modules.build
3
4
  import tinybird.tb.modules.cli
4
5
  import tinybird.tb.modules.common
tinybird/tb/client.py CHANGED
@@ -300,33 +300,33 @@ class TinyB:
300
300
  response = self._req(f"/v0/connectors?{urlencode(params)}")
301
301
  return response["connectors"]
302
302
 
303
- def connections(self, connector: Optional[str] = None):
303
+ def connections(self, connector: Optional[str] = None, datasources: Optional[List[Dict[str, Any]]] = None):
304
304
  response = self._req("/v0/connectors")
305
305
  connectors = response["connectors"]
306
+ connectors_to_return = []
307
+ for c in connectors:
308
+ if connector and c["service"] != connector:
309
+ continue
310
+ if connector == "gcscheduler":
311
+ continue
312
+ if datasources and len(datasources) > 0:
313
+ datasource_ids = [linker["datasource_id"] for linker in c["linkers"]]
314
+ datasource_names = [ds["name"] for ds in datasources if ds["id"] in datasource_ids]
315
+ connected_datasources = ", ".join(datasource_names) if len(datasource_names) > 0 else ""
316
+ else:
317
+ connected_datasources = str(len(c["linkers"]))
306
318
 
307
- if connector:
308
- return [
319
+ connectors_to_return.append(
309
320
  {
310
321
  "id": c["id"],
311
322
  "service": c["service"],
312
323
  "name": c["name"],
313
- "connected_datasources": len(c["linkers"]),
324
+ "connected_datasources": connected_datasources,
314
325
  **c["settings"],
315
326
  }
316
- for c in connectors
317
- if c["service"] == connector
318
- ]
319
- return [
320
- {
321
- "id": c["id"],
322
- "service": c["service"],
323
- "name": c["name"],
324
- "connected_datasources": len(c["linkers"]),
325
- **c["settings"],
326
- }
327
- for c in connectors
328
- if c["service"] != "gcscheduler"
329
- ]
327
+ )
328
+
329
+ return connectors_to_return
330
330
 
331
331
  def get_datasource(self, ds_name: str, used_by: bool = False) -> Dict[str, Any]:
332
332
  params = {
@@ -711,7 +711,7 @@ class TinyB:
711
711
  return self._req(f"/{version}/user/workspaces/?with_environments=true&only_environments=true")
712
712
 
713
713
  def branches(self):
714
- return self._req("/v0/environments")
714
+ return self._req("/v1/environments")
715
715
 
716
716
  def releases(self, workspace_id):
717
717
  return self._req(f"/v0/workspaces/{workspace_id}/releases")
@@ -740,7 +740,7 @@ class TinyB:
740
740
  }
741
741
  if ignore_datasources:
742
742
  params["ignore_datasources"] = ",".join(ignore_datasources)
743
- return self._req(f"/v0/environments?{urlencode(params)}", method="POST", data=b"")
743
+ return self._req(f"/v1/environments?{urlencode(params)}", method="POST", data=b"")
744
744
 
745
745
  def branch_workspace_data(
746
746
  self,
@@ -996,10 +996,92 @@ class TinyB:
996
996
  data=json.dumps(connection_params),
997
997
  )
998
998
 
999
- def kafka_list_topics(self, connection_id: str, timeout=5):
1000
- resp = self._req(f"/v0/connectors/{connection_id}/preview?preview_activity=false", timeout=timeout)
999
+ def kafka_list_topics(self, connection_id: str, timeout=10, retries=3):
1000
+ resp = self._req(
1001
+ f"/v0/connectors/{connection_id}/preview?preview_activity=false",
1002
+ timeout=timeout,
1003
+ retries=retries,
1004
+ )
1001
1005
  return [x["topic"] for x in resp["preview"]]
1002
1006
 
1007
+ def kafka_preview_group(self, connection_id: str, topic: str, group_id: str, timeout=30):
1008
+ params = {
1009
+ "log": "previewGroup",
1010
+ "kafka_group_id": group_id,
1011
+ "kafka_topic": topic,
1012
+ "preview_group": "true",
1013
+ }
1014
+ return self._req(f"/v0/connectors/{connection_id}/preview?{urlencode(params)}", method="GET", timeout=timeout)
1015
+
1016
+ def kafka_preview_topic(self, connection_id: str, topic: str, group_id: str, timeout: int = 30) -> Dict[str, Any]:
1017
+ """Preview a Kafka topic and return structured preview data.
1018
+
1019
+ Args:
1020
+ connection_id: The ID of the Kafka connection
1021
+ topic: The Kafka topic name to preview
1022
+ group_id: The Kafka consumer group ID
1023
+ timeout: Request timeout in seconds
1024
+
1025
+ Returns:
1026
+ A dictionary containing:
1027
+ - analysis: Dictionary with columns information
1028
+ - preview: Dictionary with data and meta arrays
1029
+ - earliestTimestamp: The earliest message timestamp (if available)
1030
+ """
1031
+ params = {
1032
+ "max_records": "12",
1033
+ "preview_activity": "true",
1034
+ "preview_earliest_timestamp": "true",
1035
+ "kafka_topic": topic,
1036
+ "kafka_group_id": group_id,
1037
+ "log": "previewTopic",
1038
+ }
1039
+ response = self._req(
1040
+ f"/v0/connectors/{connection_id}/preview?{urlencode(params)}", method="GET", timeout=timeout
1041
+ )
1042
+
1043
+ if not response:
1044
+ return {
1045
+ "analysis": {"columns": []},
1046
+ "preview": {"data": [], "meta": []},
1047
+ "earliestTimestamp": "",
1048
+ }
1049
+
1050
+ # Extract preview data (similar to TypeScript previewKafkaTopic)
1051
+ preview_data = response.get("preview", [])
1052
+ if not preview_data:
1053
+ return {
1054
+ "analysis": {"columns": []},
1055
+ "preview": {"data": [], "meta": []},
1056
+ "earliestTimestamp": "",
1057
+ }
1058
+
1059
+ topic_preview = preview_data[0]
1060
+ analysis = topic_preview.get("analysis", {})
1061
+ deserialized = topic_preview.get("deserialized", {})
1062
+
1063
+ # Extract columns from analysis
1064
+ columns = analysis.get("columns", []) if analysis else []
1065
+
1066
+ # Extract data and meta from deserialized
1067
+ base_data = deserialized.get("data", []) if deserialized else []
1068
+ base_meta = deserialized.get("meta", []) if deserialized else []
1069
+
1070
+ # Extract earliest timestamp
1071
+ earliest = response.get("earliest", [])
1072
+ earliest_timestamp = earliest[0].get("timestamp", "") if earliest else ""
1073
+
1074
+ return {
1075
+ "analysis": {
1076
+ "columns": columns,
1077
+ },
1078
+ "preview": {
1079
+ "data": base_data,
1080
+ "meta": base_meta,
1081
+ },
1082
+ "earliestTimestamp": earliest_timestamp,
1083
+ }
1084
+
1003
1085
  def get_gcp_service_account_details(self) -> Dict[str, Any]:
1004
1086
  return self._req("/v0/datasources-bigquery-credentials")
1005
1087
 
@@ -148,7 +148,7 @@ def execute_query(
148
148
  ctx.deps.thinking_animation.stop()
149
149
  click.echo(FeedbackManager.error(message=error))
150
150
  ctx.deps.thinking_animation.start()
151
- if "not found" in error.lower() and cloud:
151
+ if "not found" in error.lower() and cloud_or_local == "cloud":
152
152
  return f"Error executing query: {error}. Please run the query against Tinybird local instead of cloud."
153
153
  else:
154
154
  return f"Error executing query: {error}. Please try again."
@@ -87,7 +87,7 @@ def request_endpoint(
87
87
  click.echo(FeedbackManager.error(message=error))
88
88
  ctx.deps.thinking_animation.start()
89
89
  not_found_errors = ["not found", "does not exist"]
90
- if any(not_found_error in error.lower() for not_found_error in not_found_errors) and cloud:
90
+ if any(not_found_error in error.lower() for not_found_error in not_found_errors) and cloud_or_local == "cloud":
91
91
  return f"Error executing query: {error}. Please run the query against Tinybird local instead of cloud."
92
92
  else:
93
93
  return f"Error executing query: {error}. Please try again."
@@ -0,0 +1,150 @@
1
+ # This is a command file for our CLI. Please keep it clean.
2
+ #
3
+ # - If it makes sense and only when strictly necessary, you can create utility functions in this file.
4
+ # - But please, **do not** interleave utility functions and command definitions.
5
+
6
+ from typing import List, Optional, Tuple
7
+
8
+ import click
9
+
10
+ from tinybird.tb.modules.cli import cli
11
+ from tinybird.tb.modules.common import (
12
+ MAIN_BRANCH,
13
+ create_workspace_branch,
14
+ echo_safe_humanfriendly_tables_format_smart_table,
15
+ get_current_main_workspace,
16
+ get_current_workspace_branches,
17
+ get_workspace_member_email,
18
+ switch_to_workspace_by_user_workspace_data,
19
+ try_update_config_with_remote,
20
+ )
21
+ from tinybird.tb.modules.config import CLIConfig
22
+ from tinybird.tb.modules.exceptions import CLIBranchException, CLIException
23
+ from tinybird.tb.modules.feedback_manager import FeedbackManager
24
+
25
+
26
+ @cli.group()
27
+ def branch() -> None:
28
+ """Branch commands. Custom branches is an experimental feature in beta."""
29
+ pass
30
+
31
+
32
+ @branch.command(name="ls")
33
+ @click.option("--sort/--no-sort", default=False, help="Sort the table rows by name")
34
+ def branch_ls(sort: bool) -> None:
35
+ """List all the branches available in the current workspace"""
36
+
37
+ config = CLIConfig.get_project_config()
38
+ _ = try_update_config_with_remote(config, only_if_needed=True)
39
+
40
+ client = config.get_client()
41
+
42
+ current_main_workspace = get_current_main_workspace(config)
43
+ assert isinstance(current_main_workspace, dict)
44
+
45
+ if current_main_workspace["id"] != config["id"]:
46
+ client = config.get_client(token=current_main_workspace["token"])
47
+
48
+ response = client.branches()
49
+
50
+ columns = ["name", "id", "created_at", "owner", "current"]
51
+
52
+ table: List[Tuple[str, str, str, str, bool]] = []
53
+
54
+ for branch in response["environments"]:
55
+ branch_owner_email = get_workspace_member_email(branch, branch["owner"])
56
+
57
+ table.append(
58
+ (branch["name"], branch["id"], branch["created_at"], branch_owner_email, config["id"] == branch["id"])
59
+ )
60
+
61
+ current_branch = [row for row in table if row[4]]
62
+ other_branches = [row for row in table if not row[4]]
63
+
64
+ if sort:
65
+ other_branches.sort(key=lambda x: x[0])
66
+
67
+ sorted_table = current_branch + other_branches
68
+
69
+ click.echo(FeedbackManager.info(message="\n** Branches:"))
70
+ echo_safe_humanfriendly_tables_format_smart_table(sorted_table, column_names=columns)
71
+
72
+
73
+ @branch.command(name="create", short_help="Create a new branch in the current Workspace")
74
+ @click.argument("branch_name", required=False)
75
+ @click.option(
76
+ "--last-partition",
77
+ is_flag=True,
78
+ default=False,
79
+ help="Attach the last modified partition from the current workspace to the new branch",
80
+ )
81
+ @click.option(
82
+ "-i",
83
+ "--ignore-datasource",
84
+ "ignore_datasources",
85
+ type=str,
86
+ multiple=True,
87
+ help="Ignore specified data source partitions",
88
+ )
89
+ @click.option(
90
+ "--wait/--no-wait",
91
+ is_flag=True,
92
+ default=True,
93
+ help="Wait for data branch jobs to finish, showing a progress bar. Disabled by default.",
94
+ )
95
+ def create_branch(branch_name: Optional[str], last_partition: bool, ignore_datasources: List[str], wait: bool) -> None:
96
+ create_workspace_branch(branch_name, last_partition, False, list(ignore_datasources), wait)
97
+
98
+
99
+ @branch.command(name="rm", short_help="Removes an branch from the workspace. It can't be recovered.")
100
+ @click.argument("branch_name_or_id")
101
+ @click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
102
+ def delete_branch(branch_name_or_id: str, yes: bool) -> None:
103
+ """Remove an branch"""
104
+
105
+ config = CLIConfig.get_project_config()
106
+ _ = try_update_config_with_remote(config)
107
+
108
+ client = config.get_client()
109
+
110
+ if branch_name_or_id == MAIN_BRANCH:
111
+ raise CLIException(FeedbackManager.error_not_allowed_in_main_branch())
112
+
113
+ try:
114
+ workspace_branches = get_current_workspace_branches(config)
115
+ workspace_to_delete = next(
116
+ (
117
+ workspace
118
+ for workspace in workspace_branches
119
+ if workspace["name"] == branch_name_or_id or workspace["id"] == branch_name_or_id
120
+ ),
121
+ None,
122
+ )
123
+ except Exception as e:
124
+ raise CLIBranchException(FeedbackManager.error_exception(error=str(e)))
125
+
126
+ if not workspace_to_delete:
127
+ raise CLIBranchException(FeedbackManager.error_branch(branch=branch_name_or_id))
128
+
129
+ if yes or click.confirm(FeedbackManager.warning_confirm_delete_branch(branch=workspace_to_delete["name"])):
130
+ need_to_switch_to_main = workspace_to_delete.get("main") and config["id"] == workspace_to_delete["id"]
131
+ # get origin workspace if deleting current branch
132
+ if need_to_switch_to_main:
133
+ try:
134
+ workspaces = (client.user_workspaces()).get("workspaces", [])
135
+ workspace_main = next(
136
+ (workspace for workspace in workspaces if workspace["id"] == workspace_to_delete["main"]), None
137
+ )
138
+ except Exception:
139
+ workspace_main = None
140
+ try:
141
+ client.delete_branch(workspace_to_delete["id"])
142
+ click.echo(FeedbackManager.success_branch_deleted(branch_name=workspace_to_delete["name"]))
143
+ except Exception as e:
144
+ raise CLIBranchException(FeedbackManager.error_exception(error=str(e)))
145
+ else:
146
+ if need_to_switch_to_main:
147
+ if workspace_main:
148
+ switch_to_workspace_by_user_workspace_data(config, workspace_main)
149
+ else:
150
+ raise CLIException(FeedbackManager.error_switching_to_main())