tinybird 0.0.1.dev0__py3-none-any.whl
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.
Potentially problematic release.
This version of tinybird might be problematic. Click here for more details.
- tinybird/__cli__.py +8 -0
- tinybird/ch_utils/constants.py +244 -0
- tinybird/ch_utils/engine.py +855 -0
- tinybird/check_pypi.py +25 -0
- tinybird/client.py +1281 -0
- tinybird/config.py +117 -0
- tinybird/connectors.py +428 -0
- tinybird/context.py +23 -0
- tinybird/datafile.py +5589 -0
- tinybird/datatypes.py +434 -0
- tinybird/feedback_manager.py +1022 -0
- tinybird/git_settings.py +145 -0
- tinybird/sql.py +865 -0
- tinybird/sql_template.py +2343 -0
- tinybird/sql_template_fmt.py +281 -0
- tinybird/sql_toolset.py +350 -0
- tinybird/syncasync.py +682 -0
- tinybird/tb_cli.py +25 -0
- tinybird/tb_cli_modules/auth.py +252 -0
- tinybird/tb_cli_modules/branch.py +1043 -0
- tinybird/tb_cli_modules/cicd.py +434 -0
- tinybird/tb_cli_modules/cli.py +1571 -0
- tinybird/tb_cli_modules/common.py +2082 -0
- tinybird/tb_cli_modules/config.py +344 -0
- tinybird/tb_cli_modules/connection.py +803 -0
- tinybird/tb_cli_modules/datasource.py +900 -0
- tinybird/tb_cli_modules/exceptions.py +91 -0
- tinybird/tb_cli_modules/fmt.py +91 -0
- tinybird/tb_cli_modules/job.py +85 -0
- tinybird/tb_cli_modules/pipe.py +858 -0
- tinybird/tb_cli_modules/regions.py +9 -0
- tinybird/tb_cli_modules/tag.py +100 -0
- tinybird/tb_cli_modules/telemetry.py +310 -0
- tinybird/tb_cli_modules/test.py +107 -0
- tinybird/tb_cli_modules/tinyunit/tinyunit.py +340 -0
- tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +71 -0
- tinybird/tb_cli_modules/token.py +349 -0
- tinybird/tb_cli_modules/workspace.py +269 -0
- tinybird/tb_cli_modules/workspace_members.py +212 -0
- tinybird/tornado_template.py +1194 -0
- tinybird-0.0.1.dev0.dist-info/METADATA +2815 -0
- tinybird-0.0.1.dev0.dist-info/RECORD +45 -0
- tinybird-0.0.1.dev0.dist-info/WHEEL +5 -0
- tinybird-0.0.1.dev0.dist-info/entry_points.txt +2 -0
- tinybird-0.0.1.dev0.dist-info/top_level.txt +4 -0
|
@@ -0,0 +1,349 @@
|
|
|
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 AuthNoTokenException, 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 AuthNoTokenException:
|
|
48
|
+
raise
|
|
49
|
+
except Exception as e:
|
|
50
|
+
raise CLITokenException(FeedbackManager.error_exception(error=e))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@token.command(name="rm")
|
|
54
|
+
@click.argument("token_id")
|
|
55
|
+
@click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
|
|
56
|
+
@click.pass_context
|
|
57
|
+
@coro
|
|
58
|
+
async def token_rm(ctx: Context, token_id: str, yes: bool) -> None:
|
|
59
|
+
"""Remove a static token."""
|
|
60
|
+
|
|
61
|
+
obj: Dict[str, Any] = ctx.ensure_object(dict)
|
|
62
|
+
client: TinyB = obj["client"]
|
|
63
|
+
if yes or click.confirm(FeedbackManager.warning_confirm_delete_token(token=token_id)):
|
|
64
|
+
try:
|
|
65
|
+
await client.token_delete(token_id)
|
|
66
|
+
except AuthNoTokenException:
|
|
67
|
+
raise
|
|
68
|
+
except DoesNotExistException:
|
|
69
|
+
raise CLITokenException(FeedbackManager.error_token_does_not_exist(token_id=token_id))
|
|
70
|
+
except Exception as e:
|
|
71
|
+
raise CLITokenException(FeedbackManager.error_exception(error=e))
|
|
72
|
+
click.echo(FeedbackManager.success_delete_token(token=token_id))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@token.command(name="refresh")
|
|
76
|
+
@click.argument("token_id")
|
|
77
|
+
@click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
|
|
78
|
+
@click.pass_context
|
|
79
|
+
@coro
|
|
80
|
+
async def token_refresh(ctx: Context, token_id: str, yes: bool) -> None:
|
|
81
|
+
"""Refresh a Static Token."""
|
|
82
|
+
|
|
83
|
+
obj: Dict[str, Any] = ctx.ensure_object(dict)
|
|
84
|
+
client: TinyB = obj["client"]
|
|
85
|
+
if yes or click.confirm(FeedbackManager.warning_confirm_refresh_token(token=token_id)):
|
|
86
|
+
try:
|
|
87
|
+
await client.token_refresh(token_id)
|
|
88
|
+
except AuthNoTokenException:
|
|
89
|
+
raise
|
|
90
|
+
except DoesNotExistException:
|
|
91
|
+
raise CLITokenException(FeedbackManager.error_token_does_not_exist(token_id=token_id))
|
|
92
|
+
except Exception as e:
|
|
93
|
+
raise CLITokenException(FeedbackManager.error_exception(error=e))
|
|
94
|
+
click.echo(FeedbackManager.success_refresh_token(token=token_id))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@token.command(name="scopes")
|
|
98
|
+
@click.argument("token_id")
|
|
99
|
+
@click.pass_context
|
|
100
|
+
@coro
|
|
101
|
+
async def token_scopes(ctx: Context, token_id: str) -> None:
|
|
102
|
+
"""List Static Token scopes."""
|
|
103
|
+
|
|
104
|
+
obj: Dict[str, Any] = ctx.ensure_object(dict)
|
|
105
|
+
client: TinyB = obj["client"]
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
scopes = await client.token_scopes(token_id)
|
|
109
|
+
columns = ["type", "resource", "filter"]
|
|
110
|
+
table = list(map(lambda scope: [scope.get(key, "") for key in columns], scopes))
|
|
111
|
+
click.echo(FeedbackManager.info_token_scopes(token=token_id))
|
|
112
|
+
echo_safe_humanfriendly_tables_format_smart_table(table, column_names=columns)
|
|
113
|
+
click.echo("\n")
|
|
114
|
+
except AuthNoTokenException:
|
|
115
|
+
raise
|
|
116
|
+
except Exception as e:
|
|
117
|
+
raise CLITokenException(FeedbackManager.error_exception(error=e))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@token.command(name="copy")
|
|
121
|
+
@click.argument("token_id")
|
|
122
|
+
@click.pass_context
|
|
123
|
+
@coro
|
|
124
|
+
async def token_copy(ctx: Context, token_id: str) -> None:
|
|
125
|
+
"""Copy a Static Token."""
|
|
126
|
+
|
|
127
|
+
obj: Dict[str, Any] = ctx.ensure_object(dict)
|
|
128
|
+
client: TinyB = obj["client"]
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
token = await client.token_get(token_id)
|
|
132
|
+
pyperclip.copy(token["token"].strip())
|
|
133
|
+
except AuthNoTokenException:
|
|
134
|
+
raise
|
|
135
|
+
except DoesNotExistException:
|
|
136
|
+
raise CLITokenException(FeedbackManager.error_token_does_not_exist(token_id=token_id))
|
|
137
|
+
except Exception as e:
|
|
138
|
+
raise CLITokenException(FeedbackManager.error_exception(error=e))
|
|
139
|
+
click.echo(FeedbackManager.success_copy_token(token=token_id))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def parse_ttl(ctx, param, value):
|
|
143
|
+
if value is None:
|
|
144
|
+
return None
|
|
145
|
+
try:
|
|
146
|
+
seconds = parse_timespan(value)
|
|
147
|
+
return timedelta(seconds=seconds)
|
|
148
|
+
except ValueError:
|
|
149
|
+
raise click.BadParameter(f"Invalid time to live format: {value}")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def parse_fixed_params(fixed_params_list):
|
|
153
|
+
parsed_params = []
|
|
154
|
+
for fixed_param in fixed_params_list:
|
|
155
|
+
param_dict = {}
|
|
156
|
+
for param in fixed_param.split(","):
|
|
157
|
+
key, value = param.split("=")
|
|
158
|
+
param_dict[key] = value
|
|
159
|
+
parsed_params.append(param_dict)
|
|
160
|
+
return parsed_params
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@token.group()
|
|
164
|
+
@click.pass_context
|
|
165
|
+
def create(ctx: Context) -> None:
|
|
166
|
+
"""Token creation commands.
|
|
167
|
+
|
|
168
|
+
You can create two types of tokens: JWT or Static.
|
|
169
|
+
|
|
170
|
+
* 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.
|
|
171
|
+
|
|
172
|
+
* 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).
|
|
173
|
+
|
|
174
|
+
Examples:
|
|
175
|
+
|
|
176
|
+
tb token create jwt my_jwt_token --ttl 1h --scope PIPES:READ --resource my_pipe
|
|
177
|
+
|
|
178
|
+
tb token create static my_static_token --scope PIPES:READ --resource my_pipe
|
|
179
|
+
|
|
180
|
+
tb token create static my_static_token --scope DATASOURCES:READ --resource my_datasource
|
|
181
|
+
|
|
182
|
+
tb token create static my_static_token --scope DATASOURCES:READ --resource my_datasource --filters "column_name=value"
|
|
183
|
+
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@create.command(name="jwt")
|
|
188
|
+
@click.argument("name")
|
|
189
|
+
@click.option("--ttl", type=str, callback=parse_ttl, required=True, help="Time to live (e.g., '1h', '30min', '1d')")
|
|
190
|
+
@click.option(
|
|
191
|
+
"--scope",
|
|
192
|
+
multiple=True,
|
|
193
|
+
type=click.Choice(["PIPES:READ"]),
|
|
194
|
+
required=True,
|
|
195
|
+
help="Scope of the token (only PIPES:READ is allowed for JWT tokens)",
|
|
196
|
+
)
|
|
197
|
+
@click.option("--resource", multiple=True, required=True, help="Resource associated with the scope")
|
|
198
|
+
@click.option(
|
|
199
|
+
"--fixed-params", multiple=True, help="Fixed parameters in key=value format, multiple values separated by commas"
|
|
200
|
+
)
|
|
201
|
+
@click.pass_context
|
|
202
|
+
@coro
|
|
203
|
+
async def create_jwt_token(ctx: Context, name: str, ttl: timedelta, scope, resource, fixed_params) -> None:
|
|
204
|
+
"""Create a JWT token with a TTL specify."""
|
|
205
|
+
|
|
206
|
+
obj: Dict[str, Any] = ctx.ensure_object(dict)
|
|
207
|
+
client: TinyB = obj["client"]
|
|
208
|
+
|
|
209
|
+
expiration_time = int((ttl + datetime.now(timezone.utc)).timestamp())
|
|
210
|
+
if len(scope) != len(resource):
|
|
211
|
+
raise CLITokenException(FeedbackManager.error_number_of_scopes_and_resources_mismatch())
|
|
212
|
+
|
|
213
|
+
# Ensure the number of fixed-params does not exceed the number of scope/resource pairs
|
|
214
|
+
if fixed_params and len(fixed_params) > len(scope):
|
|
215
|
+
raise CLITokenException(FeedbackManager.error_number_of_fixed_params_and_resources_mismatch())
|
|
216
|
+
|
|
217
|
+
# Parse fixed params
|
|
218
|
+
parsed_fixed_params = parse_fixed_params(fixed_params) if fixed_params else []
|
|
219
|
+
|
|
220
|
+
# Create a list of fixed params for each scope/resource pair, defaulting to empty dict if not provided
|
|
221
|
+
fixed_params_list: List[Dict[str, Any]] = [{}] * len(scope)
|
|
222
|
+
for i, params in enumerate(parsed_fixed_params):
|
|
223
|
+
fixed_params_list[i] = params
|
|
224
|
+
|
|
225
|
+
scopes = []
|
|
226
|
+
for sc, res, fparams in zip(scope, resource, fixed_params_list):
|
|
227
|
+
scopes.append(
|
|
228
|
+
{
|
|
229
|
+
"type": sc,
|
|
230
|
+
"resource": res,
|
|
231
|
+
"fixed_params": fparams,
|
|
232
|
+
}
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
response = await client.create_jwt_token(name, expiration_time, scopes)
|
|
237
|
+
except AuthNoTokenException:
|
|
238
|
+
raise
|
|
239
|
+
except Exception as e:
|
|
240
|
+
raise CLITokenException(FeedbackManager.error_exception(error=e))
|
|
241
|
+
|
|
242
|
+
click.echo("The token has been generated successfully.")
|
|
243
|
+
click.echo(
|
|
244
|
+
f"The token will expire at: {datetime.fromtimestamp(expiration_time).strftime('%Y-%m-%d %H:%M:%S')} UTC "
|
|
245
|
+
)
|
|
246
|
+
click.echo(f"The token is: {response['token']}")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# Valid scopes for Static Tokens
|
|
250
|
+
valid_scopes = [
|
|
251
|
+
"DATASOURCES:READ",
|
|
252
|
+
"DATASOURCES:APPEND",
|
|
253
|
+
"DATASOURCES:CREATE",
|
|
254
|
+
"DATASOURCES:DROP",
|
|
255
|
+
"PIPES:CREATE",
|
|
256
|
+
"PIPES:READ",
|
|
257
|
+
"PIPES:DROP",
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# As we are passing dynamic options to the command, we need to create a custom class to handle the help message
|
|
262
|
+
class DynamicOptionsCommand(click.Command):
|
|
263
|
+
def get_help(self, ctx):
|
|
264
|
+
# Usage
|
|
265
|
+
usage = "Usage: tb token create static [OPTIONS] NAME\n\n"
|
|
266
|
+
dynamic_options_help = usage
|
|
267
|
+
|
|
268
|
+
# Description
|
|
269
|
+
dynamic_options_help += " Create a Static Token that will live forever.\n\n"
|
|
270
|
+
|
|
271
|
+
# Options
|
|
272
|
+
dynamic_options_help += "Options:\n"
|
|
273
|
+
dynamic_options_help += f" --scope [{','.join(valid_scopes)}] Scope for the token [Required]\n"
|
|
274
|
+
dynamic_options_help += " --resource TEXT Resource you want to associate the scope with\n"
|
|
275
|
+
dynamic_options_help += " --filter TEXT SQL condition used to filter the values when calling with this token (eg. --filter=value > 0) \n"
|
|
276
|
+
dynamic_options_help += " -h, --help Show this message and exit.\n"
|
|
277
|
+
|
|
278
|
+
return dynamic_options_help
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@create.command(
|
|
282
|
+
name="static", context_settings=dict(ignore_unknown_options=True, allow_extra_args=True), cls=DynamicOptionsCommand
|
|
283
|
+
)
|
|
284
|
+
@click.argument("name")
|
|
285
|
+
@click.pass_context
|
|
286
|
+
@coro
|
|
287
|
+
async def create_static_token(ctx, name: str):
|
|
288
|
+
"""Create a Static Token."""
|
|
289
|
+
obj: Dict[str, Any] = ctx.ensure_object(dict)
|
|
290
|
+
client: TinyB = obj["client"]
|
|
291
|
+
|
|
292
|
+
args = ctx.args
|
|
293
|
+
scopes: List[Dict[str, str]] = []
|
|
294
|
+
current_scope = None
|
|
295
|
+
|
|
296
|
+
# We parse the arguments to get the scopes, resources and filters
|
|
297
|
+
# The arguments should be in the format --scope <scope> --resource <resource> --filter <filter>
|
|
298
|
+
i = 0
|
|
299
|
+
while i < len(args):
|
|
300
|
+
if args[i] == "--scope":
|
|
301
|
+
if current_scope:
|
|
302
|
+
scopes.append(current_scope)
|
|
303
|
+
current_scope = {}
|
|
304
|
+
current_scope = {"scope": args[i + 1]}
|
|
305
|
+
i += 2
|
|
306
|
+
elif args[i] == "--resource":
|
|
307
|
+
if current_scope is None:
|
|
308
|
+
raise click.BadParameter("Resource must follow a scope")
|
|
309
|
+
if "resource" in current_scope:
|
|
310
|
+
raise click.BadParameter(
|
|
311
|
+
"Resource already defined for this scope. The format is --scope <scope> --resource <resource> --filter <filter>"
|
|
312
|
+
)
|
|
313
|
+
current_scope["resource"] = args[i + 1]
|
|
314
|
+
i += 2
|
|
315
|
+
elif args[i] == "--filter":
|
|
316
|
+
if current_scope is None:
|
|
317
|
+
raise click.BadParameter("Filter must follow a scope")
|
|
318
|
+
if "filter" in current_scope:
|
|
319
|
+
raise click.BadParameter(
|
|
320
|
+
"Filter already defined for this scope. The format is --scope <scope> --resource <resource> --filter <filter>"
|
|
321
|
+
)
|
|
322
|
+
current_scope["filter"] = args[i + 1]
|
|
323
|
+
i += 2
|
|
324
|
+
else:
|
|
325
|
+
raise click.BadParameter(f"Unknown parameter {args[i]}")
|
|
326
|
+
|
|
327
|
+
if current_scope:
|
|
328
|
+
scopes.append(current_scope)
|
|
329
|
+
|
|
330
|
+
# Parse the scopes like `SCOPE:RESOURCE:FILTER` or `SCOPE:RESOURCE` or `SCOPE` as that's what the API expsects
|
|
331
|
+
scoped_parsed: List[str] = []
|
|
332
|
+
for scope in scopes:
|
|
333
|
+
if scope.get("resource") and scope.get("filter"):
|
|
334
|
+
scoped_parsed.append(f"{scope.get('scope')}:{scope.get('resource')}:{scope.get('filter')}")
|
|
335
|
+
elif scope.get("resource"):
|
|
336
|
+
scoped_parsed.append(f"{scope.get('scope')}:{scope.get('resource')}")
|
|
337
|
+
elif "scope" in scope:
|
|
338
|
+
scoped_parsed.append(scope.get("scope", ""))
|
|
339
|
+
else:
|
|
340
|
+
raise CLITokenException("Unknown error")
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
await client.create_token(name, scoped_parsed, origin_code=None)
|
|
344
|
+
except AuthNoTokenException:
|
|
345
|
+
raise
|
|
346
|
+
except Exception as e:
|
|
347
|
+
raise CLITokenException(FeedbackManager.error_exception(error=e))
|
|
348
|
+
|
|
349
|
+
click.echo("The token has been generated successfully.")
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# This is a command file for our CLI. Please keep it clean.
|
|
2
|
+
#
|
|
3
|
+
# - If it makes sense and only when strictly necessary, you can create utility functions in this file.
|
|
4
|
+
# - But please, **do not** interleave utility functions and command definitions.
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from click import Context
|
|
10
|
+
|
|
11
|
+
from tinybird.client import CanNotBeDeletedException, DoesNotExistException, TinyB
|
|
12
|
+
from tinybird.config import get_display_host
|
|
13
|
+
from tinybird.datafile import PipeTypes
|
|
14
|
+
from tinybird.feedback_manager import FeedbackManager
|
|
15
|
+
from tinybird.tb_cli_modules.cli import cli
|
|
16
|
+
from tinybird.tb_cli_modules.common import (
|
|
17
|
+
_get_workspace_plan_name,
|
|
18
|
+
ask_for_user_token,
|
|
19
|
+
check_user_token,
|
|
20
|
+
coro,
|
|
21
|
+
create_workspace_interactive,
|
|
22
|
+
create_workspace_non_interactive,
|
|
23
|
+
echo_safe_humanfriendly_tables_format_smart_table,
|
|
24
|
+
get_current_main_workspace,
|
|
25
|
+
is_valid_starterkit,
|
|
26
|
+
print_current_workspace,
|
|
27
|
+
switch_workspace,
|
|
28
|
+
)
|
|
29
|
+
from tinybird.tb_cli_modules.config import CLIConfig
|
|
30
|
+
from tinybird.tb_cli_modules.exceptions import CLIWorkspaceException
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@cli.group()
|
|
34
|
+
@click.pass_context
|
|
35
|
+
def workspace(ctx: Context) -> None:
|
|
36
|
+
"""Workspace commands"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@workspace.command(name="ls")
|
|
40
|
+
@click.pass_context
|
|
41
|
+
@coro
|
|
42
|
+
async def workspace_ls(ctx: Context) -> None:
|
|
43
|
+
"""List all the workspaces you have access to in the account you're currently authenticated into."""
|
|
44
|
+
|
|
45
|
+
config = CLIConfig.get_project_config()
|
|
46
|
+
client = config.get_client()
|
|
47
|
+
|
|
48
|
+
response = await client.user_workspaces()
|
|
49
|
+
|
|
50
|
+
current_main_workspace = await get_current_main_workspace(config)
|
|
51
|
+
if not current_main_workspace:
|
|
52
|
+
raise CLIWorkspaceException(FeedbackManager.error_unable_to_identify_main_workspace())
|
|
53
|
+
|
|
54
|
+
columns = ["name", "id", "role", "plan", "current"]
|
|
55
|
+
table = []
|
|
56
|
+
click.echo(FeedbackManager.info_workspaces())
|
|
57
|
+
|
|
58
|
+
for workspace in response["workspaces"]:
|
|
59
|
+
table.append(
|
|
60
|
+
[
|
|
61
|
+
workspace["name"],
|
|
62
|
+
workspace["id"],
|
|
63
|
+
workspace["role"],
|
|
64
|
+
_get_workspace_plan_name(workspace["plan"]),
|
|
65
|
+
current_main_workspace["id"] == workspace["id"],
|
|
66
|
+
]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
echo_safe_humanfriendly_tables_format_smart_table(table, column_names=columns)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@workspace.command(name="use")
|
|
73
|
+
@click.argument("workspace_name_or_id")
|
|
74
|
+
@coro
|
|
75
|
+
async def workspace_use(workspace_name_or_id: str) -> None:
|
|
76
|
+
"""Switch to another workspace. Use 'tb workspace ls' to list the workspaces you have access to."""
|
|
77
|
+
config = CLIConfig.get_project_config()
|
|
78
|
+
|
|
79
|
+
await switch_workspace(config, workspace_name_or_id)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@workspace.command(name="current")
|
|
83
|
+
@coro
|
|
84
|
+
async def workspace_current():
|
|
85
|
+
"""Show the workspace you're currently authenticated to"""
|
|
86
|
+
config = CLIConfig.get_project_config()
|
|
87
|
+
|
|
88
|
+
await print_current_workspace(config)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@workspace.command(
|
|
92
|
+
name="clear",
|
|
93
|
+
short_help="Drop all the resources inside a project. This command is dangerous because it removes everything, use with care.",
|
|
94
|
+
)
|
|
95
|
+
@click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
|
|
96
|
+
@click.option("--dry-run", is_flag=True, default=False, help="Run the command without removing anything")
|
|
97
|
+
@click.pass_context
|
|
98
|
+
@coro
|
|
99
|
+
async def clear_workspace(ctx: Context, yes: bool, dry_run: bool) -> None:
|
|
100
|
+
"""Drop all the resources inside a project. This command is dangerous because it removes everything, use with care."""
|
|
101
|
+
|
|
102
|
+
# Get current workspace to add the name to the alert message
|
|
103
|
+
config = CLIConfig.get_project_config()
|
|
104
|
+
client: TinyB = config.get_client()
|
|
105
|
+
|
|
106
|
+
response = await client.user_workspaces_and_branches()
|
|
107
|
+
|
|
108
|
+
columns = ["name", "id", "role", "plan", "current"]
|
|
109
|
+
table = []
|
|
110
|
+
|
|
111
|
+
for workspace in response["workspaces"]:
|
|
112
|
+
if config["id"] == workspace["id"]:
|
|
113
|
+
if workspace.get("is_branch"):
|
|
114
|
+
raise CLIWorkspaceException(FeedbackManager.error_not_allowed_in_branch())
|
|
115
|
+
return
|
|
116
|
+
else:
|
|
117
|
+
click.echo(FeedbackManager.info_current_workspace())
|
|
118
|
+
table.append(
|
|
119
|
+
[
|
|
120
|
+
workspace["name"],
|
|
121
|
+
workspace["id"],
|
|
122
|
+
workspace["role"],
|
|
123
|
+
_get_workspace_plan_name(workspace["plan"]),
|
|
124
|
+
True,
|
|
125
|
+
]
|
|
126
|
+
)
|
|
127
|
+
break
|
|
128
|
+
|
|
129
|
+
echo_safe_humanfriendly_tables_format_smart_table(table, column_names=columns)
|
|
130
|
+
|
|
131
|
+
if yes or click.confirm(FeedbackManager.warning_confirm_clear_workspace()):
|
|
132
|
+
pipes = await client.pipes(dependencies=False, node_attrs="id,name,materialized", attrs="name,type")
|
|
133
|
+
pipe_names = [pipe["name"] for pipe in pipes]
|
|
134
|
+
|
|
135
|
+
for pipe in pipes:
|
|
136
|
+
if pipe["type"] == PipeTypes.MATERIALIZED:
|
|
137
|
+
if not dry_run:
|
|
138
|
+
node_id = None
|
|
139
|
+
for node in pipe["nodes"]:
|
|
140
|
+
if "materialized" in node and node["materialized"] is not None:
|
|
141
|
+
node_id = node["id"]
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
if node_id:
|
|
145
|
+
click.echo(FeedbackManager.info_unlinking_materialized_pipe(pipe=pipe["name"]))
|
|
146
|
+
try:
|
|
147
|
+
await client.pipe_unlink_materialized(pipe["name"], node_id)
|
|
148
|
+
except DoesNotExistException:
|
|
149
|
+
click.echo(FeedbackManager.info_materialized_unlinking_pipe_not_found(pipe=pipe["name"]))
|
|
150
|
+
else:
|
|
151
|
+
click.echo(FeedbackManager.info_materialized_dry_unlinking_pipe(pipe=pipe["name"]))
|
|
152
|
+
|
|
153
|
+
for pipe_name in pipe_names:
|
|
154
|
+
if not dry_run:
|
|
155
|
+
click.echo(FeedbackManager.info_removing_pipe(pipe=pipe_name))
|
|
156
|
+
try:
|
|
157
|
+
await client.pipe_delete(pipe_name)
|
|
158
|
+
except DoesNotExistException:
|
|
159
|
+
click.echo(FeedbackManager.info_removing_pipe_not_found(pipe=pipe_name))
|
|
160
|
+
else:
|
|
161
|
+
click.echo(FeedbackManager.info_dry_removing_pipe(pipe=pipe_name))
|
|
162
|
+
|
|
163
|
+
datasources = await client.datasources()
|
|
164
|
+
ds_names = [datasource["name"] for datasource in datasources]
|
|
165
|
+
for ds_name in ds_names:
|
|
166
|
+
if not dry_run:
|
|
167
|
+
click.echo(FeedbackManager.info_removing_datasource(datasource=ds_name))
|
|
168
|
+
try:
|
|
169
|
+
await client.datasource_delete(ds_name, force=True)
|
|
170
|
+
except DoesNotExistException:
|
|
171
|
+
click.echo(FeedbackManager.info_removing_datasource_not_found(datasource=ds_name))
|
|
172
|
+
except CanNotBeDeletedException as e:
|
|
173
|
+
raise CLIWorkspaceException(
|
|
174
|
+
FeedbackManager.error_datasource_can_not_be_deleted(datasource=ds_name, error=e)
|
|
175
|
+
)
|
|
176
|
+
except Exception as e:
|
|
177
|
+
if "is a Shared Data Source" in str(e):
|
|
178
|
+
raise CLIWorkspaceException(FeedbackManager.error_operation_can_not_be_performed(error=e))
|
|
179
|
+
else:
|
|
180
|
+
raise CLIWorkspaceException(FeedbackManager.error_exception(error=e))
|
|
181
|
+
else:
|
|
182
|
+
click.echo(FeedbackManager.info_dry_removing_datasource(datasource=ds_name))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@workspace.command(name="create", short_help="Create a new Workspace for your Tinybird user")
|
|
186
|
+
@click.argument("workspace_name", required=False)
|
|
187
|
+
@click.option("--starter_kit", "starter_kit", type=str, required=False, help="Use a Tinybird starter kit as a template")
|
|
188
|
+
@click.option("--starter-kit", "starter_kit", hidden=True)
|
|
189
|
+
@click.option("--user_token", is_flag=False, default=None, help="When passed, we won't prompt asking for it")
|
|
190
|
+
@click.option(
|
|
191
|
+
"--fork",
|
|
192
|
+
is_flag=True,
|
|
193
|
+
default=False,
|
|
194
|
+
help="When enabled, we will share all datasource from the current workspace to the new created one",
|
|
195
|
+
)
|
|
196
|
+
@click.pass_context
|
|
197
|
+
@coro
|
|
198
|
+
async def create_workspace(
|
|
199
|
+
ctx: Context, workspace_name: str, starter_kit: str, user_token: Optional[str], fork: bool
|
|
200
|
+
) -> None:
|
|
201
|
+
if starter_kit and not await is_valid_starterkit(ctx, starter_kit):
|
|
202
|
+
raise CLIWorkspaceException(FeedbackManager.error_starterkit_name(starterkit_name=starter_kit))
|
|
203
|
+
|
|
204
|
+
if not user_token:
|
|
205
|
+
config = CLIConfig.get_project_config()
|
|
206
|
+
host = config.get_host() or CLIConfig.DEFAULTS["host"]
|
|
207
|
+
ui_host = get_display_host(host)
|
|
208
|
+
user_token = ask_for_user_token("create a new workspace", ui_host)
|
|
209
|
+
if not user_token:
|
|
210
|
+
return
|
|
211
|
+
await check_user_token(ctx, user_token)
|
|
212
|
+
|
|
213
|
+
# If we have at least workspace_name, we start the non interactive
|
|
214
|
+
# process, creating an empty workspace
|
|
215
|
+
if workspace_name:
|
|
216
|
+
await create_workspace_non_interactive(ctx, workspace_name, starter_kit, user_token, fork)
|
|
217
|
+
else:
|
|
218
|
+
await create_workspace_interactive(ctx, workspace_name, starter_kit, user_token, fork)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@workspace.command(name="delete", short_help="Delete a Workspace for your Tinybird user")
|
|
222
|
+
@click.argument("workspace_name_or_id")
|
|
223
|
+
@click.option("--user_token", is_flag=False, default=None, help="When passed, we won't prompt asking for it")
|
|
224
|
+
@click.option(
|
|
225
|
+
"--confirm_hard_delete",
|
|
226
|
+
default=None,
|
|
227
|
+
help="Introduce the name of the workspace in order to confirm you want to run a hard delete over the workspace",
|
|
228
|
+
hidden=True,
|
|
229
|
+
)
|
|
230
|
+
@click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
|
|
231
|
+
@click.pass_context
|
|
232
|
+
@coro
|
|
233
|
+
async def delete_workspace(
|
|
234
|
+
ctx: Context, workspace_name_or_id: str, user_token: Optional[str], confirm_hard_delete: Optional[str], yes: bool
|
|
235
|
+
) -> None:
|
|
236
|
+
"""Delete a workspace where you are an admin."""
|
|
237
|
+
|
|
238
|
+
config = CLIConfig.get_project_config()
|
|
239
|
+
client = config.get_client()
|
|
240
|
+
host = config.get_host() or CLIConfig.DEFAULTS["host"]
|
|
241
|
+
ui_host = get_display_host(host)
|
|
242
|
+
|
|
243
|
+
if not user_token:
|
|
244
|
+
user_token = ask_for_user_token("delete a workspace", ui_host)
|
|
245
|
+
await check_user_token(ctx, user_token)
|
|
246
|
+
|
|
247
|
+
workspaces = (await client.user_workspaces()).get("workspaces", [])
|
|
248
|
+
workspace_to_delete = next(
|
|
249
|
+
(
|
|
250
|
+
workspace
|
|
251
|
+
for workspace in workspaces
|
|
252
|
+
if workspace["name"] == workspace_name_or_id or workspace["id"] == workspace_name_or_id
|
|
253
|
+
),
|
|
254
|
+
None,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
if not workspace_to_delete:
|
|
258
|
+
raise CLIWorkspaceException(FeedbackManager.error_workspace(workspace=workspace_name_or_id))
|
|
259
|
+
|
|
260
|
+
if yes or click.confirm(
|
|
261
|
+
FeedbackManager.warning_confirm_delete_workspace(workspace_name=workspace_to_delete.get("name"))
|
|
262
|
+
):
|
|
263
|
+
client.token = user_token
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
await client.delete_workspace(workspace_to_delete["id"], confirm_hard_delete)
|
|
267
|
+
click.echo(FeedbackManager.success_workspace_deleted(workspace_name=workspace_to_delete["name"]))
|
|
268
|
+
except Exception as e:
|
|
269
|
+
raise CLIWorkspaceException(FeedbackManager.error_exception(error=str(e)))
|