tinybird-cli 5.15.2.dev1__tar.gz → 5.16.1.dev0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/PKG-INFO +10 -5
  2. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/__cli__.py +2 -2
  3. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/client.py +11 -1
  4. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/feedback_manager.py +13 -2
  5. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/sql_template.py +18 -0
  6. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/tb_cli_modules/common.py +131 -7
  7. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/tb_cli_modules/config.py +3 -0
  8. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/tb_cli_modules/workspace.py +49 -6
  9. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird_cli.egg-info/PKG-INFO +10 -5
  10. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird_cli.egg-info/requires.txt +2 -2
  11. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/setup.cfg +0 -0
  12. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/ch_utils/constants.py +0 -0
  13. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/ch_utils/engine.py +0 -0
  14. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/check_pypi.py +0 -0
  15. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/config.py +0 -0
  16. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/connectors.py +0 -0
  17. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/context.py +0 -0
  18. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/datafile.py +0 -0
  19. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/datatypes.py +0 -0
  20. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/git_settings.py +0 -0
  21. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/sql.py +0 -0
  22. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/sql_template_fmt.py +0 -0
  23. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/sql_toolset.py +0 -0
  24. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/syncasync.py +0 -0
  25. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/tb_cli.py +0 -0
  26. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/tb_cli_modules/auth.py +0 -0
  27. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/tb_cli_modules/branch.py +0 -0
  28. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/tb_cli_modules/cicd.py +0 -0
  29. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/tb_cli_modules/cli.py +0 -0
  30. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/tb_cli_modules/connection.py +0 -0
  31. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/tb_cli_modules/datasource.py +0 -0
  32. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/tb_cli_modules/exceptions.py +0 -0
  33. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/tb_cli_modules/fmt.py +0 -0
  34. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/tb_cli_modules/job.py +0 -0
  35. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/tb_cli_modules/pipe.py +0 -0
  36. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/tb_cli_modules/regions.py +0 -0
  37. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/tb_cli_modules/tag.py +0 -0
  38. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/tb_cli_modules/telemetry.py +0 -0
  39. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/tb_cli_modules/test.py +0 -0
  40. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  41. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
  42. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/tb_cli_modules/token.py +0 -0
  43. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  44. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird/tornado_template.py +0 -0
  45. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird_cli.egg-info/SOURCES.txt +0 -0
  46. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird_cli.egg-info/dependency_links.txt +0 -0
  47. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird_cli.egg-info/entry_points.txt +0 -0
  48. {tinybird_cli-5.15.2.dev1 → tinybird_cli-5.16.1.dev0}/tinybird_cli.egg-info/top_level.txt +0 -0
@@ -1,15 +1,15 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: tinybird_cli
3
- Version: 5.15.2.dev1
3
+ Version: 5.16.1.dev0
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli
6
6
  Author: Tinybird
7
7
  Author-email: support@tinybird.co
8
- Requires-Python: >=3.9, <3.13
8
+ Requires-Python: >=3.9, <3.14
9
9
  Description-Content-Type: text/x-rst
10
10
  Requires-Dist: aiofiles==24.1.0
11
- Requires-Dist: click<8.2,>=8.1.6
12
- Requires-Dist: clickhouse-toolset==0.33.dev0
11
+ Requires-Dist: clickhouse-toolset==0.34.dev0
12
+ Requires-Dist: click<8.2,>=8.1.8
13
13
  Requires-Dist: colorama==0.4.6
14
14
  Requires-Dist: cryptography~=41.0.0
15
15
  Requires-Dist: croniter==1.3.15
@@ -61,11 +61,16 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
61
61
  Changelog
62
62
  ----------
63
63
 
64
- 5.15.2.dev1
64
+ 5.16.0
65
65
  ***********
66
66
 
67
+ - `Added` support for organizations on `tb workspace create`
68
+ - `Added` support for Python 3.13
67
69
  - `Updated` dependencies:
68
70
  - croniter: from 1.3.8 to 1.3.15
71
+ - clickhouse-driver: from 0.2.7 to 0.2.9
72
+ - clickhouse-toolset: from 0.33.dev0 to 0.34.dev0
73
+ - click: from 8.1.7 to 8.1.8
69
74
 
70
75
  5.15.1
71
76
  ***********
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
4
4
  __url__ = 'https://www.tinybird.co/docs/cli'
5
5
  __author__ = 'Tinybird'
6
6
  __author_email__ = 'support@tinybird.co'
7
- __version__ = '5.15.2.dev1'
8
- __revision__ = 'f29457c'
7
+ __version__ = '5.16.1.dev0'
8
+ __revision__ = '24d8376'
@@ -667,6 +667,11 @@ class TinyB:
667
667
  async def user_workspaces_and_branches(self):
668
668
  return await self._req("/v0/user/workspaces/?with_environments=true")
669
669
 
670
+ async def user_workspaces_with_organization(self):
671
+ return await self._req(
672
+ "/v0/user/workspaces/?with_environments=false&with_organization=true&with_members_and_owner=false"
673
+ )
674
+
670
675
  async def user_workspace_branches(self):
671
676
  return await self._req("/v0/user/workspaces/?with_environments=true&only_environments=true")
672
677
 
@@ -676,10 +681,12 @@ class TinyB:
676
681
  async def releases(self, workspace_id):
677
682
  return await self._req(f"/v0/workspaces/{workspace_id}/releases")
678
683
 
679
- async def create_workspace(self, name: str, template: Optional[str]):
684
+ async def create_workspace(self, name: str, template: Optional[str], organization_id: Optional[str]):
680
685
  url = f"/v0/workspaces?name={name}"
681
686
  if template:
682
687
  url += f"&starter_kit={template}"
688
+ if organization_id:
689
+ url += f"&assign_to_organization_id={organization_id}"
683
690
  return await self._req(url, method="POST", data=b"")
684
691
 
685
692
  async def create_workspace_branch(
@@ -814,6 +821,9 @@ class TinyB:
814
821
  async def workspace_info(self):
815
822
  return await self._req("/v0/workspace")
816
823
 
824
+ async def organization(self, organization_id: str):
825
+ return await self._req(f"/v0/organizations/{organization_id}")
826
+
817
827
  async def wait_for_job(
818
828
  self,
819
829
  job_id: str,
@@ -247,6 +247,14 @@ class FeedbackManager:
247
247
  error_connection_invalid_ca_pem = error_message("Invalid CA certificate in PEM format")
248
248
  error_connection_ca_pem_not_found = error_message("CA certificate in PEM format not found at {ca_pem}")
249
249
  error_workspace = error_message("Workspace {workspace} not found. use 'tb workspace ls' to list your workspaces")
250
+ error_organization_not_found = error_message("Organization with id '{organization_id}' not found")
251
+ error_organization_index = error_message(
252
+ "Error selecting organization '{organization_index}'. Select a valid index or 0 to cancel"
253
+ )
254
+ warning_none_organization = warning_message(
255
+ "Tinybird is now based on organizations. Please, go to the UI ({ui_host}) to follow the migration process. \nYour workspace will be created any way."
256
+ )
257
+ error_while_fetching_orgs = error_message("Error while fetching organizations: {error}")
250
258
  error_deleted_include = error_message(
251
259
  "Related include file {include_file} was deleted and it's used in {filename}. Delete or remove dependency from {filename}."
252
260
  )
@@ -458,7 +466,7 @@ class FeedbackManager:
458
466
  info_releases_detected = info_message("** Live Release: {current_semver} => New Release: {semver}")
459
467
  info_dry_releases_detected = info_message("** [DRY RUN] Live Release: {current_semver} => New Release: {semver}")
460
468
  info_pre_prompt_auth_login_user_token = info_message(
461
- "ℹ️ In order to log into Tinybird, you need your user token. Please, go to {host}/tokens/ and paste your User Token here."
469
+ "ℹ️ In order to log into Tinybird, you need your user token. Please, go to {host}/tokens/ and paste your User Token here." # noqa: RUF001
462
470
  )
463
471
  prompt_auth_login_user_token = prompt_message("❓ User token:")
464
472
 
@@ -609,7 +617,7 @@ Ready? """
609
617
  )
610
618
  warning_datasource_sync = warning_message("** Warning: Do you want to sync the Data Source {datasource}?")
611
619
  warning_datasource_sync_bucket = warning_message(
612
- "** Warning: Do you want to ingest all the selected files (new and previous) from the {datasource} bucket? Be aware that data might be duplicated if you have previously ingested those files.\n"
620
+ "** Warning: Do you want to ingest all the selected files (new and previous) from the {datasource} bucket? Be aware that data might be duplicated if you have previously ingested those files.\n"
613
621
  )
614
622
  warning_users_dont_exist = warning_message(
615
623
  "** Warning: The following users do not exist in the workspace {workspace}: {users}"
@@ -981,6 +989,9 @@ Ready? """
981
989
  success_connection_using = success_message("** Using connection '{connection_name}'")
982
990
  success_using_host = success_message("** Using host: {host} ({name})")
983
991
  success_workspace_created = success_message("** Workspace '{workspace_name}' has been created")
992
+ success_workspace_created_with_organization = success_message(
993
+ "** Workspace '{workspace_name}' has been created in Organization '{organization_name}' (id: '{organization_id}')"
994
+ )
984
995
  success_workspace_branch_created = success_message(
985
996
  "** Branch '{branch_name}' from '{workspace_name}' has been created"
986
997
  )
@@ -2256,6 +2256,16 @@ def render_sql_template(
2256
2256
  Traceback (most recent call last):
2257
2257
  ...
2258
2258
  tinybird.sql_template.SQLTemplateException: Template Syntax Error: Required parameter is not defined. Check the parameters test. Please provide a value or set a default value in the pipe code.
2259
+ >>> render_sql_template("SELECT * FROM test WHERE {% for item in JSON(filters, '[{}]') %} {{item.get('operand')}} {% end %}")
2260
+ Traceback (most recent call last):
2261
+ ...
2262
+ tinybird.sql_template.SQLTemplateException: Template Syntax Error: expression "item.get('operand')" evaluated to null
2263
+ >>> render_sql_template("SELECT * FROM test WHERE {% for item in JSON(filters, '[{\\\"operand\\\":\\\"test\\\"}]') %} {{item.get('operand')}} {% end %}")
2264
+ ("SELECT * FROM test WHERE 'test' ", {}, [])
2265
+ >>> render_sql_template("SELECT * FROM test WHERE {% for item in JSON(filters, '[\\\"test\\\"]') %} {{item.get('operator')}} {% end %}")
2266
+ Traceback (most recent call last):
2267
+ ...
2268
+ tinybird.sql_template.SQLTemplateException: Template Syntax Error: 'str' object has no attribute 'get'. Make sure you're using an object/dictionary where trying to use .get()
2259
2269
  """
2260
2270
  escape_split_to_array = ff_split_to_array_escape.get(False)
2261
2271
 
@@ -2323,6 +2333,14 @@ def render_sql_template(
2323
2333
  except SQLTemplateException as e:
2324
2334
  format_SQLTemplateException_message(e, vars_and_types=template_variables_with_types)
2325
2335
  raise
2336
+ except AttributeError as e:
2337
+ # This happens when trying to use `get` on a string or when the object is None
2338
+ if "'str' object has no attribute 'get'" in str(e):
2339
+ raise SQLTemplateException(
2340
+ "'str' object has no attribute 'get'. Make sure you're using an object/dictionary where trying to use .get()",
2341
+ documentation="/cli/advanced-templates.html",
2342
+ )
2343
+ raise SQLTemplateException(str(e), documentation="/cli/advanced-templates.html")
2326
2344
  except Exception as e:
2327
2345
  # errors might vary here, we need to support as much as possible
2328
2346
  # https://gitlab.com/tinybird/analytics/-/issues/943
@@ -18,7 +18,7 @@ from enum import Enum
18
18
  from functools import wraps
19
19
  from os import chmod, environ, getcwd, getenv
20
20
  from pathlib import Path
21
- from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union
21
+ from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Literal, Optional, Set, Tuple, TypedDict, Union
22
22
  from urllib.parse import urlparse
23
23
 
24
24
  import aiofiles
@@ -707,7 +707,13 @@ async def fork_workspace(client: TinyB, user_client: TinyB, created_workspace):
707
707
 
708
708
 
709
709
  async def create_workspace_non_interactive(
710
- ctx: Context, workspace_name: str, starterkit: str, user_token: str, fork: bool
710
+ ctx: Context,
711
+ workspace_name: str,
712
+ starterkit: Optional[str],
713
+ user_token: str,
714
+ fork: bool,
715
+ organization_id: Optional[str],
716
+ organization_name: Optional[str],
711
717
  ):
712
718
  """Creates a workspace using the provided name and starterkit"""
713
719
  client: TinyB = ctx.ensure_object(dict)["client"]
@@ -716,8 +722,15 @@ async def create_workspace_non_interactive(
716
722
  user_client: TinyB = deepcopy(client)
717
723
  user_client.token = user_token
718
724
 
719
- created_workspace = await user_client.create_workspace(workspace_name, starterkit)
720
- click.echo(FeedbackManager.success_workspace_created(workspace_name=workspace_name))
725
+ created_workspace = await user_client.create_workspace(workspace_name, starterkit, organization_id)
726
+ if organization_id and organization_name:
727
+ click.echo(
728
+ FeedbackManager.success_workspace_created_with_organization(
729
+ workspace_name=workspace_name, organization_name=organization_name, organization_id=organization_id
730
+ )
731
+ )
732
+ else:
733
+ click.echo(FeedbackManager.success_workspace_created(workspace_name=workspace_name))
721
734
 
722
735
  if fork:
723
736
  await fork_workspace(client, user_client, created_workspace)
@@ -727,7 +740,13 @@ async def create_workspace_non_interactive(
727
740
 
728
741
 
729
742
  async def create_workspace_interactive(
730
- ctx: Context, workspace_name: Optional[str], starterkit: Optional[str], user_token: str, fork: bool
743
+ ctx: Context,
744
+ workspace_name: Optional[str],
745
+ starterkit: Optional[str],
746
+ user_token: str,
747
+ fork: bool,
748
+ organization_id: Optional[str],
749
+ organization_name: Optional[str],
731
750
  ):
732
751
  if not starterkit and not is_ci_environment():
733
752
  click.echo("\n")
@@ -744,8 +763,17 @@ async def create_workspace_interactive(
744
763
  click.echo(FeedbackManager.info_workspace_create_greeting())
745
764
  default_name = f"new_workspace_{uuid.uuid4().hex[0:4]}"
746
765
  workspace_name = click.prompt("\nWorkspace name", default=default_name, err=True, type=str)
747
-
748
- await create_workspace_non_interactive(ctx, workspace_name, starterkit, user_token, fork) # type: ignore
766
+ assert isinstance(workspace_name, str)
767
+
768
+ await create_workspace_non_interactive(
769
+ ctx,
770
+ workspace_name,
771
+ starterkit,
772
+ user_token,
773
+ fork,
774
+ organization_id=organization_id,
775
+ organization_name=organization_name,
776
+ )
749
777
 
750
778
 
751
779
  async def create_workspace_branch(
@@ -2080,3 +2108,99 @@ def get_ca_pem_content(ca_pem: Optional[str], filename: Optional[str] = None) ->
2080
2108
  raise CLIConnectionException(FeedbackManager.error_connection_invalid_ca_pem())
2081
2109
 
2082
2110
  return ca_pem_content
2111
+
2112
+
2113
+ async def get_organizations_by_user(ctx: Context, user_token: str) -> List[Dict[str, Any]]:
2114
+ """Fetches all organizations by user using the provided user token"""
2115
+ client: TinyB = ctx.ensure_object(dict)["client"]
2116
+ organizations = []
2117
+
2118
+ try:
2119
+ user_client: TinyB = deepcopy(client)
2120
+ user_client.token = user_token
2121
+ user_workspaces = await user_client.user_workspaces_with_organization()
2122
+ admin_org_id = user_workspaces.get("organization_id")
2123
+ seen_org_ids = set()
2124
+
2125
+ for workspace in user_workspaces.get("workspaces"):
2126
+ org = workspace.get("organization")
2127
+ if org and org.get("id") not in seen_org_ids:
2128
+ org["is_admin"] = org.get("id") == admin_org_id
2129
+ organizations.append(org)
2130
+ seen_org_ids.add(org.get("id"))
2131
+
2132
+ # Case: user is admin of an organization but not a member of any workspace in it
2133
+ if admin_org_id and admin_org_id not in seen_org_ids:
2134
+ org = await user_client.organization(admin_org_id)
2135
+ org["id"] = admin_org_id
2136
+ org["is_admin"] = True
2137
+ organizations.append(org)
2138
+
2139
+ except Exception as e:
2140
+ raise CLIWorkspaceException(FeedbackManager.error_while_fetching_orgs(error=str(e)))
2141
+ return organizations
2142
+
2143
+
2144
+ OrgType = Literal["tinybird", "domain", "admin", "member"]
2145
+
2146
+
2147
+ class Organization(TypedDict):
2148
+ id: str
2149
+ name: str
2150
+ role: str
2151
+ domains: Optional[List[str]]
2152
+ type: OrgType
2153
+
2154
+
2155
+ def sort_organizations_by_user(organizations: List[Dict[str, Any]], user_email: Optional[str]) -> List[Organization]:
2156
+ """Sort organizations based on type: tinybird > domain > admin > member"""
2157
+ sorted_organizations: List[Organization] = []
2158
+ user_domain = user_email.split("@")[1] if user_email else None
2159
+ is_tinybird_user = user_domain == "tinybird.co"
2160
+
2161
+ for org in organizations:
2162
+ domain = org.get("domain") or ""
2163
+ domains = domain.split(",") if domain else None
2164
+ role: OrgType = "admin" if org.get("is_admin") else "member"
2165
+ type = role
2166
+ if domains and user_domain and user_domain in domains:
2167
+ type = "domain"
2168
+ if org.get("name") == "Tinybird" and is_tinybird_user:
2169
+ type = "tinybird"
2170
+
2171
+ sorted_organizations.append(
2172
+ {
2173
+ "id": org.get("id") or "",
2174
+ "name": org.get("name") or "",
2175
+ "role": role,
2176
+ "domains": [domain.strip() for domain in domains] if domains else None,
2177
+ "type": type,
2178
+ }
2179
+ )
2180
+
2181
+ type_priority: Dict[OrgType, int] = {"tinybird": 0, "domain": 1, "admin": 2, "member": 3}
2182
+
2183
+ sorted_organizations.sort(key=lambda x: type_priority[x["type"]])
2184
+
2185
+ return sorted_organizations
2186
+
2187
+
2188
+ async def ask_for_organization_interactively(organizations: List[Organization]) -> Optional[Organization]:
2189
+ rows = [(index + 1, org["name"], org["role"], org["id"]) for index, org in enumerate(organizations)]
2190
+
2191
+ echo_safe_humanfriendly_tables_format_smart_table(rows, column_names=["Idx", "Name", "Role", "Id"])
2192
+ click.echo("")
2193
+ click.echo(" [0] to cancel")
2194
+
2195
+ org_index = -1
2196
+ while org_index == -1:
2197
+ org_index = click.prompt("\nSelect an organization to include the workspace in", default=1)
2198
+ if org_index < 0 or org_index > len(organizations):
2199
+ click.echo(FeedbackManager.error_organization_index(organization_index=org_index))
2200
+ org_index = -1
2201
+
2202
+ if org_index == 0:
2203
+ click.echo(FeedbackManager.info_cancelled_by_user())
2204
+ return None
2205
+
2206
+ return organizations[org_index - 1]
@@ -259,6 +259,9 @@ class CLIConfig:
259
259
  def get_user_client(self, host: Optional[str] = None) -> tbc.TinyB:
260
260
  return self.get_client(self.get_user_token(), host)
261
261
 
262
+ def get_user_email(self) -> Optional[str]:
263
+ return self["user_email"]
264
+
262
265
  def set_workspace_token(self, workspace_id: str, token: str) -> None:
263
266
  pass
264
267
 
@@ -15,6 +15,7 @@ from tinybird.feedback_manager import FeedbackManager
15
15
  from tinybird.tb_cli_modules.cli import cli
16
16
  from tinybird.tb_cli_modules.common import (
17
17
  _get_workspace_plan_name,
18
+ ask_for_organization_interactively,
18
19
  ask_for_user_token,
19
20
  check_user_token,
20
21
  coro,
@@ -22,8 +23,10 @@ from tinybird.tb_cli_modules.common import (
22
23
  create_workspace_non_interactive,
23
24
  echo_safe_humanfriendly_tables_format_smart_table,
24
25
  get_current_main_workspace,
26
+ get_organizations_by_user,
25
27
  is_valid_starterkit,
26
28
  print_current_workspace,
29
+ sort_organizations_by_user,
27
30
  switch_workspace,
28
31
  )
29
32
  from tinybird.tb_cli_modules.config import CLIConfig
@@ -193,29 +196,69 @@ async def clear_workspace(ctx: Context, yes: bool, dry_run: bool) -> None:
193
196
  default=False,
194
197
  help="When enabled, we will share all datasource from the current workspace to the new created one",
195
198
  )
199
+ @click.option(
200
+ "--organization-id",
201
+ "organization_id",
202
+ type=str,
203
+ required=False,
204
+ help="When passed, the workspace will be created in the specified organization",
205
+ )
196
206
  @click.pass_context
197
207
  @coro
198
208
  async def create_workspace(
199
- ctx: Context, workspace_name: str, starter_kit: str, user_token: Optional[str], fork: bool
209
+ ctx: Context,
210
+ workspace_name: str,
211
+ starter_kit: str,
212
+ user_token: Optional[str],
213
+ fork: bool,
214
+ organization_id: Optional[str],
200
215
  ) -> None:
201
216
  if starter_kit and not await is_valid_starterkit(ctx, starter_kit):
202
217
  raise CLIWorkspaceException(FeedbackManager.error_starterkit_name(starterkit_name=starter_kit))
203
218
 
219
+ config = CLIConfig.get_project_config()
220
+ host = config.get_host() or CLIConfig.DEFAULTS["host"]
221
+ ui_host = get_display_host(host)
222
+
204
223
  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
224
  user_token = ask_for_user_token("create a new workspace", ui_host)
209
225
  if not user_token:
210
226
  return
211
227
  await check_user_token(ctx, user_token)
212
228
 
229
+ organization_name = None
230
+ organizations = await get_organizations_by_user(ctx, user_token)
231
+
232
+ if organization_id:
233
+ organization = next((org for org in organizations if org.get("id") == organization_id), None)
234
+ if not organization:
235
+ raise CLIWorkspaceException(FeedbackManager.error_organization_not_found(organization_id=organization_id))
236
+ organization_name = organization.get("name")
237
+ else:
238
+ if len(organizations) == 0:
239
+ click.echo(FeedbackManager.warning_none_organization(ui_host=ui_host))
240
+ elif len(organizations) == 1:
241
+ organization_id = organizations[0].get("id")
242
+ organization_name = organizations[0].get("name")
243
+ else:
244
+ sorted_organizations = sort_organizations_by_user(organizations, user_email=config.get_user_email())
245
+ current_organization = await ask_for_organization_interactively(sorted_organizations)
246
+ if current_organization:
247
+ organization_id = current_organization.get("id")
248
+ organization_name = current_organization.get("name")
249
+ else:
250
+ return
251
+
213
252
  # If we have at least workspace_name, we start the non interactive
214
253
  # process, creating an empty workspace
215
254
  if workspace_name:
216
- await create_workspace_non_interactive(ctx, workspace_name, starter_kit, user_token, fork)
255
+ await create_workspace_non_interactive(
256
+ ctx, workspace_name, starter_kit, user_token, fork, organization_id, organization_name
257
+ )
217
258
  else:
218
- await create_workspace_interactive(ctx, workspace_name, starter_kit, user_token, fork)
259
+ await create_workspace_interactive(
260
+ ctx, workspace_name, starter_kit, user_token, fork, organization_id, organization_name
261
+ )
219
262
 
220
263
 
221
264
  @workspace.command(name="delete", short_help="Delete a Workspace for your Tinybird user")
@@ -1,15 +1,15 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: tinybird_cli
3
- Version: 5.15.2.dev1
3
+ Version: 5.16.1.dev0
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli
6
6
  Author: Tinybird
7
7
  Author-email: support@tinybird.co
8
- Requires-Python: >=3.9, <3.13
8
+ Requires-Python: >=3.9, <3.14
9
9
  Description-Content-Type: text/x-rst
10
10
  Requires-Dist: aiofiles==24.1.0
11
- Requires-Dist: click<8.2,>=8.1.6
12
- Requires-Dist: clickhouse-toolset==0.33.dev0
11
+ Requires-Dist: clickhouse-toolset==0.34.dev0
12
+ Requires-Dist: click<8.2,>=8.1.8
13
13
  Requires-Dist: colorama==0.4.6
14
14
  Requires-Dist: cryptography~=41.0.0
15
15
  Requires-Dist: croniter==1.3.15
@@ -61,11 +61,16 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
61
61
  Changelog
62
62
  ----------
63
63
 
64
- 5.15.2.dev1
64
+ 5.16.0
65
65
  ***********
66
66
 
67
+ - `Added` support for organizations on `tb workspace create`
68
+ - `Added` support for Python 3.13
67
69
  - `Updated` dependencies:
68
70
  - croniter: from 1.3.8 to 1.3.15
71
+ - clickhouse-driver: from 0.2.7 to 0.2.9
72
+ - clickhouse-toolset: from 0.33.dev0 to 0.34.dev0
73
+ - click: from 8.1.7 to 8.1.8
69
74
 
70
75
  5.15.1
71
76
  ***********
@@ -1,6 +1,6 @@
1
1
  aiofiles==24.1.0
2
- click<8.2,>=8.1.6
3
- clickhouse-toolset==0.33.dev0
2
+ clickhouse-toolset==0.34.dev0
3
+ click<8.2,>=8.1.8
4
4
  colorama==0.4.6
5
5
  cryptography~=41.0.0
6
6
  croniter==1.3.15