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.

Files changed (45) hide show
  1. tinybird/__cli__.py +8 -0
  2. tinybird/ch_utils/constants.py +244 -0
  3. tinybird/ch_utils/engine.py +855 -0
  4. tinybird/check_pypi.py +25 -0
  5. tinybird/client.py +1281 -0
  6. tinybird/config.py +117 -0
  7. tinybird/connectors.py +428 -0
  8. tinybird/context.py +23 -0
  9. tinybird/datafile.py +5589 -0
  10. tinybird/datatypes.py +434 -0
  11. tinybird/feedback_manager.py +1022 -0
  12. tinybird/git_settings.py +145 -0
  13. tinybird/sql.py +865 -0
  14. tinybird/sql_template.py +2343 -0
  15. tinybird/sql_template_fmt.py +281 -0
  16. tinybird/sql_toolset.py +350 -0
  17. tinybird/syncasync.py +682 -0
  18. tinybird/tb_cli.py +25 -0
  19. tinybird/tb_cli_modules/auth.py +252 -0
  20. tinybird/tb_cli_modules/branch.py +1043 -0
  21. tinybird/tb_cli_modules/cicd.py +434 -0
  22. tinybird/tb_cli_modules/cli.py +1571 -0
  23. tinybird/tb_cli_modules/common.py +2082 -0
  24. tinybird/tb_cli_modules/config.py +344 -0
  25. tinybird/tb_cli_modules/connection.py +803 -0
  26. tinybird/tb_cli_modules/datasource.py +900 -0
  27. tinybird/tb_cli_modules/exceptions.py +91 -0
  28. tinybird/tb_cli_modules/fmt.py +91 -0
  29. tinybird/tb_cli_modules/job.py +85 -0
  30. tinybird/tb_cli_modules/pipe.py +858 -0
  31. tinybird/tb_cli_modules/regions.py +9 -0
  32. tinybird/tb_cli_modules/tag.py +100 -0
  33. tinybird/tb_cli_modules/telemetry.py +310 -0
  34. tinybird/tb_cli_modules/test.py +107 -0
  35. tinybird/tb_cli_modules/tinyunit/tinyunit.py +340 -0
  36. tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +71 -0
  37. tinybird/tb_cli_modules/token.py +349 -0
  38. tinybird/tb_cli_modules/workspace.py +269 -0
  39. tinybird/tb_cli_modules/workspace_members.py +212 -0
  40. tinybird/tornado_template.py +1194 -0
  41. tinybird-0.0.1.dev0.dist-info/METADATA +2815 -0
  42. tinybird-0.0.1.dev0.dist-info/RECORD +45 -0
  43. tinybird-0.0.1.dev0.dist-info/WHEEL +5 -0
  44. tinybird-0.0.1.dev0.dist-info/entry_points.txt +2 -0
  45. 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)))