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.
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/PKG-INFO +13 -1
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/__cli__.py +2 -2
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/client.py +17 -2
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/context.py +0 -1
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/datafile.py +2 -2
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/feedback_manager.py +7 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/cicd.py +1 -1
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/common.py +13 -1
- tinybird-cli-4.0.1.dev1/tinybird/tb_cli_modules/token.py +335 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird_cli.egg-info/PKG-INFO +13 -1
- tinybird-cli-4.0.1.dev0/tinybird/tb_cli_modules/token.py +0 -127
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/setup.cfg +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/ch_utils/constants.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/ch_utils/engine.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/check_pypi.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/config.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/connectors.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/datatypes.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/git_settings.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/sql.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/sql_template.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/sql_template_fmt.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/sql_toolset.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/syncasync.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/auth.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/branch.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/cli.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/config.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/connection.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/datasource.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/exceptions.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/job.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/pipe.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/regions.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/telemetry.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/test.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/workspace.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/workspace_members.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tornado_template.py +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird_cli.egg-info/SOURCES.txt +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird_cli.egg-info/dependency_links.txt +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird_cli.egg-info/entry_points.txt +0 -0
- {tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird_cli.egg-info/requires.txt +0 -0
- {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.
|
|
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.
|
|
8
|
-
__revision__ = '
|
|
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
|
-
|
|
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."
|
|
@@ -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
|
|
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.
|
|
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))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/tinyunit/tinyunit.py
RENAMED
|
File without changes
|
{tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py
RENAMED
|
File without changes
|
|
File without changes
|
{tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird/tb_cli_modules/workspace_members.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tinybird-cli-4.0.1.dev0 → tinybird-cli-4.0.1.dev1}/tinybird_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|