tinybird-cli 4.0.1.dev0__tar.gz → 4.0.1.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-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/PKG-INFO +13 -1
  2. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/__cli__.py +2 -2
  3. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/client.py +17 -2
  4. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/context.py +0 -1
  5. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/datafile.py +2 -2
  6. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/feedback_manager.py +7 -0
  7. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/cicd.py +1 -1
  8. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/common.py +13 -1
  9. tinybird-cli-4.0.1.dev1/tinybird/tb_cli_modules/token.py +335 -0
  10. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird_cli.egg-info/PKG-INFO +13 -1
  11. tinybird-cli-4.0.1.dev0/tinybird/tb_cli_modules/token.py +0 -127
  12. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/setup.cfg +0 -0
  13. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/ch_utils/constants.py +0 -0
  14. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/ch_utils/engine.py +0 -0
  15. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/check_pypi.py +0 -0
  16. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/config.py +0 -0
  17. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/connectors.py +0 -0
  18. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/datatypes.py +0 -0
  19. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/git_settings.py +0 -0
  20. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/sql.py +0 -0
  21. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/sql_template.py +0 -0
  22. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/sql_template_fmt.py +0 -0
  23. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/sql_toolset.py +0 -0
  24. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/syncasync.py +0 -0
  25. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli.py +0 -0
  26. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/auth.py +0 -0
  27. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/branch.py +0 -0
  28. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/cli.py +0 -0
  29. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/config.py +0 -0
  30. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/connection.py +0 -0
  31. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/datasource.py +0 -0
  32. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/exceptions.py +0 -0
  33. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/job.py +0 -0
  34. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/pipe.py +0 -0
  35. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/regions.py +0 -0
  36. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/telemetry.py +0 -0
  37. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/test.py +0 -0
  38. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  39. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
  40. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/workspace.py +0 -0
  41. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  42. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tornado_template.py +0 -0
  43. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird_cli.egg-info/SOURCES.txt +0 -0
  44. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird_cli.egg-info/dependency_links.txt +0 -0
  45. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird_cli.egg-info/entry_points.txt +0 -0
  46. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird_cli.egg-info/requires.txt +0 -0
  47. {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.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: 4.0.1.dev0
3
+ Version: 4.0.1.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,18 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
18
18
  Changelog
19
19
  ----------
20
20
 
21
+
22
+ 4.0.1.dev1
23
+ ************
24
+
25
+ - `Added` command `tb token create` to be able to create JWT and permanent tokens
26
+
27
+ 4.0.3.dev0
28
+ ************
29
+
30
+ - `Fixed` Pin `tinybird-cli>=4,<5` in `requirements.txt` on `tb init --git`
31
+
32
+
21
33
  4.0.0
22
34
  ************
23
35
 
@@ -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.1.dev0'
8
- __revision__ = '2edda96'
7
+ __version__ = '4.0.1.dev1'
8
+ __revision__ = '5a27172'
@@ -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)
@@ -12,7 +12,6 @@ origin: ContextVar[str] = ContextVar("origin")
12
12
  request_id: ContextVar[str] = ContextVar("request_id")
13
13
  engine: ContextVar[str] = ContextVar("engine")
14
14
  wait_parameter: ContextVar[bool] = ContextVar("wait_parameter")
15
- wait_for_gatherer: ContextVar[bool] = ContextVar("wait_for_gatherer")
16
15
  api_host: ContextVar[str] = ContextVar("api_host")
17
16
  ff_split_to_array_escape: ContextVar[bool] = ContextVar("ff_split_to_array_escape")
18
17
  ff_preprocess_parameters_circuit_breaker: ContextVar[bool] = ContextVar("ff_preprocess_parameters_circuit_breaker")
@@ -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."
@@ -17,7 +17,7 @@ class Provider(Enum):
17
17
 
18
18
  WORKFLOW_VERSION = "v3.1.0"
19
19
 
20
- DEFAULT_REQUIREMENTS_FILE = "tinybird-cli>=3,<4"
20
+ DEFAULT_REQUIREMENTS_FILE = "tinybird-cli>=4,<5"
21
21
 
22
22
  GITHUB_CI_YML = """
23
23
  ##################################################
@@ -1174,11 +1174,23 @@ class PlanName(Enum):
1174
1174
 
1175
1175
 
1176
1176
  def _get_workspace_plan_name(plan):
1177
+ """
1178
+ >>> _get_workspace_plan_name("dev")
1179
+ 'Build'
1180
+ >>> _get_workspace_plan_name("pro")
1181
+ 'Pro'
1182
+ >>> _get_workspace_plan_name("enterprise")
1183
+ 'Enterprise'
1184
+ >>> _get_workspace_plan_name("branch_enterprise")
1185
+ 'Enterprise'
1186
+ >>> _get_workspace_plan_name("other_plan")
1187
+ 'Custom'
1188
+ """
1177
1189
  if plan == "dev":
1178
1190
  return PlanName.DEV.value
1179
1191
  if plan == "pro":
1180
1192
  return PlanName.PRO.value
1181
- if plan == "enterprise":
1193
+ if plan in ("enterprise", "branch_enterprise"):
1182
1194
  return PlanName.ENTERPRISE.value
1183
1195
  return "Custom"
1184
1196
 
@@ -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(hidden=True)
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.1.dev0
3
+ Version: 4.0.1.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,18 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
18
18
  Changelog
19
19
  ----------
20
20
 
21
+
22
+ 4.0.1.dev1
23
+ ************
24
+
25
+ - `Added` command `tb token create` to be able to create JWT and permanent tokens
26
+
27
+ 4.0.3.dev0
28
+ ************
29
+
30
+ - `Fixed` Pin `tinybird-cli>=4,<5` in `requirements.txt` on `tb init --git`
31
+
32
+
21
33
  4.0.0
22
34
  ************
23
35
 
@@ -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))