tinybird-cli 5.8.0.dev0__tar.gz → 5.8.0.dev2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/PKG-INFO +11 -1
  2. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/__cli__.py +2 -2
  3. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/client.py +33 -9
  4. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/datafile.py +56 -27
  5. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/feedback_manager.py +11 -0
  6. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/tb_cli.py +2 -0
  7. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/tb_cli_modules/cli.py +0 -84
  8. tinybird-cli-5.8.0.dev2/tinybird/tb_cli_modules/fmt.py +90 -0
  9. tinybird-cli-5.8.0.dev2/tinybird/tb_cli_modules/tag.py +116 -0
  10. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird_cli.egg-info/PKG-INFO +11 -1
  11. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird_cli.egg-info/SOURCES.txt +2 -0
  12. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/setup.cfg +0 -0
  13. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/ch_utils/constants.py +0 -0
  14. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/ch_utils/engine.py +0 -0
  15. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/check_pypi.py +0 -0
  16. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/config.py +0 -0
  17. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/connectors.py +0 -0
  18. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/context.py +0 -0
  19. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/datatypes.py +0 -0
  20. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/git_settings.py +0 -0
  21. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/sql.py +0 -0
  22. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/sql_template.py +0 -0
  23. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/sql_template_fmt.py +0 -0
  24. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/sql_toolset.py +0 -0
  25. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/syncasync.py +0 -0
  26. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/tb_cli_modules/auth.py +0 -0
  27. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/tb_cli_modules/branch.py +0 -0
  28. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/tb_cli_modules/cicd.py +0 -0
  29. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/tb_cli_modules/common.py +0 -0
  30. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/tb_cli_modules/config.py +0 -0
  31. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/tb_cli_modules/connection.py +0 -0
  32. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/tb_cli_modules/datasource.py +0 -0
  33. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/tb_cli_modules/exceptions.py +0 -0
  34. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/tb_cli_modules/job.py +0 -0
  35. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/tb_cli_modules/pipe.py +0 -0
  36. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/tb_cli_modules/regions.py +0 -0
  37. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/tb_cli_modules/telemetry.py +0 -0
  38. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/tb_cli_modules/test.py +0 -0
  39. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  40. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
  41. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/tb_cli_modules/token.py +0 -0
  42. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/tb_cli_modules/workspace.py +0 -0
  43. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  44. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird/tornado_template.py +0 -0
  45. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird_cli.egg-info/dependency_links.txt +0 -0
  46. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird_cli.egg-info/entry_points.txt +0 -0
  47. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird_cli.egg-info/requires.txt +0 -0
  48. {tinybird-cli-5.8.0.dev0 → tinybird-cli-5.8.0.dev2}/tinybird_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tinybird-cli
3
- Version: 5.8.0.dev0
3
+ Version: 5.8.0.dev2
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.dev2
22
+ ***********
23
+
24
+ - `Added` support to `TAGS` in `tb fmt`.
25
+
26
+ 5.8.0.dev1
27
+ ***********
28
+
29
+ - `Added` new `tb tag` command.
30
+
21
31
  5.8.0.dev0
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.8.0.dev0'
8
- __revision__ = 'ea7486c'
7
+ __version__ = '5.8.0.dev2'
8
+ __revision__ = 'edc984c'
@@ -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:
@@ -1211,10 +1219,12 @@ class TinyB(object):
1211
1219
  async def check_auth_login(self) -> Dict[str, Any]:
1212
1220
  return await self._req("/v0/auth")
1213
1221
 
1214
- async def get_all_tags(self) -> Dict[str, Any]:
1215
- return await self._req("/v0/tags")
1222
+ async def get_all_tags(self, token: Optional[str] = None) -> Dict[str, Any]:
1223
+ return await self._req("/v0/tags", use_token=token)
1216
1224
 
1217
- async def create_tag_with_resource(self, name: str, resourceId: str, resourceName: str, resource_type: str):
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
+ ):
1218
1228
  return await self._req(
1219
1229
  "/v0/tags",
1220
1230
  method="POST",
@@ -1222,12 +1232,22 @@ class TinyB(object):
1222
1232
  data=json.dumps(
1223
1233
  {
1224
1234
  "name": name,
1225
- "resources": [{"id": resourceId, "name": resourceName, "type": resource_type}],
1235
+ "resources": [{"id": resource_id, "name": resource_name, "type": resource_type}],
1226
1236
  }
1227
1237
  ),
1238
+ use_token=token,
1228
1239
  )
1229
1240
 
1230
- async def update_tag(self, name: str, resources: List[Dict[str, Any]]):
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):
1231
1251
  await self._req(
1232
1252
  f"/v0/tags/{name}",
1233
1253
  method="PUT",
@@ -1237,4 +1257,8 @@ class TinyB(object):
1237
1257
  "resources": resources,
1238
1258
  }
1239
1259
  ),
1260
+ use_token=token,
1240
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)
@@ -904,30 +904,31 @@ def eval_var(s: str, skip: bool = False) -> str:
904
904
  return Template(s).safe_substitute(os.environ)
905
905
 
906
906
 
907
- def parse_tags(tags: str) -> Tuple[Dict[str, str], List[str]]:
907
+ def parse_tags(tags: str) -> Tuple[str, List[str]]:
908
908
  """
909
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)
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
912
  - filtering_tags: a list of tags that are used for filtering.
913
913
 
914
914
  Example: "with_staging=true&with_last_date=true,billing,stats" ->
915
915
  kv_tags = {"with_staging": "true", "with_last_date": "true"}
916
916
  filtering_tags = ["billing", "stats"]
917
917
  """
918
- kv_tags = {}
918
+ kv_tags = []
919
919
  filtering_tags = []
920
920
 
921
921
  entries = tags.split(",")
922
922
  for entry in entries:
923
923
  trimmed_entry = entry.strip()
924
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)
925
+ kv_tags.append(trimmed_entry)
927
926
  else:
928
927
  filtering_tags.append(trimmed_entry)
929
928
 
930
- return kv_tags, filtering_tags
929
+ all_kv_tags = "&".join(kv_tags)
930
+
931
+ return all_kv_tags, filtering_tags
931
932
 
932
933
 
933
934
  def parse(
@@ -1128,14 +1129,19 @@ def parse(
1128
1129
  return _f
1129
1130
 
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
+
1131
1135
  # 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
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
1139
1145
 
1140
1146
  cmds = {
1141
1147
  "from": assign("from"),
@@ -1552,9 +1558,8 @@ async def process_file(
1552
1558
  del params["format"]
1553
1559
 
1554
1560
  if "tags" in node:
1555
- tags, filtering_tags = parse_tags(node["tags"])
1561
+ tags = {k: v[0] for k, v in urllib.parse.parse_qs(node["tags"]).items()}
1556
1562
  params.update(tags)
1557
- doc.filtering_tags = filtering_tags
1558
1563
 
1559
1564
  resources: List[Dict[str, Any]] = []
1560
1565
 
@@ -1654,7 +1659,7 @@ async def process_file(
1654
1659
  sql = re.sub("([\t \\n']+|^)" + old + "([\t \\n'\\)]+|$)", "\\1" + new + "\\2", sql)
1655
1660
 
1656
1661
  if "tags" in node:
1657
- tags, _ = parse_tags(node["tags"]) # Only interested in the "old" tags
1662
+ tags = {k: v[0] for k, v in urllib.parse.parse_qs(node["tags"]).items()}
1658
1663
  params.update(tags)
1659
1664
 
1660
1665
  nodes.append(
@@ -2686,7 +2691,7 @@ def show_materialized_view_warnings(warnings):
2686
2691
  )
2687
2692
 
2688
2693
 
2689
- async def update_tags(resourceId: str, resourceName: str, resource_type: str, tags: List[str], tb_client: TinyB):
2694
+ async def update_tags(resource_id: str, resource_name: str, resource_type: str, tags: List[str], tb_client: TinyB):
2690
2695
  def get_tags_for_resource(all_tags: dict, resource_id: str, resource_name: str) -> List[str]:
2691
2696
  tag_names = []
2692
2697
 
@@ -2709,13 +2714,15 @@ async def update_tags(resourceId: str, resourceName: str, resource_type: str, ta
2709
2714
  tags_to_remove = list(set(current_tags) - set(new_tags))
2710
2715
  return tags_to_add, tags_to_remove
2711
2716
 
2717
+ token_from_main = await get_token_from_main_branch(tb_client)
2718
+
2712
2719
  try:
2713
- all_tags = await tb_client.get_all_tags()
2720
+ all_tags = await tb_client.get_all_tags(token=token_from_main)
2714
2721
  except Exception as e:
2715
2722
  raise Exception(FeedbackManager.error_getting_tags(error=str(e)))
2716
2723
 
2717
2724
  # Get all tags of that resource
2718
- current_tags = get_tags_for_resource(all_tags, resourceId, resourceName)
2725
+ current_tags = get_tags_for_resource(all_tags, resource_id, resource_name)
2719
2726
 
2720
2727
  # Get the tags to add and remove
2721
2728
  tags_to_add, tags_to_remove = compare_tags(current_tags, tags)
@@ -2727,15 +2734,21 @@ async def update_tags(resourceId: str, resourceName: str, resource_type: str, ta
2727
2734
  if not tag:
2728
2735
  # Create new tag
2729
2736
  try:
2730
- await tb_client.create_tag_with_resource(tag_name, resourceId, resourceName, resource_type)
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
+ )
2731
2744
  except Exception as e:
2732
2745
  raise Exception(FeedbackManager.error_creating_tag(error=str(e)))
2733
2746
  else:
2734
2747
  # Update tag with new resource
2735
2748
  resources = tag.get("resources", [])
2736
- resources.append({"id": resourceId, "name": resourceName, "type": resource_type})
2749
+ resources.append({"id": resource_id, "name": resource_name, "type": resource_type})
2737
2750
  try:
2738
- await tb_client.update_tag(tag.get("name", tag_name), resources)
2751
+ await tb_client.update_tag(tag.get("name", tag_name), resources, token=token_from_main)
2739
2752
  except Exception as e:
2740
2753
  raise Exception(FeedbackManager.error_updating_tag(error=str(e)))
2741
2754
 
@@ -2745,9 +2758,9 @@ async def update_tags(resourceId: str, resourceName: str, resource_type: str, ta
2745
2758
 
2746
2759
  if tag:
2747
2760
  resources = tag.get("resources", [])
2748
- resources = [resource for resource in resources if resource.get("name") != resourceName]
2761
+ resources = [resource for resource in resources if resource.get("name") != resource_name]
2749
2762
  try:
2750
- await tb_client.update_tag(tag.get("name", tag_name), resources)
2763
+ await tb_client.update_tag(tag.get("name", tag_name), resources, token=token_from_main)
2751
2764
  except Exception as e:
2752
2765
  raise Exception(FeedbackManager.error_updating_tag(error=str(e)))
2753
2766
 
@@ -2782,7 +2795,13 @@ async def update_tags_in_resource(rs: Dict[str, Any], resource_type: str, client
2782
2795
 
2783
2796
  if resource_id and resource_name:
2784
2797
  try:
2785
- await update_tags(resource_id, resource_name, resource_type, filtering_tags, client)
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
+ )
2786
2805
  except Exception as e:
2787
2806
  click.echo(FeedbackManager.error_tag_generic(error=str(e)))
2788
2807
 
@@ -4803,19 +4822,20 @@ async def format_datasource(
4803
4822
  if for_deploy_diff:
4804
4823
  format_description(file_parts, doc)
4805
4824
  format_tokens(file_parts, doc)
4825
+ format_tags(file_parts, doc)
4806
4826
  format_schema(file_parts, doc.nodes[0])
4807
4827
  format_indices(file_parts, doc.nodes[0])
4808
4828
  await format_engine(file_parts, doc.nodes[0], only_ttl=True if not for_deploy_diff else False, client=client)
4809
4829
  if for_deploy_diff:
4810
4830
  format_import_settings(file_parts, doc.nodes[0])
4811
4831
  format_shared_with(file_parts, doc)
4812
-
4813
4832
  else:
4814
4833
  format_sources(file_parts, doc)
4815
4834
  format_maintainer(file_parts, doc)
4816
4835
  format_version(file_parts, doc)
4817
4836
  format_description(file_parts, doc)
4818
4837
  format_tokens(file_parts, doc)
4838
+ format_tags(file_parts, doc)
4819
4839
  format_schema(file_parts, doc.nodes[0])
4820
4840
  format_indices(file_parts, doc.nodes[0])
4821
4841
  await format_engine(file_parts, doc.nodes[0])
@@ -4891,6 +4911,14 @@ def format_shared_with(file_parts: List[str], doc: Datafile) -> List[str]:
4891
4911
  return file_parts
4892
4912
 
4893
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
+
4894
4922
  async def format_engine(
4895
4923
  file_parts: List[str], node: Dict[str, Any], only_ttl: bool = False, client: Optional[TinyB] = None
4896
4924
  ) -> List[str]:
@@ -5027,6 +5055,7 @@ async def format_pipe(
5027
5055
  format_version(file_parts, doc)
5028
5056
  format_description(file_parts, doc)
5029
5057
  format_tokens(file_parts, doc)
5058
+ format_tags(file_parts, doc)
5030
5059
  if doc.includes and not unroll_includes:
5031
5060
  for k in doc.includes:
5032
5061
  # We filter only the include files as we currently have 2 items for each include
@@ -396,6 +396,7 @@ class FeedbackManager:
396
396
  error_creating_tag = error_message("Error creating new tag: {error}")
397
397
  error_updating_tag = error_message("Error updating tag: {error}")
398
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.")
399
400
 
400
401
  info_incl_relative_path = info_message("** Relative path {path} does not exist, skipping.")
401
402
  info_ignoring_incl_file = info_message(
@@ -610,6 +611,12 @@ Ready? """
610
611
  warning_pipe_restricted_params = warning_message(
611
612
  "** The parameter names {words} and '{last_word}' are reserved words. Please, choose another name or the pipe will not work as expected."
612
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
+ )
613
620
 
614
621
  info_fixtures_branch = info_message("** Data Fixtures are only pushed to Branches")
615
622
  info_materialize_push_datasource_exists = warning_message("** Data Source {name} already exists")
@@ -801,6 +808,8 @@ Ready? """
801
808
  info_release_rollback = info_message(
802
809
  "** The following resources IDs are present in the {semver} Release and will be restored:"
803
810
  )
811
+ info_tag_list = info_message("** Tags:")
812
+ info_tag_resources = info_message("** Resources tagged by {tag_name}:")
804
813
  warning_no_release = warning_message(
805
814
  "** Warning: Workspace does not have Releases, run `tb init --git` to activate them."
806
815
  )
@@ -985,5 +994,7 @@ Ready? """
985
994
  success_delete_token = success_message("** Token '{token}' removed successfully")
986
995
  success_refresh_token = success_message("** Token '{token}' refreshed successfully")
987
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!")
988
999
 
989
1000
  debug_running_file = print_message("** Running {file}", bcolors.CGREY)
@@ -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.",
@@ -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.8.0.dev0
3
+ Version: 5.8.0.dev2
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.dev2
22
+ ***********
23
+
24
+ - `Added` support to `TAGS` in `tb fmt`.
25
+
26
+ 5.8.0.dev1
27
+ ***********
28
+
29
+ - `Added` new `tb tag` command.
30
+
21
31
  5.8.0.dev0
22
32
  ***********
23
33
 
@@ -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