tinybird-cli 5.7.0__tar.gz → 5.8.0__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.
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/PKG-INFO +9 -1
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/__cli__.py +2 -2
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/client.py +74 -4
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/datafile.py +199 -26
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/feedback_manager.py +29 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/sql_template.py +12 -3
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/sql_toolset.py +1 -1
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/tb_cli.py +2 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/tb_cli_modules/cli.py +0 -84
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/tb_cli_modules/datasource.py +62 -0
- tinybird-cli-5.8.0/tinybird/tb_cli_modules/fmt.py +90 -0
- tinybird-cli-5.8.0/tinybird/tb_cli_modules/tag.py +116 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird_cli.egg-info/PKG-INFO +9 -1
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird_cli.egg-info/SOURCES.txt +2 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/setup.cfg +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/ch_utils/constants.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/ch_utils/engine.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/check_pypi.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/config.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/connectors.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/context.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/datatypes.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/git_settings.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/sql.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/sql_template_fmt.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/syncasync.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/tb_cli_modules/auth.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/tb_cli_modules/branch.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/tb_cli_modules/cicd.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/tb_cli_modules/common.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/tb_cli_modules/config.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/tb_cli_modules/connection.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/tb_cli_modules/exceptions.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/tb_cli_modules/job.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/tb_cli_modules/pipe.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/tb_cli_modules/regions.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/tb_cli_modules/telemetry.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/tb_cli_modules/test.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/tb_cli_modules/token.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/tb_cli_modules/workspace.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/tb_cli_modules/workspace_members.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird/tornado_template.py +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird_cli.egg-info/dependency_links.txt +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird_cli.egg-info/entry_points.txt +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/tinybird_cli.egg-info/requires.txt +0 -0
- {tinybird-cli-5.7.0 → tinybird-cli-5.8.0}/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: 5.
|
|
3
|
+
Version: 5.8.0
|
|
4
4
|
Summary: Tinybird Command Line Tool
|
|
5
5
|
Home-page: https://www.tinybird.co/docs/cli/introduction.html
|
|
6
6
|
Author: Tinybird
|
|
@@ -18,6 +18,14 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
|
|
|
18
18
|
Changelog
|
|
19
19
|
----------
|
|
20
20
|
|
|
21
|
+
5.8.0
|
|
22
|
+
**********
|
|
23
|
+
|
|
24
|
+
- `Added` `tb datasource scheduling` commands to manage the scheduling of a Data Source
|
|
25
|
+
- `Added` `tb tag` commands to manage tags from the CLI.
|
|
26
|
+
- `Added` support to `TAGS` in `tb fmt`.
|
|
27
|
+
- `Added` support to `TAGS` in `tb pull` and `tb push`. Allows tagging resources for filtering in the UI.
|
|
28
|
+
|
|
21
29
|
5.7.0
|
|
22
30
|
**********
|
|
23
31
|
|
|
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
|
|
|
4
4
|
__url__ = 'https://www.tinybird.co/docs/cli/introduction.html'
|
|
5
5
|
__author__ = 'Tinybird'
|
|
6
6
|
__author_email__ = 'support@tinybird.co'
|
|
7
|
-
__version__ = '5.
|
|
8
|
-
__revision__ = '
|
|
7
|
+
__version__ = '5.8.0'
|
|
8
|
+
__revision__ = 'cee7088'
|
|
@@ -103,12 +103,20 @@ class TinyB(object):
|
|
|
103
103
|
self.semver = semver
|
|
104
104
|
|
|
105
105
|
async def _req(
|
|
106
|
-
self,
|
|
106
|
+
self,
|
|
107
|
+
endpoint: str,
|
|
108
|
+
data=None,
|
|
109
|
+
files=None,
|
|
110
|
+
method: str = "GET",
|
|
111
|
+
retries: int = LIMIT_RETRIES,
|
|
112
|
+
use_token: Optional[str] = None,
|
|
113
|
+
**kwargs,
|
|
107
114
|
): # noqa: C901
|
|
108
115
|
url = f"{self.host.strip('/')}/{endpoint.strip('/')}"
|
|
109
116
|
|
|
110
|
-
if self.token
|
|
111
|
-
|
|
117
|
+
token_to_use = use_token if use_token else self.token
|
|
118
|
+
if token_to_use:
|
|
119
|
+
url += ("&" if "?" in endpoint else "?") + "token=" + token_to_use
|
|
112
120
|
if self.version:
|
|
113
121
|
url += ("&" if "?" in url else "?") + "cli_version=" + quote(self.version)
|
|
114
122
|
if self.semver:
|
|
@@ -162,7 +170,7 @@ class TinyB(object):
|
|
|
162
170
|
|
|
163
171
|
if response.status_code == 403:
|
|
164
172
|
error = parse_error_response(response)
|
|
165
|
-
if not
|
|
173
|
+
if not token_to_use:
|
|
166
174
|
raise AuthNoTokenException(f"Forbidden: {error}")
|
|
167
175
|
raise AuthException(f"Forbidden: {error}")
|
|
168
176
|
if response.status_code == 204 or response.status_code == 205:
|
|
@@ -466,6 +474,24 @@ class TinyB(object):
|
|
|
466
474
|
async def datasource_sync(self, datasource_id: str):
|
|
467
475
|
return await self._req(f"/v0/datasources/{datasource_id}/scheduling/runs", method="POST", data="")
|
|
468
476
|
|
|
477
|
+
async def datasource_scheduling_state(self, datasource_id: str):
|
|
478
|
+
response = await self._req(f"/v0/datasources/{datasource_id}/scheduling/state", method="GET")
|
|
479
|
+
return response["state"]
|
|
480
|
+
|
|
481
|
+
async def datasource_scheduling_pause(self, datasource_id: str):
|
|
482
|
+
return await self._req(
|
|
483
|
+
f"/v0/datasources/{datasource_id}/scheduling/state",
|
|
484
|
+
method="PUT",
|
|
485
|
+
data='{"state": "paused"}',
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
async def datasource_scheduling_resume(self, datasource_id: str):
|
|
489
|
+
return await self._req(
|
|
490
|
+
f"/v0/datasources/{datasource_id}/scheduling/state",
|
|
491
|
+
method="PUT",
|
|
492
|
+
data='{"state": "running"}',
|
|
493
|
+
)
|
|
494
|
+
|
|
469
495
|
async def datasource_exchange(self, datasource_a: str, datasource_b: str):
|
|
470
496
|
payload = {"datasource_a": datasource_a, "datasource_b": datasource_b}
|
|
471
497
|
return await self._req("/v0/datasources/exchange", method="POST", data=payload)
|
|
@@ -1210,3 +1236,47 @@ class TinyB(object):
|
|
|
1210
1236
|
|
|
1211
1237
|
async def check_auth_login(self) -> Dict[str, Any]:
|
|
1212
1238
|
return await self._req("/v0/auth")
|
|
1239
|
+
|
|
1240
|
+
async def get_all_tags(self, token: Optional[str] = None) -> Dict[str, Any]:
|
|
1241
|
+
return await self._req("/v0/tags", use_token=token)
|
|
1242
|
+
|
|
1243
|
+
async def create_tag_with_resource(
|
|
1244
|
+
self, name: str, resource_id: str, resource_name: str, resource_type: str, token: Optional[str] = None
|
|
1245
|
+
):
|
|
1246
|
+
return await self._req(
|
|
1247
|
+
"/v0/tags",
|
|
1248
|
+
method="POST",
|
|
1249
|
+
headers={"Content-Type": "application/json"},
|
|
1250
|
+
data=json.dumps(
|
|
1251
|
+
{
|
|
1252
|
+
"name": name,
|
|
1253
|
+
"resources": [{"id": resource_id, "name": resource_name, "type": resource_type}],
|
|
1254
|
+
}
|
|
1255
|
+
),
|
|
1256
|
+
use_token=token,
|
|
1257
|
+
)
|
|
1258
|
+
|
|
1259
|
+
async def create_tag(self, name: str, token: Optional[str] = None):
|
|
1260
|
+
return await self._req(
|
|
1261
|
+
"/v0/tags",
|
|
1262
|
+
method="POST",
|
|
1263
|
+
headers={"Content-Type": "application/json"},
|
|
1264
|
+
data=json.dumps({"name": name}),
|
|
1265
|
+
use_token=token,
|
|
1266
|
+
)
|
|
1267
|
+
|
|
1268
|
+
async def update_tag(self, name: str, resources: List[Dict[str, Any]], token: Optional[str] = None):
|
|
1269
|
+
await self._req(
|
|
1270
|
+
f"/v0/tags/{name}",
|
|
1271
|
+
method="PUT",
|
|
1272
|
+
headers={"Content-Type": "application/json"},
|
|
1273
|
+
data=json.dumps(
|
|
1274
|
+
{
|
|
1275
|
+
"resources": resources,
|
|
1276
|
+
}
|
|
1277
|
+
),
|
|
1278
|
+
use_token=token,
|
|
1279
|
+
)
|
|
1280
|
+
|
|
1281
|
+
async def delete_tag(self, name: str, token: Optional[str] = None):
|
|
1282
|
+
await self._req(f"/v0/tags/{name}", method="DELETE", use_token=token)
|
|
@@ -760,6 +760,7 @@ class Datafile:
|
|
|
760
760
|
self.includes: Dict[str, Any] = {}
|
|
761
761
|
self.shared_with: List[str] = []
|
|
762
762
|
self.warnings: List[str] = []
|
|
763
|
+
self.filtering_tags: Optional[List[str]] = None
|
|
763
764
|
|
|
764
765
|
def validate(self) -> None:
|
|
765
766
|
for x in self.nodes:
|
|
@@ -903,6 +904,33 @@ def eval_var(s: str, skip: bool = False) -> str:
|
|
|
903
904
|
return Template(s).safe_substitute(os.environ)
|
|
904
905
|
|
|
905
906
|
|
|
907
|
+
def parse_tags(tags: str) -> Tuple[str, List[str]]:
|
|
908
|
+
"""
|
|
909
|
+
Parses a string of tags into:
|
|
910
|
+
- kv_tags: a string of key-value tags: the previous tags we have for operational purposes. It
|
|
911
|
+
has the format key=value&key2=value2 (with_staging=true&with_last_date=true)
|
|
912
|
+
- filtering_tags: a list of tags that are used for filtering.
|
|
913
|
+
|
|
914
|
+
Example: "with_staging=true&with_last_date=true,billing,stats" ->
|
|
915
|
+
kv_tags = {"with_staging": "true", "with_last_date": "true"}
|
|
916
|
+
filtering_tags = ["billing", "stats"]
|
|
917
|
+
"""
|
|
918
|
+
kv_tags = []
|
|
919
|
+
filtering_tags = []
|
|
920
|
+
|
|
921
|
+
entries = tags.split(",")
|
|
922
|
+
for entry in entries:
|
|
923
|
+
trimmed_entry = entry.strip()
|
|
924
|
+
if "=" in trimmed_entry:
|
|
925
|
+
kv_tags.append(trimmed_entry)
|
|
926
|
+
else:
|
|
927
|
+
filtering_tags.append(trimmed_entry)
|
|
928
|
+
|
|
929
|
+
all_kv_tags = "&".join(kv_tags)
|
|
930
|
+
|
|
931
|
+
return all_kv_tags, filtering_tags
|
|
932
|
+
|
|
933
|
+
|
|
906
934
|
def parse(
|
|
907
935
|
s: str,
|
|
908
936
|
default_node: Optional[str] = None,
|
|
@@ -1100,6 +1128,21 @@ def parse(
|
|
|
1100
1128
|
|
|
1101
1129
|
return _f
|
|
1102
1130
|
|
|
1131
|
+
def tags(*args: str, **kwargs: Any) -> None:
|
|
1132
|
+
raw_tags = _unquote((" ".join(args)).strip())
|
|
1133
|
+
operational_tags, filtering_tags = parse_tags(raw_tags)
|
|
1134
|
+
|
|
1135
|
+
# Pipe nodes or Data Sources
|
|
1136
|
+
if parser_state.current_node and operational_tags:
|
|
1137
|
+
operational_tags_args = (operational_tags,)
|
|
1138
|
+
assign_node_var("tags")(*operational_tags_args, **kwargs)
|
|
1139
|
+
|
|
1140
|
+
if filtering_tags:
|
|
1141
|
+
if doc.filtering_tags is None:
|
|
1142
|
+
doc.filtering_tags = filtering_tags
|
|
1143
|
+
else:
|
|
1144
|
+
doc.filtering_tags += filtering_tags
|
|
1145
|
+
|
|
1103
1146
|
cmds = {
|
|
1104
1147
|
"from": assign("from"),
|
|
1105
1148
|
"source": sources,
|
|
@@ -1120,7 +1163,7 @@ def parse(
|
|
|
1120
1163
|
"description": description,
|
|
1121
1164
|
"type": assign_node_var("type"),
|
|
1122
1165
|
"datasource": assign_node_var("datasource"),
|
|
1123
|
-
"tags":
|
|
1166
|
+
"tags": tags,
|
|
1124
1167
|
"target_datasource": assign_node_var("target_datasource"),
|
|
1125
1168
|
"copy_schedule": assign_node_var(CopyParameters.COPY_SCHEDULE),
|
|
1126
1169
|
"copy_mode": assign_node_var("mode"),
|
|
@@ -1530,6 +1573,7 @@ async def process_file(
|
|
|
1530
1573
|
"deps": deps,
|
|
1531
1574
|
"tokens": doc.tokens,
|
|
1532
1575
|
"shared_with": doc.shared_with,
|
|
1576
|
+
"filtering_tags": doc.filtering_tags,
|
|
1533
1577
|
}
|
|
1534
1578
|
)
|
|
1535
1579
|
|
|
@@ -1638,6 +1682,7 @@ async def process_file(
|
|
|
1638
1682
|
"tokens": doc.tokens,
|
|
1639
1683
|
"description": description,
|
|
1640
1684
|
"warnings": doc.warnings,
|
|
1685
|
+
"filtering_tags": doc.filtering_tags,
|
|
1641
1686
|
}
|
|
1642
1687
|
]
|
|
1643
1688
|
elif DataFileExtensions.TOKEN in filename:
|
|
@@ -2646,6 +2691,121 @@ def show_materialized_view_warnings(warnings):
|
|
|
2646
2691
|
)
|
|
2647
2692
|
|
|
2648
2693
|
|
|
2694
|
+
async def update_tags(resource_id: str, resource_name: str, resource_type: str, tags: List[str], tb_client: TinyB):
|
|
2695
|
+
def get_tags_for_resource(all_tags: dict, resource_id: str, resource_name: str) -> List[str]:
|
|
2696
|
+
tag_names = []
|
|
2697
|
+
|
|
2698
|
+
for tag in all_tags.get("tags", []):
|
|
2699
|
+
for resource in tag.get("resources", []):
|
|
2700
|
+
if resource.get("id") == resource_id or resource.get("name") == resource_name:
|
|
2701
|
+
tag_names.append(tag.get("name"))
|
|
2702
|
+
break # No need to check other resources in this tag
|
|
2703
|
+
|
|
2704
|
+
return tag_names
|
|
2705
|
+
|
|
2706
|
+
def get_tag(all_tags: dict, tag_name: str) -> Optional[dict]:
|
|
2707
|
+
for tag in all_tags.get("tags", []):
|
|
2708
|
+
if tag.get("name") == tag_name:
|
|
2709
|
+
return tag
|
|
2710
|
+
return None
|
|
2711
|
+
|
|
2712
|
+
def compare_tags(current_tags: List[str], new_tags: List[str]) -> Tuple[List[str], List[str]]:
|
|
2713
|
+
tags_to_add = list(set(new_tags) - set(current_tags))
|
|
2714
|
+
tags_to_remove = list(set(current_tags) - set(new_tags))
|
|
2715
|
+
return tags_to_add, tags_to_remove
|
|
2716
|
+
|
|
2717
|
+
token_from_main = await get_token_from_main_branch(tb_client)
|
|
2718
|
+
|
|
2719
|
+
try:
|
|
2720
|
+
all_tags = await tb_client.get_all_tags(token=token_from_main)
|
|
2721
|
+
except Exception as e:
|
|
2722
|
+
raise Exception(FeedbackManager.error_getting_tags(error=str(e)))
|
|
2723
|
+
|
|
2724
|
+
# Get all tags of that resource
|
|
2725
|
+
current_tags = get_tags_for_resource(all_tags, resource_id, resource_name)
|
|
2726
|
+
|
|
2727
|
+
# Get the tags to add and remove
|
|
2728
|
+
tags_to_add, tags_to_remove = compare_tags(current_tags, tags)
|
|
2729
|
+
|
|
2730
|
+
# Tags to add
|
|
2731
|
+
for tag_name in tags_to_add:
|
|
2732
|
+
tag = get_tag(all_tags, tag_name)
|
|
2733
|
+
|
|
2734
|
+
if not tag:
|
|
2735
|
+
# Create new tag
|
|
2736
|
+
try:
|
|
2737
|
+
await tb_client.create_tag_with_resource(
|
|
2738
|
+
name=tag_name,
|
|
2739
|
+
resource_id=resource_id,
|
|
2740
|
+
resource_name=resource_name,
|
|
2741
|
+
resource_type=resource_type,
|
|
2742
|
+
token=token_from_main,
|
|
2743
|
+
)
|
|
2744
|
+
except Exception as e:
|
|
2745
|
+
raise Exception(FeedbackManager.error_creating_tag(error=str(e)))
|
|
2746
|
+
else:
|
|
2747
|
+
# Update tag with new resource
|
|
2748
|
+
resources = tag.get("resources", [])
|
|
2749
|
+
resources.append({"id": resource_id, "name": resource_name, "type": resource_type})
|
|
2750
|
+
try:
|
|
2751
|
+
await tb_client.update_tag(tag.get("name", tag_name), resources, token=token_from_main)
|
|
2752
|
+
except Exception as e:
|
|
2753
|
+
raise Exception(FeedbackManager.error_updating_tag(error=str(e)))
|
|
2754
|
+
|
|
2755
|
+
# Tags to delete
|
|
2756
|
+
for tag_name in tags_to_remove:
|
|
2757
|
+
tag = get_tag(all_tags, tag_name)
|
|
2758
|
+
|
|
2759
|
+
if tag:
|
|
2760
|
+
resources = tag.get("resources", [])
|
|
2761
|
+
resources = [resource for resource in resources if resource.get("name") != resource_name]
|
|
2762
|
+
try:
|
|
2763
|
+
await tb_client.update_tag(tag.get("name", tag_name), resources, token=token_from_main)
|
|
2764
|
+
except Exception as e:
|
|
2765
|
+
raise Exception(FeedbackManager.error_updating_tag(error=str(e)))
|
|
2766
|
+
|
|
2767
|
+
|
|
2768
|
+
async def update_tags_in_resource(rs: Dict[str, Any], resource_type: str, client: TinyB):
|
|
2769
|
+
filtering_tags = rs.get("filtering_tags", [])
|
|
2770
|
+
|
|
2771
|
+
if not filtering_tags:
|
|
2772
|
+
return
|
|
2773
|
+
|
|
2774
|
+
resource_id = ""
|
|
2775
|
+
resource_name = ""
|
|
2776
|
+
|
|
2777
|
+
if resource_type == "datasource":
|
|
2778
|
+
ds_name = rs["params"]["name"]
|
|
2779
|
+
try:
|
|
2780
|
+
persisted_ds = await client.get_datasource(ds_name)
|
|
2781
|
+
resource_id = persisted_ds.get("id", "")
|
|
2782
|
+
resource_name = persisted_ds.get("name", "")
|
|
2783
|
+
except DoesNotExistException:
|
|
2784
|
+
click.echo(
|
|
2785
|
+
FeedbackManager.error_tag_generic("Could not get the latest Data Source info for updating its tags.")
|
|
2786
|
+
)
|
|
2787
|
+
elif resource_type == "pipe":
|
|
2788
|
+
pipe_name = rs["name"]
|
|
2789
|
+
try:
|
|
2790
|
+
persisted_pipe = await client.pipe(pipe_name)
|
|
2791
|
+
resource_id = persisted_pipe.get("id", "")
|
|
2792
|
+
resource_name = persisted_pipe.get("name", "")
|
|
2793
|
+
except DoesNotExistException:
|
|
2794
|
+
click.echo(FeedbackManager.error_tag_generic("Could not get the latest Pipe info for updating its tags."))
|
|
2795
|
+
|
|
2796
|
+
if resource_id and resource_name:
|
|
2797
|
+
try:
|
|
2798
|
+
await update_tags(
|
|
2799
|
+
resource_id=resource_id,
|
|
2800
|
+
resource_name=resource_name,
|
|
2801
|
+
resource_type=resource_type,
|
|
2802
|
+
tags=filtering_tags,
|
|
2803
|
+
tb_client=client,
|
|
2804
|
+
)
|
|
2805
|
+
except Exception as e:
|
|
2806
|
+
click.echo(FeedbackManager.error_tag_generic(error=str(e)))
|
|
2807
|
+
|
|
2808
|
+
|
|
2649
2809
|
async def get_token_from_main_branch(branch_tb_client: TinyB) -> Optional[str]:
|
|
2650
2810
|
token_from_main_branch = None
|
|
2651
2811
|
current_workspace = await branch_tb_client.workspace_info()
|
|
@@ -3289,32 +3449,33 @@ async def new_ds(
|
|
|
3289
3449
|
|
|
3290
3450
|
if alter_response and make_changes:
|
|
3291
3451
|
# alter operation finished
|
|
3292
|
-
|
|
3293
|
-
# removed replacing by default. When a datasource is removed data is
|
|
3294
|
-
# removed and all the references needs to be updated
|
|
3295
|
-
if (
|
|
3296
|
-
os.getenv("TB_I_KNOW_WHAT_I_AM_DOING")
|
|
3297
|
-
and click.prompt(FeedbackManager.info_ask_for_datasource_confirmation()) == ds_name
|
|
3298
|
-
): # TODO move to CLI
|
|
3299
|
-
try:
|
|
3300
|
-
await client.datasource_delete(ds_name)
|
|
3301
|
-
click.echo(FeedbackManager.success_delete_datasource(datasource=ds_name))
|
|
3302
|
-
except Exception:
|
|
3303
|
-
raise click.ClickException(FeedbackManager.error_removing_datasource(datasource=ds_name))
|
|
3304
|
-
return
|
|
3452
|
+
pass
|
|
3305
3453
|
else:
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
FeedbackManager.
|
|
3315
|
-
|
|
3454
|
+
# removed replacing by default. When a datasource is removed data is
|
|
3455
|
+
# removed and all the references needs to be updated
|
|
3456
|
+
if (
|
|
3457
|
+
os.getenv("TB_I_KNOW_WHAT_I_AM_DOING")
|
|
3458
|
+
and click.prompt(FeedbackManager.info_ask_for_datasource_confirmation()) == ds_name
|
|
3459
|
+
): # TODO move to CLI
|
|
3460
|
+
try:
|
|
3461
|
+
await client.datasource_delete(ds_name)
|
|
3462
|
+
click.echo(FeedbackManager.success_delete_datasource(datasource=ds_name))
|
|
3463
|
+
except Exception:
|
|
3464
|
+
raise click.ClickException(FeedbackManager.error_removing_datasource(datasource=ds_name))
|
|
3465
|
+
return
|
|
3316
3466
|
else:
|
|
3317
|
-
|
|
3467
|
+
if alter_error_message:
|
|
3468
|
+
raise click.ClickException(
|
|
3469
|
+
FeedbackManager.error_datasource_already_exists_and_alter_failed(
|
|
3470
|
+
datasource=ds_name, alter_error_message=alter_error_message
|
|
3471
|
+
)
|
|
3472
|
+
)
|
|
3473
|
+
if promote_error_message:
|
|
3474
|
+
raise click.ClickException(
|
|
3475
|
+
FeedbackManager.error_promoting_datasource(datasource=ds_name, error=promote_error_message)
|
|
3476
|
+
)
|
|
3477
|
+
else:
|
|
3478
|
+
click.echo(FeedbackManager.warning_datasource_already_exists(datasource=ds_name))
|
|
3318
3479
|
|
|
3319
3480
|
|
|
3320
3481
|
async def new_token(token: Dict[str, Any], client: TinyB, force: bool = False):
|
|
@@ -3400,6 +3561,7 @@ async def exec_file(
|
|
|
3400
3561
|
fork_downstream=fork_downstream,
|
|
3401
3562
|
fork=fork,
|
|
3402
3563
|
)
|
|
3564
|
+
await update_tags_in_resource(r, "pipe", tb_client)
|
|
3403
3565
|
elif r["resource"] == "datasources":
|
|
3404
3566
|
await new_ds(
|
|
3405
3567
|
r,
|
|
@@ -3412,6 +3574,7 @@ async def exec_file(
|
|
|
3412
3574
|
fork=fork,
|
|
3413
3575
|
git_release=git_release,
|
|
3414
3576
|
)
|
|
3577
|
+
await update_tags_in_resource(r, "datasource", tb_client)
|
|
3415
3578
|
elif r["resource"] == "tokens":
|
|
3416
3579
|
await new_token(r, tb_client, force)
|
|
3417
3580
|
else:
|
|
@@ -4659,19 +4822,20 @@ async def format_datasource(
|
|
|
4659
4822
|
if for_deploy_diff:
|
|
4660
4823
|
format_description(file_parts, doc)
|
|
4661
4824
|
format_tokens(file_parts, doc)
|
|
4825
|
+
format_tags(file_parts, doc)
|
|
4662
4826
|
format_schema(file_parts, doc.nodes[0])
|
|
4663
4827
|
format_indices(file_parts, doc.nodes[0])
|
|
4664
4828
|
await format_engine(file_parts, doc.nodes[0], only_ttl=True if not for_deploy_diff else False, client=client)
|
|
4665
4829
|
if for_deploy_diff:
|
|
4666
4830
|
format_import_settings(file_parts, doc.nodes[0])
|
|
4667
4831
|
format_shared_with(file_parts, doc)
|
|
4668
|
-
|
|
4669
4832
|
else:
|
|
4670
4833
|
format_sources(file_parts, doc)
|
|
4671
4834
|
format_maintainer(file_parts, doc)
|
|
4672
4835
|
format_version(file_parts, doc)
|
|
4673
4836
|
format_description(file_parts, doc)
|
|
4674
4837
|
format_tokens(file_parts, doc)
|
|
4838
|
+
format_tags(file_parts, doc)
|
|
4675
4839
|
format_schema(file_parts, doc.nodes[0])
|
|
4676
4840
|
format_indices(file_parts, doc.nodes[0])
|
|
4677
4841
|
await format_engine(file_parts, doc.nodes[0])
|
|
@@ -4747,6 +4911,14 @@ def format_shared_with(file_parts: List[str], doc: Datafile) -> List[str]:
|
|
|
4747
4911
|
return file_parts
|
|
4748
4912
|
|
|
4749
4913
|
|
|
4914
|
+
def format_tags(file_parts: List[str], doc: Datafile) -> List[str]:
|
|
4915
|
+
if doc.filtering_tags:
|
|
4916
|
+
file_parts.append(f'TAGS {", ".join(doc.filtering_tags)}')
|
|
4917
|
+
file_parts.append(DATAFILE_NEW_LINE)
|
|
4918
|
+
file_parts.append(DATAFILE_NEW_LINE)
|
|
4919
|
+
return file_parts
|
|
4920
|
+
|
|
4921
|
+
|
|
4750
4922
|
async def format_engine(
|
|
4751
4923
|
file_parts: List[str], node: Dict[str, Any], only_ttl: bool = False, client: Optional[TinyB] = None
|
|
4752
4924
|
) -> List[str]:
|
|
@@ -4883,6 +5055,7 @@ async def format_pipe(
|
|
|
4883
5055
|
format_version(file_parts, doc)
|
|
4884
5056
|
format_description(file_parts, doc)
|
|
4885
5057
|
format_tokens(file_parts, doc)
|
|
5058
|
+
format_tags(file_parts, doc)
|
|
4886
5059
|
if doc.includes and not unroll_includes:
|
|
4887
5060
|
for k in doc.includes:
|
|
4888
5061
|
# We filter only the include files as we currently have 2 items for each include
|
|
@@ -157,6 +157,15 @@ class FeedbackManager:
|
|
|
157
157
|
error_creating_copy_job = error_message("Failed creating copy job: {error}")
|
|
158
158
|
error_pausing_copy_pipe = error_message("Failed pausing copy pipe: {error}")
|
|
159
159
|
error_resuming_copy_pipe = error_message("Failed resuming copy pipe: {error}")
|
|
160
|
+
error_pausing_datasource_scheduling = error_message(
|
|
161
|
+
"Failed pausing scheduling for Data Source '{datasource}': {error}"
|
|
162
|
+
)
|
|
163
|
+
error_resuming_datasource_scheduling = error_message(
|
|
164
|
+
"Failed resuming scheduling for Data Source '{datasource}': {error}"
|
|
165
|
+
)
|
|
166
|
+
error_datasource_scheduling_state = error_message(
|
|
167
|
+
"Failed requesting scheduling state for Data Source '{datasource}': {error}"
|
|
168
|
+
)
|
|
160
169
|
error_creating_pipe = error_message("Failed creating pipe {error}")
|
|
161
170
|
error_creating_sink_job = error_message("Failed creating sink job: {error}")
|
|
162
171
|
error_running_on_demand_sink_job = error_message("Failed running on-demand sink job: {error}")
|
|
@@ -392,6 +401,11 @@ class FeedbackManager:
|
|
|
392
401
|
"Several materialized nodes in pipe {pipe}. Use --node param."
|
|
393
402
|
)
|
|
394
403
|
error_unlinking_pipe_not_linked = error_message("** {pipe} is not linked (MV, Copy, or Sink).")
|
|
404
|
+
error_getting_tags = error_message("Error getting tags: {error}")
|
|
405
|
+
error_creating_tag = error_message("Error creating new tag: {error}")
|
|
406
|
+
error_updating_tag = error_message("Error updating tag: {error}")
|
|
407
|
+
error_tag_generic = error_message("There was an issue updating tags. {error}")
|
|
408
|
+
error_tag_not_found = error_message("Tag {tag_name} not found.")
|
|
395
409
|
|
|
396
410
|
info_incl_relative_path = info_message("** Relative path {path} does not exist, skipping.")
|
|
397
411
|
info_ignoring_incl_file = info_message(
|
|
@@ -606,6 +620,12 @@ Ready? """
|
|
|
606
620
|
warning_pipe_restricted_params = warning_message(
|
|
607
621
|
"** The parameter names {words} and '{last_word}' are reserved words. Please, choose another name or the pipe will not work as expected."
|
|
608
622
|
)
|
|
623
|
+
warning_tag_remove_no_resources = prompt_message(
|
|
624
|
+
"Tag {tag_name} is not used by any resource. Do you want to remove it?"
|
|
625
|
+
)
|
|
626
|
+
warning_tag_remove = prompt_message(
|
|
627
|
+
"Tag {tag_name} is used by {resources_len} resources. Do you want to remove it?"
|
|
628
|
+
)
|
|
609
629
|
|
|
610
630
|
info_fixtures_branch = info_message("** Data Fixtures are only pushed to Branches")
|
|
611
631
|
info_materialize_push_datasource_exists = warning_message("** Data Source {name} already exists")
|
|
@@ -670,6 +690,9 @@ Ready? """
|
|
|
670
690
|
info_datasource_title = print_message("** {title}", bcolors.BOLD)
|
|
671
691
|
info_datasource_row = info_message("{row}")
|
|
672
692
|
info_datasource_delete_rows_job_url = info_message("** Delete rows job url {url}")
|
|
693
|
+
info_datasource_scheduling_state = info_message("** Scheduling state for Data Source '{datasource}': {state}")
|
|
694
|
+
info_datasource_scheduling_pause = info_message("** Pausing scheduling...")
|
|
695
|
+
info_datasource_scheduling_resume = info_message("** Resuming scheduling...")
|
|
673
696
|
info_pipes = info_message("** Pipes:")
|
|
674
697
|
info_pipe_name = info_message("** - {pipe}")
|
|
675
698
|
info_using_node = print_message("** Using last node {node} as endpoint")
|
|
@@ -797,6 +820,8 @@ Ready? """
|
|
|
797
820
|
info_release_rollback = info_message(
|
|
798
821
|
"** The following resources IDs are present in the {semver} Release and will be restored:"
|
|
799
822
|
)
|
|
823
|
+
info_tag_list = info_message("** Tags:")
|
|
824
|
+
info_tag_resources = info_message("** Resources tagged by {tag_name}:")
|
|
800
825
|
warning_no_release = warning_message(
|
|
801
826
|
"** Warning: Workspace does not have Releases, run `tb init --git` to activate them."
|
|
802
827
|
)
|
|
@@ -920,6 +945,8 @@ Ready? """
|
|
|
920
945
|
success_datasource_unshared = success_message(
|
|
921
946
|
"** The Data Source {datasource} has been correctly unshared from {workspace}"
|
|
922
947
|
)
|
|
948
|
+
success_datasource_scheduling_resumed = success_message("""** Scheduling resumed for Data Source '{datasource}'""")
|
|
949
|
+
success_datasource_scheduling_paused = success_message("""** Scheduling paused for Data Source '{datasource}'""")
|
|
923
950
|
success_connection_created = success_message("** Connection {id} created successfully!")
|
|
924
951
|
|
|
925
952
|
# TODO: Update the message when the .env feature is implemented
|
|
@@ -981,5 +1008,7 @@ Ready? """
|
|
|
981
1008
|
success_delete_token = success_message("** Token '{token}' removed successfully")
|
|
982
1009
|
success_refresh_token = success_message("** Token '{token}' refreshed successfully")
|
|
983
1010
|
success_copy_token = success_message("** Token '{token}' copied to clipboard")
|
|
1011
|
+
success_tag_created = success_message("** Tag '{tag_name}' created!")
|
|
1012
|
+
success_tag_removed = success_message("** Tag '{tag_name}' removed!")
|
|
984
1013
|
|
|
985
1014
|
debug_running_file = print_message("** Running {file}", bcolors.CGREY)
|
|
@@ -386,14 +386,14 @@ def array_type(types): # noqa: C901
|
|
|
386
386
|
for i, t in enumerate(list_values):
|
|
387
387
|
if _type in testers:
|
|
388
388
|
if testers[_type](str(t)):
|
|
389
|
-
values.append(expression_wrapper(types[_type](t),
|
|
389
|
+
values.append(expression_wrapper(types[_type](t), str(t)))
|
|
390
390
|
else:
|
|
391
391
|
raise SQLTemplateException(
|
|
392
392
|
f"Error validating {x}[{i}]({t}) to type {_type}",
|
|
393
393
|
documentation="/cli/advanced-templates.html",
|
|
394
394
|
)
|
|
395
395
|
else:
|
|
396
|
-
values.append(expression_wrapper(types.get(_type, lambda x: x)(t),
|
|
396
|
+
values.append(expression_wrapper(types.get(_type, lambda x: x)(t), str(t)))
|
|
397
397
|
return Expression(f"[{','.join(map(str, values))}]")
|
|
398
398
|
except AttributeError as e:
|
|
399
399
|
logging.warning(f"AttributeError on Array: {e}")
|
|
@@ -1314,8 +1314,9 @@ def expression_wrapper(x, name, escape_arrays: bool = False):
|
|
|
1314
1314
|
elif isinstance(x, Comment):
|
|
1315
1315
|
return "-- {x} \n"
|
|
1316
1316
|
if x is None:
|
|
1317
|
+
truncated_name = name[:20] + "..." if len(name) > 20 else name
|
|
1317
1318
|
raise SQLTemplateException(
|
|
1318
|
-
f'expression "{
|
|
1319
|
+
f'expression "{truncated_name}" evaluated to null', documentation="/cli/advanced-templates.html"
|
|
1319
1320
|
)
|
|
1320
1321
|
if isinstance(x, list) and escape_arrays:
|
|
1321
1322
|
logging.warning(f"expression_wrapper -> list :{x}:")
|
|
@@ -1992,6 +1993,14 @@ def render_sql_template(
|
|
|
1992
1993
|
Traceback (most recent call last):
|
|
1993
1994
|
...
|
|
1994
1995
|
tinybird.sql_template.SQLTemplateException: Template Syntax Error: expression "test" evaluated to null
|
|
1996
|
+
>>> render_sql_template("SELECT {{testisasuperlongthingandwedontwanttoreturnthefullthing}}", {'token':'testing'})
|
|
1997
|
+
Traceback (most recent call last):
|
|
1998
|
+
...
|
|
1999
|
+
tinybird.sql_template.SQLTemplateException: Template Syntax Error: expression "testisasuperlongthin..." evaluated to null
|
|
2000
|
+
>>> render_sql_template("SELECT {{ Array(embedding, 'Float32') }}", {'token':'testing', 'embedding': '1,2,3,4, null'})
|
|
2001
|
+
Traceback (most recent call last):
|
|
2002
|
+
...
|
|
2003
|
+
tinybird.sql_template.SQLTemplateException: Template Syntax Error: Error validating 1,2,3,4, null[4]( null) to type Float32
|
|
1995
2004
|
>>> render_sql_template('{% if test %}SELECT 1{% else %} select 2 {% end %}')
|
|
1996
2005
|
(' select 2 ', {}, [])
|
|
1997
2006
|
>>> render_sql_template('{% if Int32(test, 1) %}SELECT 1{% else %} select 2 {% end %}')
|
|
@@ -293,7 +293,7 @@ def replace_tables(
|
|
|
293
293
|
and is_invalid_resource(r, database, default_database, _replaced_with, valid_tables)
|
|
294
294
|
):
|
|
295
295
|
logging.info(
|
|
296
|
-
"Resource not found in replace_tables: %s",
|
|
296
|
+
"Resource not found in replace_tables in sql_toolset: %s",
|
|
297
297
|
{
|
|
298
298
|
"r": r,
|
|
299
299
|
"default_database": default_database,
|
|
@@ -10,8 +10,10 @@ import tinybird.tb_cli_modules.cli
|
|
|
10
10
|
import tinybird.tb_cli_modules.common
|
|
11
11
|
import tinybird.tb_cli_modules.connection
|
|
12
12
|
import tinybird.tb_cli_modules.datasource
|
|
13
|
+
import tinybird.tb_cli_modules.fmt
|
|
13
14
|
import tinybird.tb_cli_modules.job
|
|
14
15
|
import tinybird.tb_cli_modules.pipe
|
|
16
|
+
import tinybird.tb_cli_modules.tag
|
|
15
17
|
import tinybird.tb_cli_modules.test
|
|
16
18
|
import tinybird.tb_cli_modules.token
|
|
17
19
|
import tinybird.tb_cli_modules.workspace
|
|
@@ -3,7 +3,6 @@
|
|
|
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
|
-
import difflib
|
|
7
6
|
import json
|
|
8
7
|
import logging
|
|
9
8
|
import os
|
|
@@ -36,21 +35,16 @@ from tinybird.datafile import (
|
|
|
36
35
|
Datafile,
|
|
37
36
|
ParseException,
|
|
38
37
|
build_graph,
|
|
39
|
-
color_diff,
|
|
40
38
|
create_release,
|
|
41
39
|
diff_command,
|
|
42
40
|
folder_pull,
|
|
43
41
|
folder_push,
|
|
44
|
-
format_datasource,
|
|
45
|
-
format_pipe,
|
|
46
42
|
get_project_filenames,
|
|
47
43
|
get_resource_versions,
|
|
48
44
|
has_internal_datafiles,
|
|
49
|
-
is_file_a_datasource,
|
|
50
45
|
parse_datasource,
|
|
51
46
|
parse_pipe,
|
|
52
47
|
parse_token,
|
|
53
|
-
peek,
|
|
54
48
|
wait_job,
|
|
55
49
|
)
|
|
56
50
|
from tinybird.feedback_manager import FeedbackManager
|
|
@@ -753,84 +747,6 @@ async def dependencies(
|
|
|
753
747
|
raise CLIException(FeedbackManager.error_partial_replace_cant_be_executed(datasource=datasource))
|
|
754
748
|
|
|
755
749
|
|
|
756
|
-
@cli.command()
|
|
757
|
-
@click.argument("filenames", type=click.Path(exists=True), nargs=-1, required=True)
|
|
758
|
-
@click.option(
|
|
759
|
-
"--line-length",
|
|
760
|
-
is_flag=False,
|
|
761
|
-
default=100,
|
|
762
|
-
help="A number indicating the maximum characters per line in the node SQL, lines will be splitted based on the SQL syntax and the number of characters passed as a parameter",
|
|
763
|
-
)
|
|
764
|
-
@click.option("--dry-run", is_flag=True, default=False, help="Don't ask to override the local file")
|
|
765
|
-
@click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation to overwrite the local file")
|
|
766
|
-
@click.option(
|
|
767
|
-
"--diff",
|
|
768
|
-
is_flag=True,
|
|
769
|
-
default=False,
|
|
770
|
-
help="Formats local file, prints the diff and exits 1 if different, 0 if equal",
|
|
771
|
-
)
|
|
772
|
-
@click.pass_context
|
|
773
|
-
@coro
|
|
774
|
-
async def fmt(
|
|
775
|
-
ctx: Context, filenames: List[str], line_length: int, dry_run: bool, yes: bool, diff: bool
|
|
776
|
-
) -> Optional[str]:
|
|
777
|
-
"""
|
|
778
|
-
Formats a .datasource, .pipe or .incl file
|
|
779
|
-
|
|
780
|
-
This command removes comments starting with # from the file, use DESCRIPTION instead.
|
|
781
|
-
|
|
782
|
-
The format command tries to parse the datafile so syntax errors might rise.
|
|
783
|
-
|
|
784
|
-
.incl files must contain a NODE definition
|
|
785
|
-
"""
|
|
786
|
-
|
|
787
|
-
result = ""
|
|
788
|
-
failed = []
|
|
789
|
-
for filename in filenames:
|
|
790
|
-
if not diff:
|
|
791
|
-
click.echo(filename)
|
|
792
|
-
extensions = Path(filename).suffixes
|
|
793
|
-
if is_file_a_datasource(filename):
|
|
794
|
-
result = await format_datasource(filename, skip_eval=True)
|
|
795
|
-
elif (".pipe" in extensions) or (".incl" in extensions):
|
|
796
|
-
result = await format_pipe(filename, line_length, skip_eval=True)
|
|
797
|
-
else:
|
|
798
|
-
click.echo("Unsupported file type. Supported files types are: .pipe, .incl and .datasource")
|
|
799
|
-
return None
|
|
800
|
-
|
|
801
|
-
if diff:
|
|
802
|
-
result = result.rstrip("\n")
|
|
803
|
-
lines_fmt = [f"{line}\n" for line in result.split("\n")]
|
|
804
|
-
with open(filename, "r") as file:
|
|
805
|
-
lines_file = file.readlines()
|
|
806
|
-
diff_result = difflib.unified_diff(
|
|
807
|
-
lines_file, lines_fmt, fromfile=f"{Path(filename).name} local", tofile="fmt datafile"
|
|
808
|
-
)
|
|
809
|
-
diff_result = color_diff(diff_result)
|
|
810
|
-
not_empty, diff_lines = peek(diff_result)
|
|
811
|
-
if not_empty:
|
|
812
|
-
sys.stdout.writelines(diff_lines)
|
|
813
|
-
failed.append(filename)
|
|
814
|
-
click.echo("")
|
|
815
|
-
else:
|
|
816
|
-
click.echo(result)
|
|
817
|
-
if dry_run:
|
|
818
|
-
return None
|
|
819
|
-
|
|
820
|
-
if yes or click.confirm(FeedbackManager.prompt_override_local_file(name=filename)):
|
|
821
|
-
with open(f"{filename}", "w") as file:
|
|
822
|
-
file.write(result)
|
|
823
|
-
|
|
824
|
-
click.echo(FeedbackManager.success_generated_local_file(file=filename))
|
|
825
|
-
|
|
826
|
-
if len(failed):
|
|
827
|
-
click.echo(FeedbackManager.error_failed_to_format_files(number=len(failed)))
|
|
828
|
-
for f in failed:
|
|
829
|
-
click.echo(f"tb fmt {f} --yes")
|
|
830
|
-
sys.exit(1)
|
|
831
|
-
return result
|
|
832
|
-
|
|
833
|
-
|
|
834
750
|
@cli.command(
|
|
835
751
|
name="diff",
|
|
836
752
|
short_help="Diffs local datafiles to the corresponding remote files in the workspace. For the case of .datasource files it just diffs VERSION and SCHEMA, since ENGINE, KAFKA or other metadata is considered immutable.",
|
|
@@ -805,3 +805,65 @@ async def datasource_copy_from_main(
|
|
|
805
805
|
if wait:
|
|
806
806
|
base_msg = "Copy from Main Workspace" if sql_from_main else f"Copy from {sql}"
|
|
807
807
|
await wait_job(client, job_id, job_url, f"{base_msg} to {datasource_name}")
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
@datasource.group(name="scheduling")
|
|
811
|
+
@click.pass_context
|
|
812
|
+
def datasource_scheduling(ctx: Context) -> None:
|
|
813
|
+
"""Data Source scheduling commands."""
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
@datasource_scheduling.command(name="state")
|
|
817
|
+
@click.argument("datasource_name")
|
|
818
|
+
@click.pass_context
|
|
819
|
+
@coro
|
|
820
|
+
async def datasource_scheduling_state(ctx: Context, datasource_name: str) -> None:
|
|
821
|
+
"""Get the scheduling state of a Data Source."""
|
|
822
|
+
client: TinyB = ctx.obj["client"]
|
|
823
|
+
try:
|
|
824
|
+
state = await client.datasource_scheduling_state(datasource_name)
|
|
825
|
+
click.echo(FeedbackManager.info_datasource_scheduling_state(datasource=datasource_name, state=state))
|
|
826
|
+
except Exception as e:
|
|
827
|
+
raise CLIDatasourceException(
|
|
828
|
+
FeedbackManager.error_datasource_scheduling_state(datasource=datasource_name, error=e)
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
@datasource_scheduling.command(name="pause")
|
|
833
|
+
@click.argument("datasource_name")
|
|
834
|
+
@click.pass_context
|
|
835
|
+
@coro
|
|
836
|
+
async def datasource_scheduling_pause(ctx: Context, datasource_name: str) -> None:
|
|
837
|
+
"""Pause the scheduling of a Data Source."""
|
|
838
|
+
|
|
839
|
+
click.echo(FeedbackManager.info_datasource_scheduling_pause())
|
|
840
|
+
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
841
|
+
|
|
842
|
+
try:
|
|
843
|
+
await client.datasource_scheduling_pause(datasource_name)
|
|
844
|
+
click.echo(FeedbackManager.success_datasource_scheduling_paused(datasource=datasource_name))
|
|
845
|
+
|
|
846
|
+
except Exception as e:
|
|
847
|
+
raise CLIDatasourceException(
|
|
848
|
+
FeedbackManager.error_pausing_datasource_scheduling(datasource=datasource_name, error=e)
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
@datasource_scheduling.command(name="resume")
|
|
853
|
+
@click.argument("datasource_name")
|
|
854
|
+
@click.pass_context
|
|
855
|
+
@coro
|
|
856
|
+
async def datasource_scheduling_resume(ctx: Context, datasource_name: str) -> None:
|
|
857
|
+
"""Resume the scheduling of a Data Source."""
|
|
858
|
+
|
|
859
|
+
click.echo(FeedbackManager.info_datasource_scheduling_resume())
|
|
860
|
+
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
861
|
+
|
|
862
|
+
try:
|
|
863
|
+
await client.datasource_scheduling_resume(datasource_name)
|
|
864
|
+
click.echo(FeedbackManager.success_datasource_scheduling_resumed(datasource=datasource_name))
|
|
865
|
+
|
|
866
|
+
except Exception as e:
|
|
867
|
+
raise CLIDatasourceException(
|
|
868
|
+
FeedbackManager.error_resuming_datasource_scheduling(datasource=datasource_name, error=e)
|
|
869
|
+
)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import difflib
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from click import Context
|
|
8
|
+
|
|
9
|
+
from tinybird.datafile import color_diff, format_datasource, format_pipe, is_file_a_datasource, peek
|
|
10
|
+
from tinybird.feedback_manager import FeedbackManager
|
|
11
|
+
from tinybird.tb_cli_modules.cli import cli
|
|
12
|
+
from tinybird.tb_cli_modules.common import coro
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@cli.command()
|
|
16
|
+
@click.argument("filenames", type=click.Path(exists=True), nargs=-1, required=True)
|
|
17
|
+
@click.option(
|
|
18
|
+
"--line-length",
|
|
19
|
+
is_flag=False,
|
|
20
|
+
default=100,
|
|
21
|
+
help="A number indicating the maximum characters per line in the node SQL, lines will be splitted based on the SQL syntax and the number of characters passed as a parameter",
|
|
22
|
+
)
|
|
23
|
+
@click.option("--dry-run", is_flag=True, default=False, help="Don't ask to override the local file")
|
|
24
|
+
@click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation to overwrite the local file")
|
|
25
|
+
@click.option(
|
|
26
|
+
"--diff",
|
|
27
|
+
is_flag=True,
|
|
28
|
+
default=False,
|
|
29
|
+
help="Formats local file, prints the diff and exits 1 if different, 0 if equal",
|
|
30
|
+
)
|
|
31
|
+
@click.pass_context
|
|
32
|
+
@coro
|
|
33
|
+
async def fmt(
|
|
34
|
+
ctx: Context, filenames: List[str], line_length: int, dry_run: bool, yes: bool, diff: bool
|
|
35
|
+
) -> Optional[str]:
|
|
36
|
+
"""
|
|
37
|
+
Formats a .datasource, .pipe or .incl file
|
|
38
|
+
|
|
39
|
+
This command removes comments starting with # from the file, use DESCRIPTION instead.
|
|
40
|
+
|
|
41
|
+
The format command tries to parse the datafile so syntax errors might rise.
|
|
42
|
+
|
|
43
|
+
.incl files must contain a NODE definition
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
result = ""
|
|
47
|
+
failed = []
|
|
48
|
+
for filename in filenames:
|
|
49
|
+
if not diff:
|
|
50
|
+
click.echo(filename)
|
|
51
|
+
extensions = Path(filename).suffixes
|
|
52
|
+
if is_file_a_datasource(filename):
|
|
53
|
+
result = await format_datasource(filename, skip_eval=True)
|
|
54
|
+
elif (".pipe" in extensions) or (".incl" in extensions):
|
|
55
|
+
result = await format_pipe(filename, line_length, skip_eval=True)
|
|
56
|
+
else:
|
|
57
|
+
click.echo("Unsupported file type. Supported files types are: .pipe, .incl and .datasource")
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
if diff:
|
|
61
|
+
result = result.rstrip("\n")
|
|
62
|
+
lines_fmt = [f"{line}\n" for line in result.split("\n")]
|
|
63
|
+
with open(filename, "r") as file:
|
|
64
|
+
lines_file = file.readlines()
|
|
65
|
+
diff_result = difflib.unified_diff(
|
|
66
|
+
lines_file, lines_fmt, fromfile=f"{Path(filename).name} local", tofile="fmt datafile"
|
|
67
|
+
)
|
|
68
|
+
diff_result = color_diff(diff_result)
|
|
69
|
+
not_empty, diff_lines = peek(diff_result)
|
|
70
|
+
if not_empty:
|
|
71
|
+
sys.stdout.writelines(diff_lines)
|
|
72
|
+
failed.append(filename)
|
|
73
|
+
click.echo("")
|
|
74
|
+
else:
|
|
75
|
+
click.echo(result)
|
|
76
|
+
if dry_run:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
if yes or click.confirm(FeedbackManager.prompt_override_local_file(name=filename)):
|
|
80
|
+
with open(f"{filename}", "w") as file:
|
|
81
|
+
file.write(result)
|
|
82
|
+
|
|
83
|
+
click.echo(FeedbackManager.success_generated_local_file(file=filename))
|
|
84
|
+
|
|
85
|
+
if len(failed):
|
|
86
|
+
click.echo(FeedbackManager.error_failed_to_format_files(number=len(failed)))
|
|
87
|
+
for f in failed:
|
|
88
|
+
click.echo(f"tb fmt {f} --yes")
|
|
89
|
+
sys.exit(1)
|
|
90
|
+
return result
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from click import Context
|
|
5
|
+
|
|
6
|
+
from tinybird.feedback_manager import FeedbackManager
|
|
7
|
+
from tinybird.tb_cli_modules.cli import cli
|
|
8
|
+
from tinybird.tb_cli_modules.common import (
|
|
9
|
+
coro,
|
|
10
|
+
echo_safe_humanfriendly_tables_format_smart_table,
|
|
11
|
+
get_current_main_workspace,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@cli.group()
|
|
16
|
+
@click.pass_context
|
|
17
|
+
def tag(ctx: Context) -> None:
|
|
18
|
+
"""Tag commands"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@tag.command(name="ls")
|
|
22
|
+
@click.argument("tag_name", required=False)
|
|
23
|
+
@click.pass_context
|
|
24
|
+
@coro
|
|
25
|
+
async def tag_ls(ctx: Context, tag_name: Optional[str]) -> None:
|
|
26
|
+
"""List all the tags of the current Workspace or the resources associated to a specific tag."""
|
|
27
|
+
|
|
28
|
+
client = ctx.ensure_object(dict)["client"]
|
|
29
|
+
config = ctx.ensure_object(dict)["config"]
|
|
30
|
+
main_workspace = await get_current_main_workspace(client, config)
|
|
31
|
+
token = main_workspace.get("token") if main_workspace else None
|
|
32
|
+
|
|
33
|
+
response = await client.get_all_tags(token=token)
|
|
34
|
+
|
|
35
|
+
if tag_name:
|
|
36
|
+
the_tag = [tag for tag in response["tags"] if tag["name"] == tag_name]
|
|
37
|
+
|
|
38
|
+
columns = ["name", "id", "type"]
|
|
39
|
+
table = []
|
|
40
|
+
|
|
41
|
+
if len(the_tag) > 0:
|
|
42
|
+
for resource in the_tag[0]["resources"]:
|
|
43
|
+
table.append([resource["name"], resource["id"], resource["type"]])
|
|
44
|
+
|
|
45
|
+
click.echo(FeedbackManager.info_tag_resources(tag_name=tag_name))
|
|
46
|
+
echo_safe_humanfriendly_tables_format_smart_table(table, column_names=columns)
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
columns = ["tag", "resources"]
|
|
50
|
+
table = []
|
|
51
|
+
|
|
52
|
+
for tag in response["tags"]:
|
|
53
|
+
unique_resources = []
|
|
54
|
+
for resource in tag["resources"]:
|
|
55
|
+
if resource.get("name", "") not in unique_resources:
|
|
56
|
+
unique_resources.append(resource) # Reducing by name in case there are duplicates.
|
|
57
|
+
table.append([tag["name"], len(unique_resources)])
|
|
58
|
+
|
|
59
|
+
click.echo(FeedbackManager.info_tag_list())
|
|
60
|
+
echo_safe_humanfriendly_tables_format_smart_table(table, column_names=columns)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@tag.command(name="create")
|
|
64
|
+
@click.argument("tag_name")
|
|
65
|
+
@click.pass_context
|
|
66
|
+
@coro
|
|
67
|
+
async def tag_create(ctx: Context, tag_name: str) -> None:
|
|
68
|
+
"""Create a tag in the current Workspace."""
|
|
69
|
+
|
|
70
|
+
client = ctx.ensure_object(dict)["client"]
|
|
71
|
+
config = ctx.ensure_object(dict)["config"]
|
|
72
|
+
main_workspace = await get_current_main_workspace(client, config)
|
|
73
|
+
token = main_workspace.get("token") if main_workspace else None
|
|
74
|
+
|
|
75
|
+
await client.create_tag(name=tag_name, token=token)
|
|
76
|
+
|
|
77
|
+
click.echo(FeedbackManager.success_tag_created(tag_name=tag_name))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@tag.command(name="rm")
|
|
81
|
+
@click.argument("tag_name")
|
|
82
|
+
@click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation to delete the tag.")
|
|
83
|
+
@click.pass_context
|
|
84
|
+
@coro
|
|
85
|
+
async def tag_rm(ctx: Context, tag_name: str, yes: bool) -> None:
|
|
86
|
+
"""Remove a tag from the current Workspace."""
|
|
87
|
+
|
|
88
|
+
client = ctx.ensure_object(dict)["client"]
|
|
89
|
+
config = ctx.ensure_object(dict)["config"]
|
|
90
|
+
main_workspace = await get_current_main_workspace(client, config)
|
|
91
|
+
token = main_workspace.get("token") if main_workspace else None
|
|
92
|
+
|
|
93
|
+
remove_tag = True
|
|
94
|
+
|
|
95
|
+
if not yes:
|
|
96
|
+
all_tags = await client.get_all_tags(token=token)
|
|
97
|
+
the_tag = [tag for tag in all_tags["tags"] if tag["name"] == tag_name]
|
|
98
|
+
if len(the_tag) > 0:
|
|
99
|
+
unique_resources = []
|
|
100
|
+
for resource in the_tag[0]["resources"]:
|
|
101
|
+
if resource.get("name", "") not in unique_resources:
|
|
102
|
+
unique_resources.append(resource) # Reducing by name in case there are duplicates.
|
|
103
|
+
|
|
104
|
+
if len(unique_resources) > 0:
|
|
105
|
+
remove_tag = click.confirm(
|
|
106
|
+
FeedbackManager.warning_tag_remove(tag_name=tag_name, resources_len=len(unique_resources))
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
remove_tag = click.confirm(FeedbackManager.warning_tag_remove_no_resources(tag_name=tag_name))
|
|
110
|
+
else:
|
|
111
|
+
remove_tag = False
|
|
112
|
+
click.echo(FeedbackManager.error_tag_not_found(tag_name=tag_name))
|
|
113
|
+
|
|
114
|
+
if remove_tag:
|
|
115
|
+
await client.delete_tag(tag_name, token=token)
|
|
116
|
+
click.echo(FeedbackManager.success_tag_removed(tag_name=tag_name))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: tinybird-cli
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.8.0
|
|
4
4
|
Summary: Tinybird Command Line Tool
|
|
5
5
|
Home-page: https://www.tinybird.co/docs/cli/introduction.html
|
|
6
6
|
Author: Tinybird
|
|
@@ -18,6 +18,14 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
|
|
|
18
18
|
Changelog
|
|
19
19
|
----------
|
|
20
20
|
|
|
21
|
+
5.8.0
|
|
22
|
+
**********
|
|
23
|
+
|
|
24
|
+
- `Added` `tb datasource scheduling` commands to manage the scheduling of a Data Source
|
|
25
|
+
- `Added` `tb tag` commands to manage tags from the CLI.
|
|
26
|
+
- `Added` support to `TAGS` in `tb fmt`.
|
|
27
|
+
- `Added` support to `TAGS` in `tb pull` and `tb push`. Allows tagging resources for filtering in the UI.
|
|
28
|
+
|
|
21
29
|
5.7.0
|
|
22
30
|
**********
|
|
23
31
|
|
|
@@ -26,9 +26,11 @@ tinybird/tb_cli_modules/config.py
|
|
|
26
26
|
tinybird/tb_cli_modules/connection.py
|
|
27
27
|
tinybird/tb_cli_modules/datasource.py
|
|
28
28
|
tinybird/tb_cli_modules/exceptions.py
|
|
29
|
+
tinybird/tb_cli_modules/fmt.py
|
|
29
30
|
tinybird/tb_cli_modules/job.py
|
|
30
31
|
tinybird/tb_cli_modules/pipe.py
|
|
31
32
|
tinybird/tb_cli_modules/regions.py
|
|
33
|
+
tinybird/tb_cli_modules/tag.py
|
|
32
34
|
tinybird/tb_cli_modules/telemetry.py
|
|
33
35
|
tinybird/tb_cli_modules/test.py
|
|
34
36
|
tinybird/tb_cli_modules/token.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|