tinybird-cli 4.0.3.dev0__tar.gz → 4.1.1.dev0__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-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/PKG-INFO +7 -7
  2. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/__cli__.py +2 -2
  3. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/client.py +17 -2
  4. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/datafile.py +2 -2
  5. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/feedback_manager.py +10 -0
  6. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/tb_cli_modules/datasource.py +4 -2
  7. tinybird-cli-4.1.1.dev0/tinybird/tb_cli_modules/token.py +335 -0
  8. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird_cli.egg-info/PKG-INFO +7 -7
  9. tinybird-cli-4.0.3.dev0/tinybird/tb_cli_modules/token.py +0 -127
  10. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/setup.cfg +0 -0
  11. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/ch_utils/constants.py +0 -0
  12. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/ch_utils/engine.py +0 -0
  13. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/check_pypi.py +0 -0
  14. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/config.py +0 -0
  15. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/connectors.py +0 -0
  16. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/context.py +0 -0
  17. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/datatypes.py +0 -0
  18. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/git_settings.py +0 -0
  19. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/sql.py +0 -0
  20. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/sql_template.py +0 -0
  21. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/sql_template_fmt.py +0 -0
  22. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/sql_toolset.py +0 -0
  23. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/syncasync.py +0 -0
  24. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/tb_cli.py +0 -0
  25. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/tb_cli_modules/auth.py +0 -0
  26. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/tb_cli_modules/branch.py +0 -0
  27. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/tb_cli_modules/cicd.py +0 -0
  28. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/tb_cli_modules/cli.py +0 -0
  29. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/tb_cli_modules/common.py +0 -0
  30. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/tb_cli_modules/config.py +0 -0
  31. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/tb_cli_modules/connection.py +0 -0
  32. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/tb_cli_modules/exceptions.py +0 -0
  33. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/tb_cli_modules/job.py +0 -0
  34. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/tb_cli_modules/pipe.py +0 -0
  35. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/tb_cli_modules/regions.py +0 -0
  36. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/tb_cli_modules/telemetry.py +0 -0
  37. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/tb_cli_modules/test.py +0 -0
  38. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  39. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
  40. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/tb_cli_modules/workspace.py +0 -0
  41. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  42. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird/tornado_template.py +0 -0
  43. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird_cli.egg-info/SOURCES.txt +0 -0
  44. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird_cli.egg-info/dependency_links.txt +0 -0
  45. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird_cli.egg-info/entry_points.txt +0 -0
  46. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/tinybird_cli.egg-info/requires.txt +0 -0
  47. {tinybird-cli-4.0.3.dev0 → tinybird-cli-4.1.1.dev0}/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: 4.0.3.dev0
3
+ Version: 4.1.1.dev0
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -18,21 +18,21 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
18
18
  Changelog
19
19
  ----------
20
20
 
21
- 4.0.3.dev0
21
+ 4.1.0
22
22
  ************
23
23
 
24
- - `Fixed` Pin `tinybird-cli>=4,<5` in `requirements.txt` on `tb init --git`
25
-
24
+ - `Added` `tb token create` command to be able to create static and JWT tokens from the CLI. You can check more information at https://www.tinybird.co/blog-posts/jwt-api-endpoints-public-beta
25
+ - `Fixed` `tb init --git` to pin `tinybird-cli>=4,<5` in `requirements.txt` to avoid issues with the latest version of the CLI.
26
26
 
27
27
  4.0.0
28
28
  ************
29
29
 
30
- This is a major release, please read the commands affected below and consider updating your scripts and workflow before upgrading to these version.
30
+ This is a major release, please read the commands affected below and consider updating your scripts and workflow before upgrading to this version.
31
31
 
32
32
  - `Deprecated` `--semver` flag and `tb release` commands are now deprecated. You can keep using `tb deploy` to integrate and deploy from git. Changes are deployed to the main Workspace instead of to a Release.
33
- - `Removed` `--cicd` flag and CI/CD templates generation from `tb init`. You can still use the git integration, just create your own pipelines. You can use the ones in this repo as an exmple https://github.com/tinybirdco/ci
33
+ - `Removed` `--cicd` flag and CI/CD templates generation from `tb init`. You can still use the git integration, just create your own pipelines. You can use the ones in this repo as an example https://github.com/tinybirdco/ci
34
34
  - `Removed` `tb env` command is removed, use `tb branch` instead.
35
- - `Deprecated` .datasource files with `ENGINE "Join"` are deprecated, use `Engine "MergeeTree"` instead.
35
+ - `Deprecated` .datasource files with `ENGINE "Join"` is deprecated, use `Engine "MergeeTree"` instead.
36
36
  - `Deprecated` `tb materialize`
37
37
  - `Removed` Drop the `--timeout` flag from `tb push` which made the populate job to timeout. You can use now `--wait` to wait for the job to finish or nothing to just create the job and return.
38
38
  - `Removed` Support for `KEY` directive is removed. The `KEY` was used to create a Data Source with Join engine by the given `KEY` column name. Join engines are also deprecated, you can use a regular `MergeTree` Data Source instead and adapt the pipes SQL accordingly.
@@ -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__ = '4.0.3.dev0'
8
- __revision__ = '29cbf03'
7
+ __version__ = '4.1.1.dev0'
8
+ __revision__ = '4b7296d'
@@ -209,11 +209,21 @@ class TinyB(object):
209
209
  return None
210
210
 
211
211
  async def create_token(
212
- self, name: str, scope: str, origin_code: Optional[str], origin_resource_name_or_id: Optional[str]
212
+ self, name: str, scope: List[str], origin_code: Optional[str], origin_resource_name_or_id: Optional[str] = None
213
213
  ):
214
214
  origin = origin_code or "C" # == Origins.CUSTOM if none specified
215
+ params = {
216
+ "name": name,
217
+ "origin": origin,
218
+ }
219
+ if origin_resource_name_or_id:
220
+ params["resource_id"] = origin_resource_name_or_id
221
+
222
+ # TODO: We should support sending multiple scopes in the body of the request
223
+ url = f"/v0/tokens?{urlencode(params)}"
224
+ url = url + "&" + "&".join([f"scope={scope}" for scope in scope])
215
225
  return await self._req(
216
- f"/v0/tokens?name={name}&scope={scope}&origin={origin}&resource_id={origin_resource_name_or_id}",
226
+ url,
217
227
  method="POST",
218
228
  data="",
219
229
  )
@@ -1163,6 +1173,11 @@ class TinyB(object):
1163
1173
  params = self._token_to_params(token)
1164
1174
  return await self._req(f"/v0/tokens?{params}", method="POST", data="")
1165
1175
 
1176
+ async def create_jwt_token(self, name: str, expiration_time: int, scopes: List[Dict[str, Any]]):
1177
+ url_params = {"name": name, "expiration_time": expiration_time}
1178
+ body = json.dumps({"scopes": scopes})
1179
+ return await self._req(f"/v0/tokens?{urlencode(url_params)}", method="POST", data=body)
1180
+
1166
1181
  async def token_update(self, token: Dict[str, Any]):
1167
1182
  name = token["name"]
1168
1183
  params = self._token_to_params(token)
@@ -2785,7 +2785,7 @@ async def new_pipe(
2785
2785
  click.echo(FeedbackManager.info_create_not_found_token(token=token_name))
2786
2786
  try:
2787
2787
  r = await tb_client.create_token(
2788
- token_name, f"PIPES:{tk['permissions']}:{p['name']}", "P", p["name"]
2788
+ token_name, [f"PIPES:{tk['permissions']}:{p['name']}"], "P", p["name"]
2789
2789
  )
2790
2790
  token = r["token"] # type: ignore
2791
2791
  except Exception as e:
@@ -2896,7 +2896,7 @@ async def new_ds(
2896
2896
  token_name = tk["token_name"]
2897
2897
  click.echo(FeedbackManager.info_create_not_found_token(token=token_name))
2898
2898
  # DS == token_origin.Origins.DATASOURCE
2899
- await client.create_token(token_name, f"DATASOURCES:{tk['permissions']}:{ds_name}", "DS", ds_name)
2899
+ await client.create_token(token_name, [f"DATASOURCES:{tk['permissions']}:{ds_name}"], "DS", ds_name)
2900
2900
  else:
2901
2901
  click.echo(FeedbackManager.info_create_found_token(token=token_name))
2902
2902
  scopes = [f"DATASOURCES:{tk['permissions']}:{ds_name}"]
@@ -348,6 +348,13 @@ class FeedbackManager:
348
348
  "{connector} Data sources require a post-release deployment. Increment the post-release number of the semver (for example: 0.0.1 -> 0.0.1-1) to do so. You can read more about post-releases at https://www.tinybird.co/docs/production/deployment-strategies"
349
349
  )
350
350
 
351
+ error_number_of_scopes_and_resources_mismatch = error_message(
352
+ "The number of --scope and --resource options must be the same"
353
+ )
354
+ error_number_of_fixed_params_and_resources_mismatch = error_message(
355
+ "The number of --fixed-params options must not exceed the number of --scope and --resource options."
356
+ )
357
+
351
358
  info_incl_relative_path = info_message("** Relative path {path} does not exist, skipping.")
352
359
  info_ignoring_incl_file = info_message(
353
360
  "** Ignoring file {filename}. .incl files are not checked independently. They are checked as part of the file that includes them. Please check the file that includes this .incl file."
@@ -422,6 +429,9 @@ Ready? """
422
429
  "You are going to manually update workspace commit reference manually, this is just for special occasions. Do you want to update current commit reference '{current_commit}' to '{new_commit}'?"
423
430
  )
424
431
 
432
+ warning_exchange = warning_message(
433
+ "Warning: Do you want to exchange Data Source {datasource_a} by Data Source {datasource_b}?"
434
+ )
425
435
  warning_no_test_results = warning_message("Warning: No test results to show")
426
436
  warning_using_branch_token = warning_message("** You're using the token defined in $TB_TOKEN.")
427
437
  warning_using_branch_host = warning_message("** You're using the token defined in $TB_HOST.")
@@ -738,15 +738,17 @@ async def datasource_sync(ctx, datasource_name: str, yes: bool):
738
738
  @datasource.command(name="exchange", hidden=True)
739
739
  @click.argument("datasource_a", required=True)
740
740
  @click.argument("datasource_b", required=True)
741
+ @click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
741
742
  @click.pass_context
742
743
  @coro
743
- async def datasource_exchange(ctx, datasource_a, datasource_b):
744
+ async def datasource_exchange(ctx, datasource_a: str, datasource_b: str, yes: bool):
744
745
  """Exchange two data sources"""
745
746
 
746
747
  client = ctx.obj["client"]
747
748
 
748
749
  try:
749
- await client.datasource_exchange(datasource_a, datasource_b)
750
+ if yes or click.confirm(FeedbackManager.warning_exchange(datasource_a=datasource_a, datasource_b=datasource_b)):
751
+ await client.datasource_exchange(datasource_a, datasource_b)
750
752
  except Exception as e:
751
753
  raise CLIDatasourceException(FeedbackManager.error_exception(error=e))
752
754
 
@@ -0,0 +1,335 @@
1
+ from datetime import datetime, timedelta, timezone
2
+ from typing import Any, Dict, List, Optional
3
+
4
+ import click
5
+ import pyperclip
6
+ from click import Context
7
+ from humanfriendly import parse_timespan
8
+
9
+ from tinybird.client import TinyB
10
+ from tinybird.feedback_manager import FeedbackManager
11
+ from tinybird.tb_cli_modules.cli import cli
12
+ from tinybird.tb_cli_modules.common import (
13
+ DoesNotExistException,
14
+ coro,
15
+ echo_safe_humanfriendly_tables_format_smart_table,
16
+ )
17
+ from tinybird.tb_cli_modules.exceptions import CLITokenException
18
+
19
+
20
+ @cli.group()
21
+ @click.pass_context
22
+ def token(ctx: Context) -> None:
23
+ """Token commands."""
24
+
25
+
26
+ @token.command(name="ls")
27
+ @click.option("--match", default=None, help="Retrieve any token matching the pattern. eg --match _test")
28
+ @click.pass_context
29
+ @coro
30
+ async def token_ls(
31
+ ctx: Context,
32
+ match: Optional[str] = None,
33
+ ) -> None:
34
+ """List static tokens."""
35
+
36
+ obj: Dict[str, Any] = ctx.ensure_object(dict)
37
+ client: TinyB = obj["client"]
38
+
39
+ try:
40
+ tokens = await client.token_list(match)
41
+ columns = ["id", "name", "description"]
42
+ table = list(map(lambda token: [token.get(key, "") for key in columns], tokens))
43
+
44
+ click.echo(FeedbackManager.info_tokens())
45
+ echo_safe_humanfriendly_tables_format_smart_table(table, column_names=columns)
46
+ click.echo("\n")
47
+ except Exception as e:
48
+ raise CLITokenException(FeedbackManager.error_exception(error=e))
49
+
50
+
51
+ @token.command(name="rm")
52
+ @click.argument("token_id")
53
+ @click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
54
+ @click.pass_context
55
+ @coro
56
+ async def token_rm(ctx: Context, token_id: str, yes: bool) -> None:
57
+ """Remove a static token."""
58
+
59
+ obj: Dict[str, Any] = ctx.ensure_object(dict)
60
+ client: TinyB = obj["client"]
61
+ if yes or click.confirm(FeedbackManager.warning_confirm_delete_token(token=token_id)):
62
+ try:
63
+ await client.token_delete(token_id)
64
+ except DoesNotExistException:
65
+ raise CLITokenException(FeedbackManager.error_token_does_not_exist(token_id=token_id))
66
+ except Exception as e:
67
+ raise CLITokenException(FeedbackManager.error_exception(error=e))
68
+ click.echo(FeedbackManager.success_delete_token(token=token_id))
69
+
70
+
71
+ @token.command(name="refresh")
72
+ @click.argument("token_id")
73
+ @click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
74
+ @click.pass_context
75
+ @coro
76
+ async def token_refresh(ctx: Context, token_id: str, yes: bool) -> None:
77
+ """Refresh a static token."""
78
+
79
+ obj: Dict[str, Any] = ctx.ensure_object(dict)
80
+ client: TinyB = obj["client"]
81
+ if yes or click.confirm(FeedbackManager.warning_confirm_refresh_token(token=token_id)):
82
+ try:
83
+ await client.token_refresh(token_id)
84
+ except DoesNotExistException:
85
+ raise CLITokenException(FeedbackManager.error_token_does_not_exist(token=token_id))
86
+ except Exception as e:
87
+ raise CLITokenException(FeedbackManager.error_exception(error=e))
88
+ click.echo(FeedbackManager.success_refresh_token(token=token_id))
89
+
90
+
91
+ @token.command(name="scopes")
92
+ @click.argument("token_id")
93
+ @click.pass_context
94
+ @coro
95
+ async def token_scopes(ctx: Context, token_id: str) -> None:
96
+ """List static token scopes."""
97
+
98
+ obj: Dict[str, Any] = ctx.ensure_object(dict)
99
+ client: TinyB = obj["client"]
100
+
101
+ try:
102
+ scopes = await client.token_scopes(token_id)
103
+ columns = ["type", "resource", "filter"]
104
+ table = list(map(lambda scope: [scope.get(key, "") for key in columns], scopes))
105
+ click.echo(FeedbackManager.info_token_scopes(token=token_id))
106
+ echo_safe_humanfriendly_tables_format_smart_table(table, column_names=columns)
107
+ click.echo("\n")
108
+ except Exception as e:
109
+ raise CLITokenException(FeedbackManager.error_exception(error=e))
110
+
111
+
112
+ @token.command(name="copy")
113
+ @click.argument("token_id")
114
+ @click.pass_context
115
+ @coro
116
+ async def token_copy(ctx: Context, token_id: str) -> None:
117
+ """Copy a static token."""
118
+
119
+ obj: Dict[str, Any] = ctx.ensure_object(dict)
120
+ client: TinyB = obj["client"]
121
+
122
+ try:
123
+ token = await client.token_get(token_id)
124
+ pyperclip.copy(token["token"].strip())
125
+ except DoesNotExistException:
126
+ raise CLITokenException(FeedbackManager.error_token_does_not_exist(token=token_id))
127
+ except Exception as e:
128
+ raise CLITokenException(FeedbackManager.error_exception(error=e))
129
+ click.echo(FeedbackManager.success_copy_token(token=token_id))
130
+
131
+
132
+ def parse_ttl(ctx, param, value):
133
+ if value is None:
134
+ return None
135
+ try:
136
+ seconds = parse_timespan(value)
137
+ return timedelta(seconds=seconds)
138
+ except ValueError:
139
+ raise click.BadParameter(f"Invalid time to live format: {value}")
140
+
141
+
142
+ def parse_fixed_params(fixed_params_list):
143
+ parsed_params = []
144
+ for fixed_param in fixed_params_list:
145
+ param_dict = {}
146
+ for param in fixed_param.split(","):
147
+ key, value = param.split("=")
148
+ param_dict[key] = value
149
+ parsed_params.append(param_dict)
150
+ return parsed_params
151
+
152
+
153
+ @token.group()
154
+ @click.pass_context
155
+ def create(ctx: Context) -> None:
156
+ """Token creation commands.
157
+
158
+ You can create two types of tokens: JWT or Static.
159
+
160
+ * JWT tokens have a TTL and can only have the PIPES:READ scope.Their main use case is allow your users to call your endpoints without exposing your API key.
161
+
162
+ * Static tokens do not have a TTL and can have any valid scope (DATASOURCES:READ, DATASOURCES:APPEND, DATASOURCES:CREATE, DATASOURCES:DROP, PIPES:CREATE, PIPES:READ, PIPES:DROP).
163
+
164
+ Examples:
165
+
166
+ tb token create jwt my_jwt_token --ttl 1h --scope PIPES:READ --resource my_pipe
167
+
168
+ tb token create static my_static_token --scope PIPES:READ --resource my_pipe
169
+
170
+ tb token create static my_static_token --scope DATASOURCES:READ --resource my_datasource
171
+
172
+ tb token create static my_static_token --scope DATASOURCES:READ --resource my_datasource --filters "column_name=value"
173
+
174
+ """
175
+
176
+
177
+ @create.command(name="jwt")
178
+ @click.argument("name")
179
+ @click.option("--ttl", type=str, callback=parse_ttl, required=True, help="Time to live (e.g., '1h', '30min', '1d')")
180
+ @click.option(
181
+ "--scope",
182
+ multiple=True,
183
+ type=click.Choice(["PIPES:READ"]),
184
+ required=True,
185
+ help="Scope of the token (only PIPES:READ is allowed for JWT tokens)",
186
+ )
187
+ @click.option("--resource", multiple=True, required=True, help="Resource associated with the scope")
188
+ @click.option(
189
+ "--fixed-params", multiple=True, help="Fixed parameters in key=value format, multiple values separated by commas"
190
+ )
191
+ @click.pass_context
192
+ @coro
193
+ async def create_jwt_token(ctx: Context, name: str, ttl: timedelta, scope, resource, fixed_params) -> None:
194
+ """Create a JWT token with a TTL specify."""
195
+
196
+ obj: Dict[str, Any] = ctx.ensure_object(dict)
197
+ client: TinyB = obj["client"]
198
+
199
+ expiration_time = int((ttl + datetime.now(timezone.utc)).timestamp())
200
+ if len(scope) != len(resource):
201
+ raise CLITokenException(FeedbackManager.error_number_of_scopes_and_resources_mismatch())
202
+
203
+ # Ensure the number of fixed-params does not exceed the number of scope/resource pairs
204
+ if fixed_params and len(fixed_params) > len(scope):
205
+ raise CLITokenException(FeedbackManager.error_number_of_fixed_params_and_resources_mismatch())
206
+
207
+ # Parse fixed params
208
+ parsed_fixed_params = parse_fixed_params(fixed_params) if fixed_params else []
209
+
210
+ # Create a list of fixed params for each scope/resource pair, defaulting to empty dict if not provided
211
+ fixed_params_list: List[Dict[str, Any]] = [{}] * len(scope)
212
+ for i, params in enumerate(parsed_fixed_params):
213
+ fixed_params_list[i] = params
214
+
215
+ scopes = []
216
+ for sc, res, fparams in zip(scope, resource, fixed_params_list):
217
+ scopes.append(
218
+ {
219
+ "type": sc,
220
+ "resource": res,
221
+ "fixed_params": fparams,
222
+ }
223
+ )
224
+
225
+ try:
226
+ response = await client.create_jwt_token(name, expiration_time, scopes)
227
+ except Exception as e:
228
+ raise CLITokenException(FeedbackManager.error_exception(error=e))
229
+
230
+ click.echo("The token has been generated successfully.")
231
+ click.echo(
232
+ f"The token will expire at: {datetime.fromtimestamp(expiration_time).strftime('%Y-%m-%d %H:%M:%S')} UTC "
233
+ )
234
+ click.echo(f"The token is: {response['token']}")
235
+
236
+
237
+ # Valid scopes for static tokens
238
+ valid_scopes = [
239
+ "DATASOURCES:READ",
240
+ "DATASOURCES:APPEND",
241
+ "DATASOURCES:CREATE",
242
+ "DATASOURCES:DROP",
243
+ "PIPES:CREATE",
244
+ "PIPES:READ",
245
+ "PIPES:DROP",
246
+ ]
247
+
248
+
249
+ # As we are passing dynamic options to the command, we need to create a custom class to handle the help message
250
+ class DynamicOptionsCommand(click.Command):
251
+ def get_help(self, ctx):
252
+ # Usage
253
+ usage = "Usage: tb token create static [OPTIONS] NAME\n\n"
254
+ dynamic_options_help = usage
255
+
256
+ # Description
257
+ dynamic_options_help += " Create a static token that will live forever.\n\n"
258
+
259
+ # Options
260
+ dynamic_options_help += "Options:\n"
261
+ dynamic_options_help += f" --scope [{','.join(valid_scopes)}] Scope for the token [Required]\n"
262
+ dynamic_options_help += " --resource TEXT Resource you want to associate the scope with\n"
263
+ dynamic_options_help += " --filter TEXT SQL condition used to filter the values when calling with this token (eg. --filter=value > 0) \n"
264
+ dynamic_options_help += " -h, --help Show this message and exit.\n"
265
+
266
+ return dynamic_options_help
267
+
268
+
269
+ @create.command(
270
+ name="static", context_settings=dict(ignore_unknown_options=True, allow_extra_args=True), cls=DynamicOptionsCommand
271
+ )
272
+ @click.argument("name")
273
+ @click.pass_context
274
+ @coro
275
+ async def create_static_token(ctx, name: str):
276
+ """Create a static token."""
277
+ obj: Dict[str, Any] = ctx.ensure_object(dict)
278
+ client: TinyB = obj["client"]
279
+
280
+ args = ctx.args
281
+ scopes: List[Dict[str, str]] = []
282
+ current_scope = None
283
+
284
+ # We parse the arguments to get the scopes, resources and filters
285
+ # The arguments should be in the format --scope <scope> --resource <resource> --filter <filter>
286
+ i = 0
287
+ while i < len(args):
288
+ if args[i] == "--scope":
289
+ if current_scope:
290
+ scopes.append(current_scope)
291
+ current_scope = {}
292
+ current_scope = {"scope": args[i + 1]}
293
+ i += 2
294
+ elif args[i] == "--resource":
295
+ if current_scope is None:
296
+ raise click.BadParameter("Resource must follow a scope")
297
+ if "resource" in current_scope:
298
+ raise click.BadParameter(
299
+ "Resource already defined for this scope. The format is --scope <scope> --resource <resource> --filter <filter>"
300
+ )
301
+ current_scope["resource"] = args[i + 1]
302
+ i += 2
303
+ elif args[i] == "--filter":
304
+ if current_scope is None:
305
+ raise click.BadParameter("Filter must follow a scope")
306
+ if "filter" in current_scope:
307
+ raise click.BadParameter(
308
+ "Filter already defined for this scope. The format is --scope <scope> --resource <resource> --filter <filter>"
309
+ )
310
+ current_scope["filter"] = args[i + 1]
311
+ i += 2
312
+ else:
313
+ raise click.BadParameter(f"Unknown parameter {args[i]}")
314
+
315
+ if current_scope:
316
+ scopes.append(current_scope)
317
+
318
+ # Parse the scopes like `SCOPE:RESOURCE:FILTER` or `SCOPE:RESOURCE` or `SCOPE` as that's what the API expsects
319
+ scoped_parsed: List[str] = []
320
+ for scope in scopes:
321
+ if scope.get("resource") and scope.get("filter"):
322
+ scoped_parsed.append(f"{scope.get('scope')}:{scope.get('resource')}:{scope.get('filter')}")
323
+ elif scope.get("resource"):
324
+ scoped_parsed.append(f"{scope.get('scope')}:{scope.get('resource')}")
325
+ elif "scope" in scope:
326
+ scoped_parsed.append(scope.get("scope", ""))
327
+ else:
328
+ raise CLITokenException("Unknown error")
329
+
330
+ try:
331
+ await client.create_token(name, scoped_parsed, origin_code=None)
332
+ except Exception as e:
333
+ raise CLITokenException(FeedbackManager.error_exception(error=e))
334
+
335
+ click.echo("The token has been generated successfully.")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tinybird-cli
3
- Version: 4.0.3.dev0
3
+ Version: 4.1.1.dev0
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -18,21 +18,21 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
18
18
  Changelog
19
19
  ----------
20
20
 
21
- 4.0.3.dev0
21
+ 4.1.0
22
22
  ************
23
23
 
24
- - `Fixed` Pin `tinybird-cli>=4,<5` in `requirements.txt` on `tb init --git`
25
-
24
+ - `Added` `tb token create` command to be able to create static and JWT tokens from the CLI. You can check more information at https://www.tinybird.co/blog-posts/jwt-api-endpoints-public-beta
25
+ - `Fixed` `tb init --git` to pin `tinybird-cli>=4,<5` in `requirements.txt` to avoid issues with the latest version of the CLI.
26
26
 
27
27
  4.0.0
28
28
  ************
29
29
 
30
- This is a major release, please read the commands affected below and consider updating your scripts and workflow before upgrading to these version.
30
+ This is a major release, please read the commands affected below and consider updating your scripts and workflow before upgrading to this version.
31
31
 
32
32
  - `Deprecated` `--semver` flag and `tb release` commands are now deprecated. You can keep using `tb deploy` to integrate and deploy from git. Changes are deployed to the main Workspace instead of to a Release.
33
- - `Removed` `--cicd` flag and CI/CD templates generation from `tb init`. You can still use the git integration, just create your own pipelines. You can use the ones in this repo as an exmple https://github.com/tinybirdco/ci
33
+ - `Removed` `--cicd` flag and CI/CD templates generation from `tb init`. You can still use the git integration, just create your own pipelines. You can use the ones in this repo as an example https://github.com/tinybirdco/ci
34
34
  - `Removed` `tb env` command is removed, use `tb branch` instead.
35
- - `Deprecated` .datasource files with `ENGINE "Join"` are deprecated, use `Engine "MergeeTree"` instead.
35
+ - `Deprecated` .datasource files with `ENGINE "Join"` is deprecated, use `Engine "MergeeTree"` instead.
36
36
  - `Deprecated` `tb materialize`
37
37
  - `Removed` Drop the `--timeout` flag from `tb push` which made the populate job to timeout. You can use now `--wait` to wait for the job to finish or nothing to just create the job and return.
38
38
  - `Removed` Support for `KEY` directive is removed. The `KEY` was used to create a Data Source with Join engine by the given `KEY` column name. Join engines are also deprecated, you can use a regular `MergeTree` Data Source instead and adapt the pipes SQL accordingly.
@@ -1,127 +0,0 @@
1
- from typing import Any, Dict, Optional
2
-
3
- import click
4
- import pyperclip
5
- from click import Context
6
-
7
- from tinybird.client import TinyB
8
- from tinybird.feedback_manager import FeedbackManager
9
- from tinybird.tb_cli_modules.cli import cli
10
- from tinybird.tb_cli_modules.common import (
11
- DoesNotExistException,
12
- coro,
13
- echo_safe_humanfriendly_tables_format_smart_table,
14
- )
15
- from tinybird.tb_cli_modules.exceptions import CLITokenException
16
-
17
-
18
- @cli.group()
19
- @click.pass_context
20
- def token(ctx: Context) -> None:
21
- """Token commands."""
22
-
23
-
24
- @token.command(name="ls")
25
- @click.option("--match", default=None, help="Retrieve any token matching the pattern. eg --match _test")
26
- @click.pass_context
27
- @coro
28
- async def token_ls(
29
- ctx: Context,
30
- match: Optional[str] = None,
31
- ) -> None:
32
- """List tokens."""
33
-
34
- obj: Dict[str, Any] = ctx.ensure_object(dict)
35
- client: TinyB = obj["client"]
36
-
37
- try:
38
- tokens = await client.token_list(match)
39
- columns = ["id", "name", "description"]
40
- table = list(map(lambda token: [token.get(key, "") for key in columns], tokens))
41
-
42
- click.echo(FeedbackManager.info_tokens())
43
- echo_safe_humanfriendly_tables_format_smart_table(table, column_names=columns)
44
- click.echo("\n")
45
- except Exception as e:
46
- raise CLITokenException(FeedbackManager.error_exception(error=e))
47
-
48
-
49
- @token.command(name="rm")
50
- @click.argument("token_id")
51
- @click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
52
- @click.pass_context
53
- @coro
54
- async def token_rm(ctx: Context, token_id: str, yes: bool) -> None:
55
- """Remove a token."""
56
-
57
- obj: Dict[str, Any] = ctx.ensure_object(dict)
58
- client: TinyB = obj["client"]
59
- if yes or click.confirm(FeedbackManager.warning_confirm_delete_token(token=token_id)):
60
- try:
61
- await client.token_delete(token_id)
62
- except DoesNotExistException:
63
- raise CLITokenException(FeedbackManager.error_token_does_not_exist(token_id=token_id))
64
- except Exception as e:
65
- raise CLITokenException(FeedbackManager.error_exception(error=e))
66
- click.echo(FeedbackManager.success_delete_token(token=token_id))
67
-
68
-
69
- @token.command(name="refresh")
70
- @click.argument("token_id")
71
- @click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
72
- @click.pass_context
73
- @coro
74
- async def token_refresh(ctx: Context, token_id: str, yes: bool) -> None:
75
- """Refresh a token."""
76
-
77
- obj: Dict[str, Any] = ctx.ensure_object(dict)
78
- client: TinyB = obj["client"]
79
- if yes or click.confirm(FeedbackManager.warning_confirm_refresh_token(token=token_id)):
80
- try:
81
- await client.token_refresh(token_id)
82
- except DoesNotExistException:
83
- raise CLITokenException(FeedbackManager.error_token_does_not_exist(token=token_id))
84
- except Exception as e:
85
- raise CLITokenException(FeedbackManager.error_exception(error=e))
86
- click.echo(FeedbackManager.success_refresh_token(token=token_id))
87
-
88
-
89
- @token.command(name="scopes")
90
- @click.argument("token_id")
91
- @click.pass_context
92
- @coro
93
- async def token_scopes(ctx: Context, token_id: str) -> None:
94
- """List token scopes."""
95
-
96
- obj: Dict[str, Any] = ctx.ensure_object(dict)
97
- client: TinyB = obj["client"]
98
-
99
- try:
100
- scopes = await client.token_scopes(token_id)
101
- columns = ["type", "resource", "filter"]
102
- table = list(map(lambda scope: [scope.get(key, "") for key in columns], scopes))
103
- click.echo(FeedbackManager.info_token_scopes(token=token_id))
104
- echo_safe_humanfriendly_tables_format_smart_table(table, column_names=columns)
105
- click.echo("\n")
106
- except Exception as e:
107
- raise CLITokenException(FeedbackManager.error_exception(error=e))
108
-
109
-
110
- @token.command(name="copy")
111
- @click.argument("token_id")
112
- @click.pass_context
113
- @coro
114
- async def token_copy(ctx: Context, token_id: str) -> None:
115
- """Copy a token."""
116
-
117
- obj: Dict[str, Any] = ctx.ensure_object(dict)
118
- client: TinyB = obj["client"]
119
-
120
- try:
121
- token = await client.token_get(token_id)
122
- pyperclip.copy(token["token"].strip())
123
- except DoesNotExistException:
124
- raise CLITokenException(FeedbackManager.error_token_does_not_exist(token=token_id))
125
- except Exception as e:
126
- raise CLITokenException(FeedbackManager.error_exception(error=e))
127
- click.echo(FeedbackManager.success_copy_token(token=token_id))