tinybird 0.0.1.dev117__py3-none-any.whl → 0.0.1.dev119__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/tb/__cli__.py +2 -2
  2. tinybird/{client.py → tb/client.py} +1 -1
  3. tinybird/tb/config.py +96 -0
  4. tinybird/tb/modules/auth.py +1 -1
  5. tinybird/tb/modules/build.py +1 -1
  6. tinybird/tb/modules/cli.py +2 -73
  7. tinybird/tb/modules/common.py +114 -43
  8. tinybird/tb/modules/config.py +2 -2
  9. tinybird/tb/modules/connection.py +45 -13
  10. tinybird/tb/modules/copy.py +1 -1
  11. tinybird/tb/modules/create.py +10 -9
  12. tinybird/tb/modules/datafile/build.py +1 -1
  13. tinybird/tb/modules/datafile/build_common.py +1 -1
  14. tinybird/tb/modules/datafile/build_datasource.py +1 -1
  15. tinybird/tb/modules/datafile/build_pipe.py +1 -1
  16. tinybird/tb/modules/datafile/diff.py +1 -1
  17. tinybird/tb/modules/datafile/format_datasource.py +1 -1
  18. tinybird/tb/modules/datafile/playground.py +1 -1
  19. tinybird/tb/modules/datafile/pull.py +1 -1
  20. tinybird/tb/modules/datasource.py +1 -1
  21. tinybird/tb/modules/deployment.py +19 -15
  22. tinybird/tb/modules/endpoint.py +1 -1
  23. tinybird/tb/modules/feedback_manager.py +92 -5
  24. tinybird/tb/modules/infra.py +1 -1
  25. tinybird/tb/modules/job.py +1 -1
  26. tinybird/tb/modules/local_common.py +1 -1
  27. tinybird/tb/modules/login.py +10 -0
  28. tinybird/tb/modules/materialization.py +1 -1
  29. tinybird/tb/modules/mock.py +1 -1
  30. tinybird/tb/modules/pipe.py +1 -1
  31. tinybird/tb/modules/secret.py +1 -1
  32. tinybird/tb/modules/shell.py +1 -1
  33. tinybird/tb/modules/telemetry.py +1 -1
  34. tinybird/tb/modules/test.py +1 -1
  35. tinybird/tb/modules/tinyunit/tinyunit.py +1 -1
  36. tinybird/tb/modules/token.py +1 -1
  37. tinybird/tb/modules/workspace.py +1 -1
  38. tinybird/tb/modules/workspace_members.py +2 -2
  39. {tinybird-0.0.1.dev117.dist-info → tinybird-0.0.1.dev119.dist-info}/METADATA +1 -1
  40. {tinybird-0.0.1.dev117.dist-info → tinybird-0.0.1.dev119.dist-info}/RECORD +43 -44
  41. tinybird/__cli__.py +0 -7
  42. tinybird/config.py +0 -142
  43. {tinybird-0.0.1.dev117.dist-info → tinybird-0.0.1.dev119.dist-info}/WHEEL +0 -0
  44. {tinybird-0.0.1.dev117.dist-info → tinybird-0.0.1.dev119.dist-info}/entry_points.txt +0 -0
  45. {tinybird-0.0.1.dev117.dist-info → tinybird-0.0.1.dev119.dist-info}/top_level.txt +0 -0
tinybird/tb/__cli__.py CHANGED
@@ -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__ = '0.0.1.dev117'
8
- __revision__ = 'f1a78c6'
7
+ __version__ = '0.0.1.dev119'
8
+ __revision__ = 'bbe4729'
@@ -15,7 +15,7 @@ from urllib3 import Retry
15
15
 
16
16
  from tinybird.ch_utils.constants import COPY_ENABLED_TABLE_FUNCTIONS
17
17
  from tinybird.syncasync import sync_to_async
18
- from tinybird.tb_cli_modules.telemetry import add_telemetry_event
18
+ from tinybird.tb.modules.telemetry import add_telemetry_event
19
19
 
20
20
  HOST = "https://api.tinybird.co"
21
21
  LIMIT_RETRIES = 10
tinybird/tb/config.py ADDED
@@ -0,0 +1,96 @@
1
+ import json
2
+ from os import environ, getcwd
3
+ from pathlib import Path
4
+ from typing import Any, Dict, Optional
5
+
6
+ import aiofiles
7
+ import click
8
+
9
+ from tinybird.tb import __cli__
10
+ from tinybird.tb.modules.feedback_manager import FeedbackManager
11
+
12
+ try:
13
+ from tinybird.tb.__cli__ import __revision__
14
+ except Exception:
15
+ __revision__ = ""
16
+
17
+ DEFAULT_API_HOST = "https://api.tinybird.co"
18
+ DEFAULT_LOCALHOST = "http://localhost:8001"
19
+ CURRENT_VERSION = f"{__cli__.__version__}"
20
+ VERSION = f"{__cli__.__version__} (rev {__revision__})"
21
+ DEFAULT_UI_HOST = "https://cloud.tinybird.co"
22
+ SUPPORTED_CONNECTORS = ["bigquery", "snowflake"]
23
+ PROJECT_PATHS = ["datasources", "datasources/fixtures", "endpoints", "pipes", "tests", "scripts", "deploy"]
24
+ DEPRECATED_PROJECT_PATHS = ["endpoints"]
25
+ MIN_WORKSPACE_ID_LENGTH = 36
26
+
27
+ CLOUD_HOSTS = {
28
+ "https://api.tinybird.co": "https://cloud.tinybird.co/gcp/europe-west3",
29
+ "https://api.us-east.tinybird.co": "https://cloud.tinybird.co/gcp/us-east4",
30
+ "https://api.us-east.aws.tinybird.co": "https://cloud.tinybird.co/aws/us-east-1",
31
+ "https://api.us-west-2.aws.tinybird.co": "https://cloud.tinybird.co/aws/us-west-2",
32
+ "https://api.eu-central-1.aws.tinybird.co": "https://cloud.tinybird.co/aws/eu-central-1",
33
+ "https://api.eu-west-1.aws.tinybird.co": "https://cloud.tinybird.co/aws/eu-west-1",
34
+ "https://api.europe-west2.gcp.tinybird.co": "https://cloud.tinybird.co/gcp/europe-west2",
35
+ "https://api.ap-east.aws.tinybird.co": "https://cloud.tinybird.co/aws/ap-east",
36
+ "https://ui.tinybird.co": "https://cloud.tinybird.co/gcp/europe-west3",
37
+ "https://ui.us-east.tinybird.co": "https://cloud.tinybird.co/gcp/us-east4",
38
+ "https://ui.us-east.aws.tinybird.co": "https://cloud.tinybird.co/aws/us-east-1",
39
+ "https://ui.us-west-2.aws.tinybird.co": "https://cloud.tinybird.co/aws/us-west-2",
40
+ "https://ui.eu-central-1.aws.tinybird.co": "https://cloud.tinybird.co/aws/eu-central-1",
41
+ "https://ui.europe-west2.gcp.tinybird.co": "https://cloud.tinybird.co/gcp/europe-west2",
42
+ }
43
+
44
+
45
+ async def get_config(
46
+ host: str, token: Optional[str], semver: Optional[str] = None, config_file: Optional[str] = None
47
+ ) -> Dict[str, Any]:
48
+ if host:
49
+ host = host.rstrip("/")
50
+
51
+ config = {}
52
+ try:
53
+ async with aiofiles.open(config_file or Path(getcwd()) / ".tinyb") as file:
54
+ res = await file.read()
55
+ config = json.loads(res)
56
+ except OSError:
57
+ pass
58
+ except json.decoder.JSONDecodeError:
59
+ click.echo(FeedbackManager.error_load_file_config(config_file=config_file))
60
+ return config
61
+
62
+ config["token_passed"] = token
63
+ config["token"] = token or config.get("token", None)
64
+ config["semver"] = semver or config.get("semver", None)
65
+ config["host"] = host or config.get("host", DEFAULT_API_HOST)
66
+ config["workspaces"] = config.get("workspaces", [])
67
+ config["cwd"] = config.get("cwd", getcwd())
68
+ return config
69
+
70
+
71
+ async def write_config(config: Dict[str, Any], dest_file: str = ".tinyb"):
72
+ config_file = Path(getcwd()) / dest_file
73
+ async with aiofiles.open(config_file, "w") as file:
74
+ await file.write(json.dumps(config, indent=4, sort_keys=True))
75
+
76
+
77
+ def get_display_cloud_host(api_host: str) -> str:
78
+ is_local = "localhost" in api_host
79
+ if is_local:
80
+ port = api_host.split(":")[-1]
81
+ return f"http://cloud.tinybird.co/local/{port}"
82
+ return CLOUD_HOSTS.get(api_host, api_host)
83
+
84
+
85
+ class FeatureFlags:
86
+ @classmethod
87
+ def ignore_sql_errors(cls) -> bool: # Context: #1155
88
+ return "TB_IGNORE_SQL_ERRORS" in environ
89
+
90
+ @classmethod
91
+ def is_localhost(cls) -> bool:
92
+ return "SET_LOCALHOST" in environ
93
+
94
+ @classmethod
95
+ def enable_snowflake_connector_command(cls) -> bool:
96
+ return "ENABLE_SNOWFLAKE_CONNECTOR_COMMAND" in environ
@@ -9,7 +9,7 @@ from typing import Any, Dict, List, Optional
9
9
  import click
10
10
  import humanfriendly.tables
11
11
 
12
- from tinybird.config import get_display_cloud_host
12
+ from tinybird.tb.config import get_display_cloud_host
13
13
  from tinybird.tb.modules.cli import cli
14
14
  from tinybird.tb.modules.common import (
15
15
  configure_connector,
@@ -14,7 +14,7 @@ import click
14
14
  import requests
15
15
 
16
16
  import tinybird.context as context
17
- from tinybird.client import TinyB
17
+ from tinybird.tb.client import TinyB
18
18
  from tinybird.tb.modules.cli import cli
19
19
  from tinybird.tb.modules.common import push_data
20
20
  from tinybird.tb.modules.config import CLIConfig
@@ -17,25 +17,22 @@ import click
17
17
  import humanfriendly
18
18
  from click import Context
19
19
 
20
- from tinybird.client import (
20
+ from tinybird.tb import __cli__
21
+ from tinybird.tb.client import (
21
22
  AuthException,
22
23
  AuthNoTokenException,
23
- TinyB,
24
24
  )
25
- from tinybird.tb import __cli__
26
25
  from tinybird.tb.modules.common import (
27
26
  CatchAuthExceptions,
28
27
  CLIException,
29
28
  _get_tb_client,
30
29
  coro,
31
30
  echo_safe_format_table,
32
- get_current_main_workspace,
33
31
  getenv_bool,
34
32
  try_update_config_with_remote,
35
33
  )
36
34
  from tinybird.tb.modules.config import CLIConfig
37
35
  from tinybird.tb.modules.datafile.build import build_graph
38
- from tinybird.tb.modules.datafile.diff import diff_command
39
36
  from tinybird.tb.modules.datafile.pull import folder_pull
40
37
  from tinybird.tb.modules.feedback_manager import FeedbackManager
41
38
  from tinybird.tb.modules.local_common import get_tinybird_local_client
@@ -165,74 +162,6 @@ async def pull(ctx: Context, force: bool, fmt: bool) -> None:
165
162
  return await folder_pull(client, project.path, force, fmt=fmt)
166
163
 
167
164
 
168
- @cli.command(
169
- name="diff",
170
- short_help="Diff local datafiles to the corresponding remote files in the workspace. Only diffs VERSION and SCHEMA for .datasource files.",
171
- )
172
- @click.argument("filename", type=click.Path(exists=True), nargs=-1, required=False)
173
- @click.option(
174
- "--fmt/--no-fmt",
175
- is_flag=True,
176
- default=True,
177
- help="Format files before doing the diff, default is True so both files match the format",
178
- )
179
- @click.option("--no-color", is_flag=True, default=False, help="Don't colorize diff")
180
- @click.option(
181
- "--no-verbose", is_flag=True, default=False, help="List the resources changed not the content of the diff"
182
- )
183
- @click.option(
184
- "--main",
185
- is_flag=True,
186
- default=False,
187
- help="Diff local datafiles to the corresponding remote files in the main workspace. Only works when authenticated on a Branch.",
188
- hidden=True,
189
- )
190
- @click.pass_context
191
- @coro
192
- async def diff(
193
- ctx: Context, filename: Optional[Tuple], fmt: bool, no_color: bool, no_verbose: bool, main: bool
194
- ) -> None:
195
- only_resources_changed = no_verbose
196
- client: TinyB = ctx.ensure_object(dict)["client"]
197
-
198
- if not main:
199
- changed = await diff_command(
200
- list(filename) if filename else None, fmt, client, no_color, with_print=not only_resources_changed
201
- )
202
- else:
203
- config = CLIConfig.get_project_config()
204
-
205
- response = await client.user_workspaces_and_branches(version="v1")
206
- ws_client = None
207
- for workspace in response["workspaces"]:
208
- if config["id"] == workspace["id"]:
209
- if not workspace.get("is_branch"):
210
- raise CLIException(FeedbackManager.error_not_a_branch())
211
-
212
- origin = workspace["main"]
213
- workspace = await get_current_main_workspace(config)
214
-
215
- if not workspace:
216
- raise CLIException(FeedbackManager.error_workspace(workspace=origin))
217
-
218
- ws_client = _get_tb_client(workspace["token"], config["host"])
219
- break
220
-
221
- if not ws_client:
222
- raise CLIException(FeedbackManager.error_workspace(workspace=origin))
223
- changed = await diff_command(
224
- list(filename) if filename else None, fmt, ws_client, no_color, with_print=not only_resources_changed
225
- )
226
-
227
- if only_resources_changed:
228
- click.echo("\n")
229
- for resource, status in dict(sorted(changed.items(), key=lambda item: str(item[1]))).items():
230
- if status is None:
231
- continue
232
- status = "changed" if status not in ["remote", "local", "shared"] else status
233
- click.echo(f"{status}: {resource}")
234
-
235
-
236
165
  @cli.command()
237
166
  @click.argument("query", required=False)
238
167
  @click.option("--rows_limit", default=100, help="Max number of rows retrieved")
@@ -35,7 +35,7 @@ from humanfriendly.tables import format_pretty_table
35
35
  from packaging.version import Version
36
36
  from thefuzz import process
37
37
 
38
- from tinybird.client import (
38
+ from tinybird.tb.client import (
39
39
  AuthException,
40
40
  AuthNoTokenException,
41
41
  DoesNotExistException,
@@ -43,7 +43,7 @@ from tinybird.client import (
43
43
  OperationCanNotBePerformed,
44
44
  TinyB,
45
45
  )
46
- from tinybird.config import (
46
+ from tinybird.tb.config import (
47
47
  DEFAULT_API_HOST,
48
48
  DEFAULT_UI_HOST,
49
49
  SUPPORTED_CONNECTORS,
@@ -518,7 +518,10 @@ def get_region_info(ctx, region=None):
518
518
  api_host = format_host(
519
519
  region["api_host"] if region else ctx.obj["config"].get("host", DEFAULT_API_HOST), subdomain="api"
520
520
  )
521
- ui_host = format_host(region["host"] if region else ctx.obj["config"].get("host", DEFAULT_UI_HOST), subdomain="ui")
521
+ ui_host = format_host(
522
+ region["host"] if region else ctx.obj["config"].get("host", DEFAULT_UI_HOST), subdomain="cloud"
523
+ )
524
+
522
525
  return name, api_host, ui_host
523
526
 
524
527
 
@@ -553,13 +556,13 @@ def format_host(host: str, subdomain: Optional[str] = None) -> str:
553
556
  if subdomain and not is_localhost:
554
557
  url_info = urlparse(host)
555
558
  current_subdomain = url_info.netloc.split(".")[0]
556
- if current_subdomain in ("api", "ui"):
559
+ if current_subdomain in ("api", "ui", "app", "cloud"):
557
560
  host = host.replace(current_subdomain, subdomain)
558
561
  if "localhost" in host or is_localhost:
559
562
  host = f"http://{host}" if "http" not in host else host
560
563
  elif not host.startswith("http"):
561
564
  host = f"https://{host}"
562
- return host
565
+ return host.replace("app.tinybird.co", "cloud.tinybird.co")
563
566
 
564
567
 
565
568
  def region_from_host(region_name_or_host, regions):
@@ -1506,7 +1509,7 @@ async def try_authenticate(
1506
1509
  ui_host: str
1507
1510
  token: Optional[str]
1508
1511
  if host and not selected_region:
1509
- name, api_host, ui_host = (host, format_host(host, subdomain="api"), format_host(host, subdomain="ui"))
1512
+ name, api_host, ui_host = (host, format_host(host, subdomain="api"), format_host(host, subdomain="cloud"))
1510
1513
  token = config.get_token()
1511
1514
  else:
1512
1515
  name, api_host, ui_host = get_region_info(config, selected_region)
@@ -1754,27 +1757,26 @@ async def remove_release(
1754
1757
  async def run_aws_iamrole_connection_flow(
1755
1758
  client: TinyB,
1756
1759
  service: str,
1757
- policy: str = "read",
1758
- ):
1760
+ environment: str,
1761
+ ) -> Tuple[str, str, str]:
1759
1762
  if service == DataConnectorType.AMAZON_DYNAMODB:
1760
- raise NotImplementedError("DynamoDB is not supported yet")
1763
+ raise NotImplementedError("DynamoDB is not supported")
1761
1764
 
1762
- resource_type = "table" if service == DataConnectorType.AMAZON_DYNAMODB else "bucket"
1763
- resource_name = click.prompt(f"Enter the name of the {resource_type}")
1764
- validate_string_connector_param(resource_type.capitalize(), resource_name)
1765
+ bucket_name = click.prompt(
1766
+ "📦 Bucket name (specific name recommended, use '*' for unrestricted access in IAM policy)",
1767
+ prompt_suffix="\n> ",
1768
+ )
1769
+ validate_string_connector_param("Bucket", bucket_name)
1765
1770
 
1766
- resource_region = click.prompt(f"Enter the region where the {resource_type} is located")
1767
- validate_string_connector_param("Region", resource_region)
1771
+ region = click.prompt("🌐 Region (the region where the bucket is located, e.g. 'us-east-1')", prompt_suffix="\n> ")
1772
+ validate_string_connector_param("Region", region)
1768
1773
 
1769
- access_policy, trust_policy, external_id = await get_aws_iamrole_policies(client, service=service, policy=policy)
1770
- access_policy = access_policy.replace("<bucket>", resource_name)
1774
+ access_policy, trust_policy, _ = await get_aws_iamrole_policies(client, service=service, policy="read")
1775
+ access_policy = access_policy.replace("<bucket>", bucket_name)
1771
1776
 
1772
- if not click.confirm(
1773
- FeedbackManager.prompt_s3_iamrole_connection_login_aws(),
1774
- show_default=False,
1775
- prompt_suffix="Press y to continue:",
1776
- ):
1777
- sys.exit(1)
1777
+ click.echo(FeedbackManager.prompt_s3_iamrole_connection_login_aws())
1778
+ click.echo(FeedbackManager.click_enter_to_continue())
1779
+ input()
1778
1780
 
1779
1781
  access_policy_copied = True
1780
1782
  try:
@@ -1782,16 +1784,58 @@ async def run_aws_iamrole_connection_flow(
1782
1784
  except Exception:
1783
1785
  access_policy_copied = False
1784
1786
 
1785
- if not click.confirm(
1787
+ # Display message and wait for user to press Enter
1788
+ click.echo(
1789
+ FeedbackManager.prompt_s3_iamrole_connection_policy(
1790
+ access_policy=access_policy, aws_region=region, bucket=bucket_name
1791
+ )
1792
+ if access_policy_copied
1793
+ else FeedbackManager.prompt_s3_iamrole_connection_policy_not_copied(
1794
+ access_policy=access_policy, aws_region=region, bucket=bucket_name
1795
+ )
1796
+ )
1797
+ click.echo(FeedbackManager.click_enter_to_continue())
1798
+ input()
1799
+
1800
+ trust_policy_copied = True
1801
+ try:
1802
+ pyperclip.copy(trust_policy)
1803
+ except Exception:
1804
+ trust_policy_copied = False
1805
+
1806
+ role_arn = click.prompt(
1786
1807
  (
1787
- FeedbackManager.prompt_s3_iamrole_connection_policy(access_policy=access_policy)
1788
- if access_policy_copied
1789
- else FeedbackManager.prompt_s3_iamrole_connection_policy_not_copied(access_policy=access_policy)
1808
+ FeedbackManager.prompt_s3_iamrole_connection_role(
1809
+ trust_policy=trust_policy,
1810
+ aws_region=region,
1811
+ bucket=bucket_name,
1812
+ environment=environment.upper(),
1813
+ step="3",
1814
+ )
1815
+ if trust_policy_copied
1816
+ else FeedbackManager.prompt_s3_iamrole_connection_role_not_copied(
1817
+ trust_policy=trust_policy,
1818
+ aws_region=region,
1819
+ bucket=bucket_name,
1820
+ environment=environment.upper(),
1821
+ step="3",
1822
+ )
1790
1823
  ),
1791
1824
  show_default=False,
1792
- prompt_suffix="Press y to continue:",
1793
- ):
1794
- sys.exit(1)
1825
+ )
1826
+ validate_string_connector_param("Role ARN", role_arn)
1827
+
1828
+ return role_arn, region, bucket_name
1829
+
1830
+
1831
+ async def production_aws_iamrole_only(
1832
+ prod_client: TinyB,
1833
+ service: str,
1834
+ region: str,
1835
+ bucket_name: str,
1836
+ environment: str,
1837
+ ) -> Tuple[str, str, str]:
1838
+ _, trust_policy, external_id = await get_aws_iamrole_policies(prod_client, service=service, policy="read")
1795
1839
 
1796
1840
  trust_policy_copied = True
1797
1841
  try:
@@ -1799,21 +1843,29 @@ async def run_aws_iamrole_connection_flow(
1799
1843
  except Exception:
1800
1844
  trust_policy_copied = False
1801
1845
 
1802
- if not click.confirm(
1846
+ role_arn = click.prompt(
1803
1847
  (
1804
- FeedbackManager.prompt_s3_iamrole_connection_role(trust_policy=trust_policy)
1848
+ FeedbackManager.prompt_s3_iamrole_connection_role(
1849
+ trust_policy=trust_policy,
1850
+ aws_region=region,
1851
+ bucket=bucket_name,
1852
+ environment=environment.upper(),
1853
+ step="4",
1854
+ )
1805
1855
  if trust_policy_copied
1806
- else FeedbackManager.prompt_s3_iamrole_connection_role_not_copied(trust_policy=trust_policy)
1856
+ else FeedbackManager.prompt_s3_iamrole_connection_role_not_copied(
1857
+ trust_policy=trust_policy,
1858
+ aws_region=region,
1859
+ bucket=bucket_name,
1860
+ environment=environment.upper(),
1861
+ step="4",
1862
+ )
1807
1863
  ),
1808
1864
  show_default=False,
1809
- prompt_suffix="Press y to continue:",
1810
- ):
1811
- sys.exit(1)
1812
-
1813
- role_arn = click.prompt("Enter the ARN of the role you just created")
1865
+ )
1814
1866
  validate_string_connector_param("Role ARN", role_arn)
1815
1867
 
1816
- return role_arn, resource_region, external_id
1868
+ return role_arn, region, external_id
1817
1869
 
1818
1870
 
1819
1871
  async def get_aws_iamrole_policies(client: TinyB, service: str, policy: str = "write"):
@@ -1846,16 +1898,35 @@ async def get_aws_iamrole_policies(client: TinyB, service: str, policy: str = "w
1846
1898
  return json.dumps(access_policy, indent=4), json.dumps(trust_policy, indent=4), external_id
1847
1899
 
1848
1900
 
1849
- async def validate_aws_iamrole_connection_name(client: TinyB) -> str:
1901
+ def get_s3_connection_name(project_folder: str) -> str:
1850
1902
  connection_name = None
1903
+ valid_pattern = r"^[a-zA-Z][a-zA-Z0-9_]*$"
1904
+
1851
1905
  while not connection_name:
1852
- connection_name = click.prompt("Enter the name for this connection", default=None, show_default=False)
1906
+ connection_name = click.prompt(
1907
+ "🔗 Enter a name for your new Tinybird S3 connection (use alphanumeric characters, and underscores)",
1908
+ prompt_suffix="\n> ",
1909
+ show_default=True,
1910
+ )
1853
1911
  assert isinstance(connection_name, str)
1854
1912
 
1855
- if await client.get_connector(connection_name) is not None:
1856
- click.echo(FeedbackManager.info_connection_already_exists(name=connection_name))
1913
+ # Validate against invalid characters
1914
+ if not re.match(valid_pattern, connection_name):
1915
+ if not connection_name[0].isalpha():
1916
+ click.echo("Error: Connection name must start with a letter.")
1917
+ else:
1918
+ click.echo("Error: Connection name can only contain letters, numbers, and underscores.")
1857
1919
  connection_name = None
1858
- assert isinstance(connection_name, str)
1920
+ continue
1921
+
1922
+ # Check for existing connection with the same name
1923
+ project_folder_path = Path(project_folder)
1924
+ connection_files = list(project_folder_path.glob("*.connection"))
1925
+ for conn_file in connection_files:
1926
+ if conn_file.stem == connection_name:
1927
+ click.echo(FeedbackManager.error_connection_file_already_exists(name=f"{connection_name}.connection"))
1928
+ connection_name = None
1929
+ break
1859
1930
  return connection_name
1860
1931
 
1861
1932
 
@@ -8,8 +8,8 @@ from urllib.parse import urlparse
8
8
 
9
9
  from packaging import version
10
10
 
11
- import tinybird.client as tbc
12
- from tinybird.config import CURRENT_VERSION, DEFAULT_API_HOST, DEFAULT_LOCALHOST
11
+ import tinybird.tb.client as tbc
12
+ from tinybird.tb.config import CURRENT_VERSION, DEFAULT_API_HOST, DEFAULT_LOCALHOST
13
13
 
14
14
  APP_CONFIG_NAME = "tinybird"
15
15
 
@@ -3,22 +3,24 @@
3
3
  # - If it makes sense and only when strictly necessary, you can create utility functions in this file.
4
4
  # - But please, **do not** interleave utility functions and command definitions.
5
5
 
6
+ import uuid
6
7
  from typing import Any, Dict, List, Optional
7
8
 
8
9
  import click
9
10
  from click import Context
10
11
 
11
- from tinybird.client import TinyB
12
+ from tinybird.tb.client import TinyB
12
13
  from tinybird.tb.modules.cli import cli
13
14
  from tinybird.tb.modules.common import (
14
15
  DataConnectorType,
15
16
  _get_setting_value,
16
17
  coro,
17
18
  echo_safe_humanfriendly_tables_format_smart_table,
19
+ get_s3_connection_name,
20
+ production_aws_iamrole_only,
18
21
  run_aws_iamrole_connection_flow,
19
- validate_aws_iamrole_connection_name,
20
22
  )
21
- from tinybird.tb.modules.create import generate_aws_iamrole_connection_file
23
+ from tinybird.tb.modules.create import generate_aws_iamrole_connection_file_with_secret
22
24
  from tinybird.tb.modules.feedback_manager import FeedbackManager
23
25
  from tinybird.tb.modules.project import Project
24
26
 
@@ -148,20 +150,50 @@ async def connection_create_s3(ctx: Context) -> None:
148
150
  project: Project = ctx.ensure_object(dict)["project"]
149
151
  obj: Dict[str, Any] = ctx.ensure_object(dict)
150
152
  client: TinyB = obj["client"]
153
+
151
154
  service = DataConnectorType.AMAZON_S3
152
- connection_name = await validate_aws_iamrole_connection_name(client)
153
- role_arn, region, external_id = await run_aws_iamrole_connection_flow(
155
+ click.echo(FeedbackManager.prompt_s3_connection_header())
156
+ connection_name = get_s3_connection_name(project.folder)
157
+ role_arn, region, bucket_name = await run_aws_iamrole_connection_flow(
154
158
  client,
155
159
  service=service,
156
- policy="read", # For now only read since we only support import from S3
160
+ environment=obj["env"],
161
+ )
162
+ unique_suffix = uuid.uuid4().hex[:8] # Use first 8 chars of a UUID for brevity
163
+ secret_name = f"s3_role_arn_{connection_name}_{unique_suffix}"
164
+ await client.create_secret(name=secret_name, value=role_arn)
165
+
166
+ create_in_cloud = (
167
+ click.confirm(FeedbackManager.prompt_s3_iamrole_success_cloud(), default=True)
168
+ if obj["env"] == "local"
169
+ else False
157
170
  )
158
171
 
159
- await generate_aws_iamrole_connection_file(
160
- name=connection_name, service=service, role_arn=role_arn, region=region, folder=project.folder
172
+ if create_in_cloud:
173
+ prod_config = obj["config"]
174
+ host = prod_config["host"]
175
+ token = prod_config["token"]
176
+ prod_client = TinyB(
177
+ token=token,
178
+ host=host,
179
+ staging=False,
180
+ )
181
+ prod_role_arn, _, _ = await production_aws_iamrole_only(
182
+ prod_client, service=service, region=region, bucket_name=bucket_name, environment="cloud"
183
+ )
184
+ await prod_client.create_secret(name=secret_name, value=prod_role_arn)
185
+
186
+ connection_file_path = await generate_aws_iamrole_connection_file_with_secret(
187
+ name=connection_name,
188
+ service=service,
189
+ role_arn_secret_name=secret_name,
190
+ region=region,
191
+ folder=project.folder,
161
192
  )
162
- if external_id:
163
- click.echo(
164
- FeedbackManager.success_s3_iam_connection_created(
165
- connection_name=connection_name, external_id=external_id, role_arn=role_arn
166
- )
193
+
194
+ click.echo(
195
+ FeedbackManager.prompt_s3_iamrole_success(
196
+ connection_name=connection_name,
197
+ connection_path=str(connection_file_path),
167
198
  )
199
+ )
@@ -10,7 +10,7 @@ from typing import Optional, Tuple
10
10
  import click
11
11
  from click import Context
12
12
 
13
- from tinybird.client import AuthNoTokenException, TinyB
13
+ from tinybird.tb.client import AuthNoTokenException, TinyB
14
14
  from tinybird.tb.modules.cli import cli
15
15
  from tinybird.tb.modules.common import coro, echo_safe_humanfriendly_tables_format_smart_table, wait_job
16
16
  from tinybird.tb.modules.datafile.common import get_name_version
@@ -5,8 +5,8 @@ from typing import Optional, Tuple
5
5
 
6
6
  import click
7
7
 
8
- from tinybird.client import TinyB
9
8
  from tinybird.prompts import create_prompt, mock_prompt, readme_prompt, rules_prompt
9
+ from tinybird.tb.client import TinyB
10
10
  from tinybird.tb.modules.cicd import init_cicd
11
11
  from tinybird.tb.modules.cli import cli
12
12
  from tinybird.tb.modules.common import _generate_datafile, coro, generate_datafile
@@ -362,26 +362,27 @@ def generate_pipe_file(name: str, content: str, folder: str) -> Path:
362
362
  return f.relative_to(folder)
363
363
 
364
364
 
365
- def generate_connection_file(name: str, content: str, folder: str) -> Path:
365
+ def generate_connection_file(name: str, content: str, folder: str, skip_feedback: bool = False) -> Path:
366
366
  base = Path(folder) / "connections"
367
367
  if not base.exists():
368
368
  base.mkdir()
369
369
  f = base / (f"{name}.connection")
370
370
  with open(f"{f}", "w") as file:
371
371
  file.write(content)
372
- click.echo(FeedbackManager.info_file_created(file=f.relative_to(folder)))
372
+ if not skip_feedback:
373
+ click.echo(FeedbackManager.info_file_created(file=f.relative_to(folder)))
373
374
  return f.relative_to(folder)
374
375
 
375
376
 
376
- async def generate_aws_iamrole_connection_file(
377
- name: str, service: str, role_arn: str, region: str, folder: str
378
- ) -> None:
377
+ async def generate_aws_iamrole_connection_file_with_secret(
378
+ name: str, service: str, role_arn_secret_name: str, region: str, folder: str
379
+ ) -> Path:
379
380
  content = f"""TYPE {service}
380
-
381
- S3_ARN {role_arn}
381
+ S3_ARN {{{{ tb_secret("{role_arn_secret_name}") }}}}
382
382
  S3_REGION {region}
383
383
  """
384
- generate_connection_file(name, content, folder)
384
+ file_path = generate_connection_file(name, content, folder, skip_feedback=True)
385
+ return file_path
385
386
 
386
387
 
387
388
  def create_rules(folder: str, source: str, agent: str):
@@ -11,9 +11,9 @@ from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union
11
11
  import click
12
12
  from toposort import toposort
13
13
 
14
- from tinybird.client import TinyB
15
14
  from tinybird.sql import parse_table_structure, schema_to_sql_columns
16
15
  from tinybird.sql_template import get_used_tables_in_template, render_sql_template
16
+ from tinybird.tb.client import TinyB
17
17
  from tinybird.tb.modules.common import get_ca_pem_content
18
18
  from tinybird.tb.modules.datafile.build_datasource import is_datasource
19
19
  from tinybird.tb.modules.datafile.build_pipe import (
@@ -2,7 +2,7 @@ from typing import Any, Dict, List, Optional, Tuple
2
2
 
3
3
  import click
4
4
 
5
- from tinybird.client import DoesNotExistException, TinyB
5
+ from tinybird.tb.client import DoesNotExistException, TinyB
6
6
  from tinybird.tb.modules.feedback_manager import FeedbackManager
7
7
 
8
8
 
@@ -5,7 +5,7 @@ from typing import Any, Dict, List, Optional
5
5
 
6
6
  import click
7
7
 
8
- from tinybird.client import DoesNotExistException, TinyB
8
+ from tinybird.tb.client import DoesNotExistException, TinyB
9
9
  from tinybird.tb.modules.datafile.common import PREVIEW_CONNECTOR_SERVICES, ImportReplacements
10
10
  from tinybird.tb.modules.feedback_manager import FeedbackManager
11
11