tinybird-cli 5.7.1.dev0__tar.gz → 5.8.0.dev1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/PKG-INFO +11 -1
  2. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/__cli__.py +2 -2
  3. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/client.py +56 -4
  4. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/datafile.py +185 -27
  5. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/feedback_manager.py +15 -0
  6. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/tb_cli.py +1 -0
  7. tinybird-cli-5.8.0.dev1/tinybird/tb_cli_modules/tag.py +116 -0
  8. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird_cli.egg-info/PKG-INFO +11 -1
  9. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird_cli.egg-info/SOURCES.txt +1 -0
  10. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/setup.cfg +0 -0
  11. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/ch_utils/constants.py +0 -0
  12. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/ch_utils/engine.py +0 -0
  13. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/check_pypi.py +0 -0
  14. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/config.py +0 -0
  15. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/connectors.py +0 -0
  16. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/context.py +0 -0
  17. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/datatypes.py +0 -0
  18. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/git_settings.py +0 -0
  19. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/sql.py +0 -0
  20. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/sql_template.py +0 -0
  21. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/sql_template_fmt.py +0 -0
  22. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/sql_toolset.py +0 -0
  23. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/syncasync.py +0 -0
  24. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/tb_cli_modules/auth.py +0 -0
  25. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/tb_cli_modules/branch.py +0 -0
  26. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/tb_cli_modules/cicd.py +0 -0
  27. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/tb_cli_modules/cli.py +0 -0
  28. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/tb_cli_modules/common.py +0 -0
  29. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/tb_cli_modules/config.py +0 -0
  30. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/tb_cli_modules/connection.py +0 -0
  31. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/tb_cli_modules/datasource.py +0 -0
  32. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/tb_cli_modules/exceptions.py +0 -0
  33. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/tb_cli_modules/job.py +0 -0
  34. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/tb_cli_modules/pipe.py +0 -0
  35. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/tb_cli_modules/regions.py +0 -0
  36. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/tb_cli_modules/telemetry.py +0 -0
  37. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/tb_cli_modules/test.py +0 -0
  38. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  39. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
  40. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/tb_cli_modules/token.py +0 -0
  41. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/tb_cli_modules/workspace.py +0 -0
  42. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  43. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird/tornado_template.py +0 -0
  44. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird_cli.egg-info/dependency_links.txt +0 -0
  45. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird_cli.egg-info/entry_points.txt +0 -0
  46. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/tinybird_cli.egg-info/requires.txt +0 -0
  47. {tinybird-cli-5.7.1.dev0 → tinybird-cli-5.8.0.dev1}/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.7.1.dev0
3
+ Version: 5.8.0.dev1
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,16 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
18
18
  Changelog
19
19
  ----------
20
20
 
21
+ 5.8.0.dev1
22
+ ***********
23
+
24
+ - `Added` new `tb tag` command.
25
+
26
+ 5.8.0.dev0
27
+ ***********
28
+
29
+ - `Added` support to `TAGS` in `tb pull` and `tb push`. Allows tagging resources for filtering in the UI.
30
+
21
31
  5.7.0
22
32
  **********
23
33
 
@@ -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.7.1.dev0'
8
- __revision__ = '2608aa4'
7
+ __version__ = '5.8.0.dev1'
8
+ __revision__ = 'a3056db'
@@ -103,12 +103,20 @@ class TinyB(object):
103
103
  self.semver = semver
104
104
 
105
105
  async def _req(
106
- self, endpoint: str, data=None, files=None, method: str = "GET", retries: int = LIMIT_RETRIES, **kwargs
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
- url += ("&" if "?" in endpoint else "?") + "token=" + self.token
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 self.token:
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:
@@ -1210,3 +1218,47 @@ class TinyB(object):
1210
1218
 
1211
1219
  async def check_auth_login(self) -> Dict[str, Any]:
1212
1220
  return await self._req("/v0/auth")
1221
+
1222
+ async def get_all_tags(self, token: Optional[str] = None) -> Dict[str, Any]:
1223
+ return await self._req("/v0/tags", use_token=token)
1224
+
1225
+ async def create_tag_with_resource(
1226
+ self, name: str, resource_id: str, resource_name: str, resource_type: str, token: Optional[str] = None
1227
+ ):
1228
+ return await self._req(
1229
+ "/v0/tags",
1230
+ method="POST",
1231
+ headers={"Content-Type": "application/json"},
1232
+ data=json.dumps(
1233
+ {
1234
+ "name": name,
1235
+ "resources": [{"id": resource_id, "name": resource_name, "type": resource_type}],
1236
+ }
1237
+ ),
1238
+ use_token=token,
1239
+ )
1240
+
1241
+ async def create_tag(self, name: str, token: Optional[str] = None):
1242
+ return await self._req(
1243
+ "/v0/tags",
1244
+ method="POST",
1245
+ headers={"Content-Type": "application/json"},
1246
+ data=json.dumps({"name": name}),
1247
+ use_token=token,
1248
+ )
1249
+
1250
+ async def update_tag(self, name: str, resources: List[Dict[str, Any]], token: Optional[str] = None):
1251
+ await self._req(
1252
+ f"/v0/tags/{name}",
1253
+ method="PUT",
1254
+ headers={"Content-Type": "application/json"},
1255
+ data=json.dumps(
1256
+ {
1257
+ "resources": resources,
1258
+ }
1259
+ ),
1260
+ use_token=token,
1261
+ )
1262
+
1263
+ async def delete_tag(self, name: str, token: Optional[str] = None):
1264
+ 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,32 @@ 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[Dict[str, str], List[str]]:
908
+ """
909
+ Parses a string of tags into:
910
+ - kv_tags: a dictionary of key-value tags: the previous tags we have for operational purposes. They
911
+ have 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
+ the_tags = {k: v[0] for k, v in urllib.parse.parse_qs(trimmed_entry).items()}
926
+ kv_tags.update(the_tags)
927
+ else:
928
+ filtering_tags.append(trimmed_entry)
929
+
930
+ return kv_tags, filtering_tags
931
+
932
+
906
933
  def parse(
907
934
  s: str,
908
935
  default_node: Optional[str] = None,
@@ -1100,6 +1127,16 @@ def parse(
1100
1127
 
1101
1128
  return _f
1102
1129
 
1130
+ def tags(*args: str, **kwargs: Any) -> None:
1131
+ # Pipe nodes or Data Sources
1132
+ if parser_state.current_node:
1133
+ assign_node_var("tags")(*args, **kwargs)
1134
+ else:
1135
+ # Pipes, at pipe level
1136
+ raw_tags = _unquote((" ".join(args)).strip())
1137
+ _, filtering_tags = parse_tags(raw_tags)
1138
+ doc.filtering_tags = filtering_tags
1139
+
1103
1140
  cmds = {
1104
1141
  "from": assign("from"),
1105
1142
  "source": sources,
@@ -1120,7 +1157,7 @@ def parse(
1120
1157
  "description": description,
1121
1158
  "type": assign_node_var("type"),
1122
1159
  "datasource": assign_node_var("datasource"),
1123
- "tags": assign_node_var("tags"),
1160
+ "tags": tags,
1124
1161
  "target_datasource": assign_node_var("target_datasource"),
1125
1162
  "copy_schedule": assign_node_var(CopyParameters.COPY_SCHEDULE),
1126
1163
  "copy_mode": assign_node_var("mode"),
@@ -1515,8 +1552,9 @@ async def process_file(
1515
1552
  del params["format"]
1516
1553
 
1517
1554
  if "tags" in node:
1518
- tags = {k: v[0] for k, v in urllib.parse.parse_qs(node["tags"]).items()}
1555
+ tags, filtering_tags = parse_tags(node["tags"])
1519
1556
  params.update(tags)
1557
+ doc.filtering_tags = filtering_tags
1520
1558
 
1521
1559
  resources: List[Dict[str, Any]] = []
1522
1560
 
@@ -1530,6 +1568,7 @@ async def process_file(
1530
1568
  "deps": deps,
1531
1569
  "tokens": doc.tokens,
1532
1570
  "shared_with": doc.shared_with,
1571
+ "filtering_tags": doc.filtering_tags,
1533
1572
  }
1534
1573
  )
1535
1574
 
@@ -1615,7 +1654,7 @@ async def process_file(
1615
1654
  sql = re.sub("([\t \\n']+|^)" + old + "([\t \\n'\\)]+|$)", "\\1" + new + "\\2", sql)
1616
1655
 
1617
1656
  if "tags" in node:
1618
- tags = {k: v[0] for k, v in urllib.parse.parse_qs(node["tags"]).items()}
1657
+ tags, _ = parse_tags(node["tags"]) # Only interested in the "old" tags
1619
1658
  params.update(tags)
1620
1659
 
1621
1660
  nodes.append(
@@ -1638,6 +1677,7 @@ async def process_file(
1638
1677
  "tokens": doc.tokens,
1639
1678
  "description": description,
1640
1679
  "warnings": doc.warnings,
1680
+ "filtering_tags": doc.filtering_tags,
1641
1681
  }
1642
1682
  ]
1643
1683
  elif DataFileExtensions.TOKEN in filename:
@@ -2646,6 +2686,121 @@ def show_materialized_view_warnings(warnings):
2646
2686
  )
2647
2687
 
2648
2688
 
2689
+ async def update_tags(resource_id: str, resource_name: str, resource_type: str, tags: List[str], tb_client: TinyB):
2690
+ def get_tags_for_resource(all_tags: dict, resource_id: str, resource_name: str) -> List[str]:
2691
+ tag_names = []
2692
+
2693
+ for tag in all_tags.get("tags", []):
2694
+ for resource in tag.get("resources", []):
2695
+ if resource.get("id") == resource_id or resource.get("name") == resource_name:
2696
+ tag_names.append(tag.get("name"))
2697
+ break # No need to check other resources in this tag
2698
+
2699
+ return tag_names
2700
+
2701
+ def get_tag(all_tags: dict, tag_name: str) -> Optional[dict]:
2702
+ for tag in all_tags.get("tags", []):
2703
+ if tag.get("name") == tag_name:
2704
+ return tag
2705
+ return None
2706
+
2707
+ def compare_tags(current_tags: List[str], new_tags: List[str]) -> Tuple[List[str], List[str]]:
2708
+ tags_to_add = list(set(new_tags) - set(current_tags))
2709
+ tags_to_remove = list(set(current_tags) - set(new_tags))
2710
+ return tags_to_add, tags_to_remove
2711
+
2712
+ token_from_main = await get_token_from_main_branch(tb_client)
2713
+
2714
+ try:
2715
+ all_tags = await tb_client.get_all_tags(token=token_from_main)
2716
+ except Exception as e:
2717
+ raise Exception(FeedbackManager.error_getting_tags(error=str(e)))
2718
+
2719
+ # Get all tags of that resource
2720
+ current_tags = get_tags_for_resource(all_tags, resource_id, resource_name)
2721
+
2722
+ # Get the tags to add and remove
2723
+ tags_to_add, tags_to_remove = compare_tags(current_tags, tags)
2724
+
2725
+ # Tags to add
2726
+ for tag_name in tags_to_add:
2727
+ tag = get_tag(all_tags, tag_name)
2728
+
2729
+ if not tag:
2730
+ # Create new tag
2731
+ try:
2732
+ await tb_client.create_tag_with_resource(
2733
+ name=tag_name,
2734
+ resource_id=resource_id,
2735
+ resource_name=resource_name,
2736
+ resource_type=resource_type,
2737
+ token=token_from_main,
2738
+ )
2739
+ except Exception as e:
2740
+ raise Exception(FeedbackManager.error_creating_tag(error=str(e)))
2741
+ else:
2742
+ # Update tag with new resource
2743
+ resources = tag.get("resources", [])
2744
+ resources.append({"id": resource_id, "name": resource_name, "type": resource_type})
2745
+ try:
2746
+ await tb_client.update_tag(tag.get("name", tag_name), resources, token=token_from_main)
2747
+ except Exception as e:
2748
+ raise Exception(FeedbackManager.error_updating_tag(error=str(e)))
2749
+
2750
+ # Tags to delete
2751
+ for tag_name in tags_to_remove:
2752
+ tag = get_tag(all_tags, tag_name)
2753
+
2754
+ if tag:
2755
+ resources = tag.get("resources", [])
2756
+ resources = [resource for resource in resources if resource.get("name") != resource_name]
2757
+ try:
2758
+ await tb_client.update_tag(tag.get("name", tag_name), resources, token=token_from_main)
2759
+ except Exception as e:
2760
+ raise Exception(FeedbackManager.error_updating_tag(error=str(e)))
2761
+
2762
+
2763
+ async def update_tags_in_resource(rs: Dict[str, Any], resource_type: str, client: TinyB):
2764
+ filtering_tags = rs.get("filtering_tags", [])
2765
+
2766
+ if not filtering_tags:
2767
+ return
2768
+
2769
+ resource_id = ""
2770
+ resource_name = ""
2771
+
2772
+ if resource_type == "datasource":
2773
+ ds_name = rs["params"]["name"]
2774
+ try:
2775
+ persisted_ds = await client.get_datasource(ds_name)
2776
+ resource_id = persisted_ds.get("id", "")
2777
+ resource_name = persisted_ds.get("name", "")
2778
+ except DoesNotExistException:
2779
+ click.echo(
2780
+ FeedbackManager.error_tag_generic("Could not get the latest Data Source info for updating its tags.")
2781
+ )
2782
+ elif resource_type == "pipe":
2783
+ pipe_name = rs["name"]
2784
+ try:
2785
+ persisted_pipe = await client.pipe(pipe_name)
2786
+ resource_id = persisted_pipe.get("id", "")
2787
+ resource_name = persisted_pipe.get("name", "")
2788
+ except DoesNotExistException:
2789
+ click.echo(FeedbackManager.error_tag_generic("Could not get the latest Pipe info for updating its tags."))
2790
+
2791
+ if resource_id and resource_name:
2792
+ try:
2793
+ await update_tags(
2794
+ resource_id=resource_id,
2795
+ resource_name=resource_name,
2796
+ resource_type=resource_type,
2797
+ tags=filtering_tags,
2798
+ tb_client=client,
2799
+ )
2800
+ except Exception as e:
2801
+ click.echo(FeedbackManager.error_tag_generic(error=str(e)))
2802
+
2803
+
2649
2804
  async def get_token_from_main_branch(branch_tb_client: TinyB) -> Optional[str]:
2650
2805
  token_from_main_branch = None
2651
2806
  current_workspace = await branch_tb_client.workspace_info()
@@ -3289,32 +3444,33 @@ async def new_ds(
3289
3444
 
3290
3445
  if alter_response and make_changes:
3291
3446
  # alter operation finished
3292
- return
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
3447
+ pass
3305
3448
  else:
3306
- if alter_error_message:
3307
- raise click.ClickException(
3308
- FeedbackManager.error_datasource_already_exists_and_alter_failed(
3309
- datasource=ds_name, alter_error_message=alter_error_message
3310
- )
3311
- )
3312
- if promote_error_message:
3313
- raise click.ClickException(
3314
- FeedbackManager.error_promoting_datasource(datasource=ds_name, error=promote_error_message)
3315
- )
3449
+ # removed replacing by default. When a datasource is removed data is
3450
+ # removed and all the references needs to be updated
3451
+ if (
3452
+ os.getenv("TB_I_KNOW_WHAT_I_AM_DOING")
3453
+ and click.prompt(FeedbackManager.info_ask_for_datasource_confirmation()) == ds_name
3454
+ ): # TODO move to CLI
3455
+ try:
3456
+ await client.datasource_delete(ds_name)
3457
+ click.echo(FeedbackManager.success_delete_datasource(datasource=ds_name))
3458
+ except Exception:
3459
+ raise click.ClickException(FeedbackManager.error_removing_datasource(datasource=ds_name))
3460
+ return
3316
3461
  else:
3317
- click.echo(FeedbackManager.warning_datasource_already_exists(datasource=ds_name))
3462
+ if alter_error_message:
3463
+ raise click.ClickException(
3464
+ FeedbackManager.error_datasource_already_exists_and_alter_failed(
3465
+ datasource=ds_name, alter_error_message=alter_error_message
3466
+ )
3467
+ )
3468
+ if promote_error_message:
3469
+ raise click.ClickException(
3470
+ FeedbackManager.error_promoting_datasource(datasource=ds_name, error=promote_error_message)
3471
+ )
3472
+ else:
3473
+ click.echo(FeedbackManager.warning_datasource_already_exists(datasource=ds_name))
3318
3474
 
3319
3475
 
3320
3476
  async def new_token(token: Dict[str, Any], client: TinyB, force: bool = False):
@@ -3400,6 +3556,7 @@ async def exec_file(
3400
3556
  fork_downstream=fork_downstream,
3401
3557
  fork=fork,
3402
3558
  )
3559
+ await update_tags_in_resource(r, "pipe", tb_client)
3403
3560
  elif r["resource"] == "datasources":
3404
3561
  await new_ds(
3405
3562
  r,
@@ -3412,6 +3569,7 @@ async def exec_file(
3412
3569
  fork=fork,
3413
3570
  git_release=git_release,
3414
3571
  )
3572
+ await update_tags_in_resource(r, "datasource", tb_client)
3415
3573
  elif r["resource"] == "tokens":
3416
3574
  await new_token(r, tb_client, force)
3417
3575
  else:
@@ -392,6 +392,11 @@ class FeedbackManager:
392
392
  "Several materialized nodes in pipe {pipe}. Use --node param."
393
393
  )
394
394
  error_unlinking_pipe_not_linked = error_message("** {pipe} is not linked (MV, Copy, or Sink).")
395
+ error_getting_tags = error_message("Error getting tags: {error}")
396
+ error_creating_tag = error_message("Error creating new tag: {error}")
397
+ error_updating_tag = error_message("Error updating tag: {error}")
398
+ error_tag_generic = error_message("There was an issue updating tags. {error}")
399
+ error_tag_not_found = error_message("Tag {tag_name} not found.")
395
400
 
396
401
  info_incl_relative_path = info_message("** Relative path {path} does not exist, skipping.")
397
402
  info_ignoring_incl_file = info_message(
@@ -606,6 +611,12 @@ Ready? """
606
611
  warning_pipe_restricted_params = warning_message(
607
612
  "** The parameter names {words} and '{last_word}' are reserved words. Please, choose another name or the pipe will not work as expected."
608
613
  )
614
+ warning_tag_remove_no_resources = prompt_message(
615
+ "Tag {tag_name} is not used by any resource. Do you want to remove it?"
616
+ )
617
+ warning_tag_remove = prompt_message(
618
+ "Tag {tag_name} is used by {resources_len} resources. Do you want to remove it?"
619
+ )
609
620
 
610
621
  info_fixtures_branch = info_message("** Data Fixtures are only pushed to Branches")
611
622
  info_materialize_push_datasource_exists = warning_message("** Data Source {name} already exists")
@@ -797,6 +808,8 @@ Ready? """
797
808
  info_release_rollback = info_message(
798
809
  "** The following resources IDs are present in the {semver} Release and will be restored:"
799
810
  )
811
+ info_tag_list = info_message("** Tags:")
812
+ info_tag_resources = info_message("** Resources tagged by {tag_name}:")
800
813
  warning_no_release = warning_message(
801
814
  "** Warning: Workspace does not have Releases, run `tb init --git` to activate them."
802
815
  )
@@ -981,5 +994,7 @@ Ready? """
981
994
  success_delete_token = success_message("** Token '{token}' removed successfully")
982
995
  success_refresh_token = success_message("** Token '{token}' refreshed successfully")
983
996
  success_copy_token = success_message("** Token '{token}' copied to clipboard")
997
+ success_tag_created = success_message("** Tag '{tag_name}' created!")
998
+ success_tag_removed = success_message("** Tag '{tag_name}' removed!")
984
999
 
985
1000
  debug_running_file = print_message("** Running {file}", bcolors.CGREY)
@@ -12,6 +12,7 @@ import tinybird.tb_cli_modules.connection
12
12
  import tinybird.tb_cli_modules.datasource
13
13
  import tinybird.tb_cli_modules.job
14
14
  import tinybird.tb_cli_modules.pipe
15
+ import tinybird.tb_cli_modules.tag
15
16
  import tinybird.tb_cli_modules.test
16
17
  import tinybird.tb_cli_modules.token
17
18
  import tinybird.tb_cli_modules.workspace
@@ -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.7.1.dev0
3
+ Version: 5.8.0.dev1
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,16 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
18
18
  Changelog
19
19
  ----------
20
20
 
21
+ 5.8.0.dev1
22
+ ***********
23
+
24
+ - `Added` new `tb tag` command.
25
+
26
+ 5.8.0.dev0
27
+ ***********
28
+
29
+ - `Added` support to `TAGS` in `tb pull` and `tb push`. Allows tagging resources for filtering in the UI.
30
+
21
31
  5.7.0
22
32
  **********
23
33
 
@@ -29,6 +29,7 @@ tinybird/tb_cli_modules/exceptions.py
29
29
  tinybird/tb_cli_modules/job.py
30
30
  tinybird/tb_cli_modules/pipe.py
31
31
  tinybird/tb_cli_modules/regions.py
32
+ tinybird/tb_cli_modules/tag.py
32
33
  tinybird/tb_cli_modules/telemetry.py
33
34
  tinybird/tb_cli_modules/test.py
34
35
  tinybird/tb_cli_modules/token.py