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.
- tinybird/datafile/common.py +4 -1
- tinybird/feedback_manager.py +3 -0
- tinybird/service_datasources.py +57 -8
- tinybird/sql_template.py +1 -1
- tinybird/sql_template_fmt.py +14 -4
- tinybird/tb/__cli__.py +2 -2
- tinybird/tb/cli.py +1 -0
- tinybird/tb/client.py +104 -22
- tinybird/tb/modules/agent/tools/execute_query.py +1 -1
- tinybird/tb/modules/agent/tools/request_endpoint.py +1 -1
- tinybird/tb/modules/branch.py +150 -0
- tinybird/tb/modules/build.py +51 -10
- tinybird/tb/modules/build_common.py +4 -2
- tinybird/tb/modules/cli.py +32 -10
- tinybird/tb/modules/common.py +161 -134
- tinybird/tb/modules/connection.py +125 -194
- tinybird/tb/modules/connection_kafka.py +382 -0
- tinybird/tb/modules/copy.py +3 -1
- tinybird/tb/modules/create.py +11 -0
- tinybird/tb/modules/datafile/build.py +1 -1
- tinybird/tb/modules/datafile/format_pipe.py +44 -5
- tinybird/tb/modules/datafile/playground.py +1 -1
- tinybird/tb/modules/datasource.py +475 -324
- tinybird/tb/modules/deployment.py +2 -0
- tinybird/tb/modules/deployment_common.py +81 -43
- tinybird/tb/modules/deprecations.py +4 -4
- tinybird/tb/modules/dev_server.py +33 -12
- tinybird/tb/modules/info.py +50 -7
- tinybird/tb/modules/job_common.py +15 -0
- tinybird/tb/modules/local.py +91 -21
- tinybird/tb/modules/local_common.py +320 -13
- tinybird/tb/modules/local_logs.py +209 -0
- tinybird/tb/modules/login.py +3 -2
- tinybird/tb/modules/login_common.py +252 -9
- tinybird/tb/modules/open.py +10 -5
- tinybird/tb/modules/project.py +14 -5
- tinybird/tb/modules/shell.py +14 -6
- tinybird/tb/modules/sink.py +3 -1
- tinybird/tb/modules/telemetry.py +7 -3
- tinybird/tb_cli_modules/telemetry.py +1 -1
- {tinybird-0.0.1.dev306.dist-info → tinybird-1.0.5.dist-info}/METADATA +29 -4
- {tinybird-0.0.1.dev306.dist-info → tinybird-1.0.5.dist-info}/RECORD +45 -41
- {tinybird-0.0.1.dev306.dist-info → tinybird-1.0.5.dist-info}/WHEEL +1 -1
- {tinybird-0.0.1.dev306.dist-info → tinybird-1.0.5.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev306.dist-info → tinybird-1.0.5.dist-info}/top_level.txt +0 -0
tinybird/datafile/common.py
CHANGED
|
@@ -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(
|
|
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
|
"""
|
tinybird/feedback_manager.py
CHANGED
|
@@ -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}?"
|
tinybird/service_datasources.py
CHANGED
|
@@ -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": "
|
|
521
|
+
"dateColumn": "second_slot",
|
|
520
522
|
"engine": {
|
|
521
523
|
"engine": "AggregatingMergeTree",
|
|
522
|
-
"sorting_key": "organization_id,
|
|
523
|
-
"partition_key": "toStartOfDay(
|
|
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": "
|
|
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": "
|
|
1092
|
+
"dateColumn": "second_slot",
|
|
1046
1093
|
"engine": {
|
|
1047
1094
|
"engine": "AggregatingMergeTree",
|
|
1048
|
-
"sorting_key": "organization_id,
|
|
1049
|
-
"partition_key": "toStartOfDay(
|
|
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": "
|
|
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=
|
|
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
|
|
tinybird/sql_template_fmt.py
CHANGED
|
@@ -203,13 +203,15 @@ class TinybirdDialect(ClickHouse):
|
|
|
203
203
|
Rule(
|
|
204
204
|
name="jinja_if_block_end",
|
|
205
205
|
priority=203,
|
|
206
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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__ = '
|
|
8
|
-
__revision__ = '
|
|
7
|
+
__version__ = '1.0.5'
|
|
8
|
+
__revision__ = '60ae688'
|
tinybird/tb/cli.py
CHANGED
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
|
-
|
|
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":
|
|
324
|
+
"connected_datasources": connected_datasources,
|
|
314
325
|
**c["settings"],
|
|
315
326
|
}
|
|
316
|
-
|
|
317
|
-
|
|
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("/
|
|
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"/
|
|
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=
|
|
1000
|
-
resp = self._req(
|
|
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())
|