tinybird 0.0.1.dev93__tar.gz → 0.0.1.dev95__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.

Potentially problematic release.


This version of tinybird might be problematic. Click here for more details.

Files changed (108) hide show
  1. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/PKG-INFO +1 -1
  2. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/__cli__.py +2 -2
  3. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/cli.py +2 -0
  4. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/cli.py +3 -3
  5. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/common.py +26 -0
  6. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/datafile/common.py +27 -5
  7. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/deployment.py +23 -3
  8. tinybird-0.0.1.dev95/tinybird/tb/modules/infra.py +473 -0
  9. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/local_common.py +7 -2
  10. tinybird-0.0.1.dev95/tinybird/tb/modules/open.py +42 -0
  11. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird.egg-info/PKG-INFO +1 -1
  12. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird.egg-info/SOURCES.txt +2 -0
  13. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/setup.cfg +0 -0
  14. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/__cli__.py +0 -0
  15. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/ch_utils/constants.py +0 -0
  16. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/ch_utils/engine.py +0 -0
  17. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/check_pypi.py +0 -0
  18. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/client.py +0 -0
  19. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/config.py +0 -0
  20. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/connectors.py +0 -0
  21. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/context.py +0 -0
  22. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/datafile.py +0 -0
  23. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/datatypes.py +0 -0
  24. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/feedback_manager.py +0 -0
  25. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/git_settings.py +0 -0
  26. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/prompts.py +0 -0
  27. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/sql.py +0 -0
  28. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/sql_template.py +0 -0
  29. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/sql_template_fmt.py +0 -0
  30. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/sql_toolset.py +0 -0
  31. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/syncasync.py +0 -0
  32. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/auth.py +0 -0
  33. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/build.py +0 -0
  34. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/cicd.py +0 -0
  35. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/config.py +0 -0
  36. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/connection.py +0 -0
  37. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/copy.py +0 -0
  38. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/create.py +0 -0
  39. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/datafile/build.py +0 -0
  40. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/datafile/build_common.py +0 -0
  41. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/datafile/build_datasource.py +0 -0
  42. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/datafile/build_pipe.py +0 -0
  43. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/datafile/diff.py +0 -0
  44. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/datafile/exceptions.py +0 -0
  45. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/datafile/fixture.py +0 -0
  46. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/datafile/format_common.py +0 -0
  47. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/datafile/format_datasource.py +0 -0
  48. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/datafile/format_pipe.py +0 -0
  49. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/datafile/parse_datasource.py +0 -0
  50. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/datafile/parse_pipe.py +0 -0
  51. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/datafile/pipe_checker.py +0 -0
  52. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/datafile/playground.py +0 -0
  53. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/datafile/pull.py +0 -0
  54. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/datasource.py +0 -0
  55. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/endpoint.py +0 -0
  56. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/exceptions.py +0 -0
  57. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/feedback_manager.py +0 -0
  58. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/fmt.py +0 -0
  59. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/job.py +0 -0
  60. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/llm.py +0 -0
  61. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/llm_utils.py +0 -0
  62. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/local.py +0 -0
  63. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/login.py +0 -0
  64. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/logout.py +0 -0
  65. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/materialization.py +0 -0
  66. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/mock.py +0 -0
  67. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/pipe.py +0 -0
  68. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/playground.py +0 -0
  69. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/project.py +0 -0
  70. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/regions.py +0 -0
  71. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/secret.py +0 -0
  72. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/shell.py +0 -0
  73. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/table.py +0 -0
  74. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/tag.py +0 -0
  75. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/telemetry.py +0 -0
  76. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/test.py +0 -0
  77. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/tinyunit/tinyunit.py +0 -0
  78. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/tinyunit/tinyunit_lib.py +0 -0
  79. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/token.py +0 -0
  80. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/watch.py +0 -0
  81. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/workspace.py +0 -0
  82. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb/modules/workspace_members.py +0 -0
  83. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb_cli.py +0 -0
  84. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb_cli_modules/auth.py +0 -0
  85. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb_cli_modules/branch.py +0 -0
  86. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb_cli_modules/cicd.py +0 -0
  87. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb_cli_modules/cli.py +0 -0
  88. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb_cli_modules/common.py +0 -0
  89. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb_cli_modules/config.py +0 -0
  90. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb_cli_modules/connection.py +0 -0
  91. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb_cli_modules/datasource.py +0 -0
  92. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb_cli_modules/exceptions.py +0 -0
  93. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb_cli_modules/fmt.py +0 -0
  94. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb_cli_modules/job.py +0 -0
  95. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb_cli_modules/pipe.py +0 -0
  96. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb_cli_modules/regions.py +0 -0
  97. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb_cli_modules/tag.py +0 -0
  98. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb_cli_modules/telemetry.py +0 -0
  99. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb_cli_modules/test.py +0 -0
  100. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  101. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
  102. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb_cli_modules/workspace.py +0 -0
  103. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  104. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird/tornado_template.py +0 -0
  105. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird.egg-info/dependency_links.txt +0 -0
  106. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird.egg-info/entry_points.txt +0 -0
  107. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird.egg-info/requires.txt +0 -0
  108. {tinybird-0.0.1.dev93 → tinybird-0.0.1.dev95}/tinybird.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: tinybird
3
- Version: 0.0.1.dev93
3
+ Version: 0.0.1.dev95
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -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.dev93'
8
- __revision__ = '8ee6a21'
7
+ __version__ = '0.0.1.dev95'
8
+ __revision__ = 'dc3767a'
@@ -15,12 +15,14 @@ import tinybird.tb.modules.datasource
15
15
  import tinybird.tb.modules.deployment
16
16
  import tinybird.tb.modules.endpoint
17
17
  import tinybird.tb.modules.fmt
18
+ import tinybird.tb.modules.infra
18
19
  import tinybird.tb.modules.job
19
20
  import tinybird.tb.modules.local
20
21
  import tinybird.tb.modules.login
21
22
  import tinybird.tb.modules.logout
22
23
  import tinybird.tb.modules.materialization
23
24
  import tinybird.tb.modules.mock
25
+ import tinybird.tb.modules.open
24
26
  import tinybird.tb.modules.pipe
25
27
  import tinybird.tb.modules.playground
26
28
  import tinybird.tb.modules.secret
@@ -110,7 +110,7 @@ async def cli(
110
110
  folder = os.path.join(config_temp._path.replace(".tinyb", ""), config.get("cwd", os.getcwd()))
111
111
  project = Project(folder=folder)
112
112
  config["path"] = str(project.path)
113
- # If they have passed a token or host as paramter and it's different that record in .tinyb, refresh the workspace id
113
+ # If they have passed a token or host as parameter and it's different that record in .tinyb, refresh the workspace id
114
114
  if token or host:
115
115
  try:
116
116
  workspace = await client.workspace_info()
@@ -393,12 +393,12 @@ def __unpatch_click_output():
393
393
 
394
394
 
395
395
  async def create_ctx_client(ctx: Context, config: Dict[str, Any], cloud: bool, build: bool, staging: bool):
396
- commands_without_ctx_client = ["auth", "check", "login", "local", "update", "upgrade", "logout"]
396
+ commands_without_ctx_client = ["auth", "check", "local", "login", "logout", "update", "upgrade"]
397
397
  command = ctx.invoked_subcommand
398
398
  if command in commands_without_ctx_client:
399
399
  return None
400
400
 
401
- commands_always_cloud = ["pull", "playground"]
401
+ commands_always_cloud = ["pull", "playground", "infra"]
402
402
  commands_always_build = ["build", "test", "dev", "create"]
403
403
  commands_always_local: List[str] = []
404
404
  if (
@@ -81,6 +81,8 @@ SUPPORTED_FORMATS = ["csv", "ndjson", "json", "parquet"]
81
81
  OLDEST_ROLLBACK = "oldest_rollback"
82
82
  MAIN_BRANCH = "main"
83
83
 
84
+ CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
85
+
84
86
 
85
87
  def obfuscate_token(value: Optional[str]) -> Optional[str]:
86
88
  if not value:
@@ -2138,3 +2140,27 @@ async def get_user_token(config: CLIConfig, user_token: Optional[str] = None) ->
2138
2140
  await check_user_token_with_client(client, user_token)
2139
2141
 
2140
2142
  return user_token
2143
+
2144
+
2145
+ def get_ui_url(api_host: str) -> str:
2146
+ """Transforms API URLs into their corresponding UI URLs.
2147
+ Examples:
2148
+ >>> get_ui_url("http://localhost:8000")
2149
+ 'https://cloud.tinybird.co/local/8000'
2150
+ >>> get_ui_url("https://api.europe-west2.gcp.tinybird.co")
2151
+ 'https://cloud.tinybird.co/gcp/europe-west2'
2152
+ >>> get_ui_url("https://other-domain.com")
2153
+ 'https://other-domain.com'
2154
+ """
2155
+ if "//localhost" in api_host:
2156
+ port = api_host.split(":")[-1] or "80"
2157
+ return f"https://cloud.tinybird.co/local/{port}"
2158
+
2159
+ if api_host.endswith("tinybird.co") and "api." in api_host:
2160
+ parts = api_host.split(".")
2161
+ if len(parts) >= 4:
2162
+ region = parts[1] or "europe-west2"
2163
+ cloud = parts[2] or "gcp"
2164
+ return f"https://cloud.tinybird.co/{cloud}/{region}"
2165
+
2166
+ return api_host
@@ -241,6 +241,11 @@ class Datafile:
241
241
  raise DatafileValidationError(f"Materialized node {repr(node['name'])} missing target datasource")
242
242
  if node.get("type", "").lower() == PipeNodeTypes.COPY:
243
243
  self.validate_copy_node(node)
244
+ for token in self.tokens:
245
+ if token["permission"].upper() != "READ":
246
+ raise DatafileValidationError(
247
+ f"Invalid permission {token['permission']} for token {token['token_name']}. Only READ is allowed for pipes"
248
+ )
244
249
  elif self.kind == DatafileKind.datasource:
245
250
  # TODO(eclbg):
246
251
  # [x] Just one node
@@ -253,6 +258,11 @@ class Datafile:
253
258
  node = self.nodes[0]
254
259
  if "schema" not in node:
255
260
  raise DatafileValidationError("SCHEMA is mandatory")
261
+ for token in self.tokens:
262
+ if token["permission"].upper() not in {"READ", "APPEND"}:
263
+ raise DatafileValidationError(
264
+ f"Invalid permission {token['permission']} for token {token['token_name']}. Only READ and APPEND are allowed for datasources"
265
+ )
256
266
  else:
257
267
  # We cannot validate a datafile whose kind is unknown
258
268
  pass
@@ -1274,16 +1284,28 @@ def parse(
1274
1284
 
1275
1285
  @multiline_not_supported
1276
1286
  def add_token(*args: str, **kwargs: Any) -> None: # token_name, permissions):
1277
- # lineno = kwargs["lineno"]
1287
+ lineno = kwargs["lineno"]
1278
1288
  if len(args) < 2:
1279
1289
  raise DatafileSyntaxError(
1280
- message='TOKEN takes two params: token name and permissions e.g TOKEN "read api token" READ',
1290
+ message='TOKEN takes two params: token name and permission e.g TOKEN "read api token" READ',
1281
1291
  lineno=lineno,
1282
1292
  pos=1,
1283
1293
  )
1284
- # TODO(eclbg): We should validate that the permissions are a valid string. We only support READ for pipes and
1285
- # APPEND for datasources
1286
- doc.tokens.append({"token_name": _unquote(args[0]), "permissions": args[1]})
1294
+ if len(args) > 2:
1295
+ raise DatafileSyntaxError(
1296
+ f"Invalid number of arguments for TOKEN command: {len(args)}. Expected 2 arguments: token name and permission",
1297
+ lineno=lineno,
1298
+ pos=len("token") + len(args[0]) + 3, # Naive handling of whitespace. Assuming there's 2
1299
+ )
1300
+ permission = args[1]
1301
+ if permission.upper() not in ["READ", "APPEND"]:
1302
+ raise DatafileSyntaxError(
1303
+ f"Invalid permission: {permission}. Only READ and APPEND are supported",
1304
+ lineno=lineno,
1305
+ pos=len("token") + len(args[0]) + 3, # Naive handling of whitespace. Assuming there's 2
1306
+ )
1307
+ token_name = _unquote(args[0])
1308
+ doc.tokens.append({"token_name": token_name, "permission": permission.upper()})
1287
1309
 
1288
1310
  @not_supported_yet()
1289
1311
  def include(*args: str, **kwargs: Any) -> None:
@@ -4,7 +4,7 @@ import sys
4
4
  import time
5
5
  from datetime import datetime
6
6
  from pathlib import Path
7
- from typing import Any, Dict, Optional, Union
7
+ from typing import Any, Dict, Optional, Tuple, Union
8
8
 
9
9
  import click
10
10
  import requests
@@ -451,8 +451,10 @@ def create_deployment(
451
451
 
452
452
  def print_changes(result: dict, project: Project) -> None:
453
453
  deployment = result.get("deployment", {})
454
- columns = ["status", "name", "path"]
454
+ resources_columns = ["status", "name", "path"]
455
455
  resources: list[list[Union[str, None]]] = []
456
+ tokens_columns = ["Change", "Token name", "Added permissions", "Removed permissions"]
457
+ tokens: list[Tuple[str, str, str, str]] = []
456
458
 
457
459
  for ds in deployment.get("new_datasource_names", []):
458
460
  resources.append(["new", ds, project.get_resource_path(ds, "datasource")])
@@ -481,8 +483,26 @@ def print_changes(result: dict, project: Project) -> None:
481
483
  for dc in deployment.get("deleted_data_connector_names", []):
482
484
  resources.append(["deleted", dc, project.get_resource_path(dc, "data_connector")])
483
485
 
486
+ for token_change in deployment.get("token_changes", []):
487
+ token_name = token_change.get("token_name")
488
+ change_type = token_change.get("change_type")
489
+ added_perms = []
490
+ removed_perms = []
491
+ permission_changes = token_change.get("permission_changes", {})
492
+ for perm in permission_changes.get("added_permissions", []):
493
+ added_perms.append(f"{perm['resource_name']}.{perm['resource_type']}:{perm['permission']}")
494
+ for perm in permission_changes.get("removed_permissions", []):
495
+ removed_perms.append(f"{perm['resource_name']}.{perm['resource_type']}:{perm['permission']}")
496
+
497
+ tokens.append((change_type, token_name, "\n".join(added_perms), "\n".join(removed_perms)))
498
+
484
499
  if resources:
485
500
  click.echo(FeedbackManager.highlight(message="\n» Changes to be deployed...\n"))
486
- echo_safe_humanfriendly_tables_format_smart_table(resources, column_names=columns)
501
+ echo_safe_humanfriendly_tables_format_smart_table(resources, column_names=resources_columns)
487
502
  else:
488
503
  click.echo(FeedbackManager.highlight(message="\n» No changes to be deployed\n"))
504
+ if tokens:
505
+ click.echo(FeedbackManager.highlight(message="\n» Changes in tokens to be deployed...\n"))
506
+ echo_safe_humanfriendly_tables_format_smart_table(tokens, column_names=tokens_columns)
507
+ else:
508
+ click.echo(FeedbackManager.highlight(message="\n» No changes in tokens to be deployed\n"))
@@ -0,0 +1,473 @@
1
+ from pathlib import Path
2
+ from typing import Optional
3
+
4
+ import click
5
+ from click import Context
6
+
7
+ from tinybird.tb.modules.cli import cli
8
+
9
+ from .common import CONTEXT_SETTINGS
10
+
11
+ K8S_YML = """
12
+ ---
13
+ apiVersion: v1
14
+ kind: Namespace
15
+ metadata:
16
+ name: %(namespace)s
17
+ labels:
18
+ name: tinybird
19
+ ---
20
+ apiVersion: v1
21
+ kind: ServiceAccount
22
+ metadata:
23
+ name: tinybird
24
+ namespace: %(namespace)s
25
+ labels:
26
+ name: tinybird
27
+ automountServiceAccountToken: true
28
+ ---
29
+ apiVersion: v1
30
+ kind: Service
31
+ metadata:
32
+ name: tinybird
33
+ namespace: %(namespace)s
34
+ labels:
35
+ name: tinybird
36
+ annotations:
37
+ service.beta.kubernetes.io/aws-load-balancer-type: 'external'
38
+ service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: 'ip'
39
+ service.beta.kubernetes.io/aws-load-balancer-backend-protocol: 'tcp'
40
+ service.beta.kubernetes.io/aws-load-balancer-scheme: 'internet-facing'
41
+ service.beta.kubernetes.io/aws-load-balancer-ssl-negotiation-policy: 'ELBSecurityPolicy-TLS13-1-2-2021-06'
42
+ service.beta.kubernetes.io/aws-load-balancer-ssl-cert: '%(cert_arn)s'
43
+ spec:
44
+ type: LoadBalancer
45
+ ports:
46
+ - port: 443
47
+ targetPort: http
48
+ protocol: TCP
49
+ name: https
50
+ selector:
51
+ name: tinybird
52
+ ---
53
+ apiVersion: apps/v1
54
+ kind: Deployment
55
+ metadata:
56
+ name: tinybird
57
+ namespace: %(namespace)s
58
+ labels:
59
+ name: tinybird
60
+ spec:
61
+ replicas: 1
62
+ selector:
63
+ matchLabels:
64
+ name: tinybird
65
+ template:
66
+ metadata:
67
+ labels:
68
+ name: tinybird
69
+ spec:
70
+ serviceAccountName: tinybird
71
+ containers:
72
+ - name: tinybird
73
+ image: "tinybirdco/tinybird-local:beta"
74
+ imagePullPolicy: Always
75
+ ports:
76
+ - name: http
77
+ containerPort: 7181
78
+ protocol: TCP
79
+ """
80
+
81
+ TERRAFORM_FIRST_TEMPLATE = """
82
+ terraform {
83
+ required_providers {
84
+ aws = {
85
+ source = "hashicorp/aws"
86
+ version = "~> 5.0"
87
+ }
88
+ }
89
+ }
90
+
91
+ provider "aws" {
92
+ region = "%(aws_region)s"
93
+ }
94
+
95
+ # Get the hosted zone data
96
+ data "aws_route53_zone" "selected" {
97
+ name = "%(domain)s"
98
+ }
99
+
100
+ # Create ACM certificate
101
+ resource "aws_acm_certificate" "cert" {
102
+ domain_name = "*.${data.aws_route53_zone.selected.name}"
103
+ validation_method = "DNS"
104
+ subject_alternative_names = [data.aws_route53_zone.selected.name]
105
+
106
+ lifecycle {
107
+ create_before_destroy = true
108
+ }
109
+ }
110
+
111
+ # Create DNS records for certificate validation
112
+ resource "aws_route53_record" "cert_validation" {
113
+ for_each = {
114
+ for dvo in aws_acm_certificate.cert.domain_validation_options : dvo.domain_name => {
115
+ name = dvo.resource_record_name
116
+ record = dvo.resource_record_value
117
+ type = dvo.resource_record_type
118
+ }
119
+ }
120
+
121
+ allow_overwrite = true
122
+ name = each.value.name
123
+ records = [each.value.record]
124
+ ttl = 60
125
+ type = each.value.type
126
+ zone_id = data.aws_route53_zone.selected.zone_id
127
+ }
128
+
129
+ # Certificate validation
130
+ resource "aws_acm_certificate_validation" "cert" {
131
+ certificate_arn = aws_acm_certificate.cert.arn
132
+ validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn]
133
+ }
134
+
135
+ output "certificate_arn" {
136
+ description = "The ARN of the ACM certificate"
137
+ value = aws_acm_certificate.cert.arn
138
+ }
139
+ """
140
+
141
+ TERRAFORM_SECOND_TEMPLATE = """
142
+ # Create Route 53 record for the load balancer
143
+ resource "aws_route53_record" "tinybird" {
144
+ zone_id = data.aws_route53_zone.selected.zone_id
145
+ name = "%(full_dns_name)s"
146
+ type = "CNAME"
147
+ ttl = 300
148
+ records = ["%(external_ip)s"]
149
+ }
150
+
151
+ output "tinybird_dns" {
152
+ description = "The DNS name for Tinybird"
153
+ value = aws_route53_record.tinybird.fqdn
154
+ }
155
+ """
156
+
157
+
158
+ @cli.group(context_settings=CONTEXT_SETTINGS)
159
+ @click.pass_context
160
+ def infra(ctx: Context) -> None:
161
+ """Infra commands."""
162
+
163
+
164
+ @infra.command(name="ls")
165
+ @click.pass_context
166
+ async def infra_ls(ctx: Context) -> None:
167
+ """List infra"""
168
+
169
+
170
+ @infra.command(name="init")
171
+ @click.option("--provider", default="aws", type=str, help="Infrastructure provider (aws, gcp, azure)")
172
+ @click.option("--region", type=str, help="AWS region (for AWS provider)")
173
+ @click.option("--domain", type=str, help="Route53 domain name (for AWS provider)")
174
+ @click.option("--namespace", type=str, help="Kubernetes namespace for deployment")
175
+ @click.option("--dns-record", type=str, help="DNS record name to create (without domain, e.g. 'tinybird')")
176
+ @click.option(
177
+ "--auto-apply-terraform", is_flag=True, help="Automatically apply Terraform configuration without prompting"
178
+ )
179
+ @click.option("--auto-apply-dns", is_flag=True, help="Automatically apply DNS configuration without prompting")
180
+ @click.option(
181
+ "--auto-apply-kubectl", is_flag=True, help="Automatically apply Kubernetes configuration without prompting"
182
+ )
183
+ @click.option("--skip-terraform", is_flag=True, help="Skip Terraform configuration and application")
184
+ @click.option("--skip-kubectl", is_flag=True, help="Skip Kubernetes configuration and application")
185
+ @click.option("--skip-dns", is_flag=True, help="Skip DNS configuration and application")
186
+ @click.pass_context
187
+ def infra_init(
188
+ ctx: Context,
189
+ provider: str,
190
+ region: Optional[str] = None,
191
+ domain: Optional[str] = None,
192
+ namespace: Optional[str] = None,
193
+ dns_record: Optional[str] = None,
194
+ auto_apply_terraform: bool = False,
195
+ auto_apply_dns: bool = False,
196
+ auto_apply_kubectl: bool = False,
197
+ skip_terraform: bool = False,
198
+ skip_kubectl: bool = False,
199
+ skip_dns: bool = False,
200
+ ) -> None:
201
+ """Init infra"""
202
+ # AWS-specific Terraform template creation
203
+ if provider.lower() != "aws":
204
+ click.echo("Provider not supported yet.")
205
+ return
206
+
207
+ # Create infra directory if it doesn't exist
208
+ infra_dir = Path("infra")
209
+ infra_dir.mkdir(exist_ok=True)
210
+ yaml_path = infra_dir / "k8s.yaml"
211
+ tf_path = infra_dir / "main.tf"
212
+
213
+ # Write the Terraform template
214
+ region = region or click.prompt("Enter aws region", default="us-east-1", type=str)
215
+ domain = domain or click.prompt("Enter route 53 domain name", type=str)
216
+
217
+ terraform_content = TERRAFORM_FIRST_TEMPLATE % {"aws_region": region, "domain": domain}
218
+
219
+ with open(tf_path, "w") as f:
220
+ f.write(terraform_content.lstrip())
221
+
222
+ click.echo(f"Creating Terraform configuration in {tf_path}")
223
+
224
+ # Apply Terraform configuration if user confirms
225
+ if not skip_terraform and (
226
+ auto_apply_terraform or click.confirm("Would you like to apply the Terraform configuration now?")
227
+ ):
228
+ import subprocess
229
+
230
+ # Initialize Terraform
231
+ click.echo("Initializing Terraform...")
232
+ init_result = subprocess.run(["terraform", "-chdir=infra", "init"], capture_output=True, text=True)
233
+
234
+ if init_result.returncode != 0:
235
+ click.echo("Terraform initialization failed:")
236
+ click.echo(init_result.stderr)
237
+ return
238
+
239
+ click.echo(init_result.stdout)
240
+
241
+ # Apply Terraform configuration
242
+ click.echo("\nApplying Terraform configuration...\n")
243
+ apply_result = subprocess.run(
244
+ ["terraform", "-chdir=infra", "apply", "-auto-approve"], capture_output=True, text=True
245
+ )
246
+
247
+ if apply_result.returncode != 0:
248
+ click.echo("Terraform apply failed:")
249
+ click.echo(apply_result.stderr)
250
+ return
251
+
252
+ click.echo(apply_result.stdout)
253
+
254
+ # Get the certificate ARN output
255
+ output_result = subprocess.run(
256
+ ["terraform", "-chdir=infra", "output", "certificate_arn"], capture_output=True, text=True
257
+ )
258
+
259
+ if output_result.returncode == 0:
260
+ cert_arn = output_result.stdout.strip().replace('"', "")
261
+
262
+ namespace = namespace or click.prompt("Enter namespace name", default="tinybird", type=str)
263
+ new_content = K8S_YML % {"namespace": namespace, "cert_arn": cert_arn}
264
+
265
+ with open(yaml_path, "w") as f:
266
+ f.write(new_content.lstrip())
267
+
268
+ click.echo(f"Created Kubernetes configuration with certificate ARN in {yaml_path}")
269
+
270
+ # Prompt to apply the k8s configuration
271
+ if not skip_kubectl and (
272
+ auto_apply_kubectl or click.confirm("Would you like to apply the Kubernetes configuration now?")
273
+ ):
274
+ import subprocess
275
+
276
+ # Get current kubectl context
277
+ current_context_result = subprocess.run(
278
+ ["kubectl", "config", "current-context"], capture_output=True, text=True
279
+ )
280
+
281
+ current_context = (
282
+ current_context_result.stdout.strip() if current_context_result.returncode == 0 else "unknown"
283
+ )
284
+
285
+ # Get available contexts
286
+ contexts_result = subprocess.run(
287
+ ["kubectl", "config", "get-contexts", "-o", "name"], capture_output=True, text=True
288
+ )
289
+
290
+ if contexts_result.returncode != 0:
291
+ click.echo("Failed to get kubectl contexts:")
292
+ click.echo(contexts_result.stderr)
293
+ return
294
+
295
+ available_contexts = [
296
+ context.strip() for context in contexts_result.stdout.splitlines() if context.strip()
297
+ ]
298
+
299
+ if not available_contexts:
300
+ click.echo("No kubectl contexts found. Please configure kubectl first.")
301
+ return
302
+
303
+ # Prompt user to select a context
304
+ if len(available_contexts) == 1:
305
+ selected_context = available_contexts[0]
306
+ click.echo(f"Using the only available kubectl context: {selected_context}")
307
+ else:
308
+ click.echo("\nAvailable kubectl contexts:")
309
+ for i, context in enumerate(available_contexts):
310
+ marker = " (current)" if context == current_context else ""
311
+ click.echo(f" {i + 1}. {context}{marker}")
312
+
313
+ click.echo("")
314
+ default_index = (
315
+ available_contexts.index(current_context) + 1 if current_context in available_contexts else 1
316
+ )
317
+
318
+ selected_index = click.prompt(
319
+ "Select kubectl context number to apply configuration",
320
+ type=click.IntRange(1, len(available_contexts)),
321
+ default=default_index,
322
+ )
323
+
324
+ selected_context = available_contexts[selected_index - 1]
325
+ click.echo(f"Selected context: {selected_context}")
326
+
327
+ # Apply the configuration to the selected context
328
+ click.echo(f"Applying Kubernetes configuration to context '{selected_context}'...")
329
+ apply_result = subprocess.run(
330
+ ["kubectl", "--context", selected_context, "apply", "-f", str(yaml_path)],
331
+ capture_output=True,
332
+ text=True,
333
+ )
334
+
335
+ if apply_result.returncode != 0:
336
+ click.echo("Failed to apply Kubernetes configuration:")
337
+ click.echo(apply_result.stderr)
338
+ else:
339
+ click.echo("Kubernetes configuration applied successfully:")
340
+ click.echo(apply_result.stdout)
341
+
342
+ # Get the namespace from the applied configuration
343
+ namespace = None
344
+ with open(yaml_path, "r") as f:
345
+ for line in f:
346
+ if "namespace:" in line and not namespace:
347
+ namespace = line.split("namespace:")[1].strip()
348
+ break
349
+
350
+ if not namespace:
351
+ namespace = "tinybird" # Default namespace
352
+
353
+ click.echo(f"\nWaiting for load balancer to be provisioned in namespace '{namespace}'...")
354
+
355
+ # Wait for the load balancer to get an external IP
356
+ max_attempts = 30
357
+ attempt = 0
358
+ external_ip = None
359
+
360
+ while attempt < max_attempts and not external_ip:
361
+ attempt += 1
362
+
363
+ # Get the service details
364
+ get_service_result = subprocess.run(
365
+ [
366
+ "kubectl",
367
+ "--context",
368
+ selected_context,
369
+ "-n",
370
+ namespace,
371
+ "get",
372
+ "service",
373
+ "tinybird",
374
+ "-o",
375
+ "jsonpath='{.status.loadBalancer.ingress[0].hostname}'",
376
+ ],
377
+ capture_output=True,
378
+ text=True,
379
+ )
380
+
381
+ if get_service_result.returncode == 0:
382
+ potential_ip = get_service_result.stdout.strip().replace("'", "")
383
+ if potential_ip and potential_ip != "":
384
+ external_ip = potential_ip
385
+ break
386
+
387
+ click.echo(
388
+ f"Attempt {attempt}/{max_attempts}: Load balancer not ready yet, waiting 10 seconds..."
389
+ )
390
+ import time
391
+
392
+ time.sleep(10)
393
+
394
+ if external_ip:
395
+ click.echo("\n✅ Load balancer provisioned successfully!")
396
+
397
+ # Update the Terraform configuration with the load balancer DNS
398
+ if not skip_dns and domain and tf_path.exists():
399
+ click.echo("\nUpdating Terraform configuration with load balancer DNS...")
400
+
401
+ with open(tf_path, "r") as f:
402
+ tf_content = f.read()
403
+
404
+ # Check if the Route 53 record already exists in the file
405
+ if 'resource "aws_route53_record" "tinybird"' not in tf_content:
406
+ # Get the DNS record name
407
+ dns_record = dns_record or click.prompt(
408
+ "Enter DNS record name (without domain)", default="tinybird", type=str
409
+ )
410
+
411
+ # Create the full DNS name
412
+ full_dns_name = f"{dns_record}.{domain}"
413
+
414
+ # Use in the Terraform template
415
+ route53_record = TERRAFORM_SECOND_TEMPLATE % {
416
+ "external_ip": external_ip,
417
+ "full_dns_name": full_dns_name,
418
+ }
419
+
420
+ # Append the Route 53 record to the Terraform file
421
+ with open(tf_path, "a") as f:
422
+ f.write(route53_record.lstrip())
423
+
424
+ click.echo("Added Route 53 record to Terraform configuration")
425
+ else:
426
+ # Update the existing Route 53 record
427
+ updated_tf = tf_content.replace(
428
+ 'records = ["LOAD_BALANCER_DNS_PLACEHOLDER"]', f'records = ["{external_ip}"]'
429
+ )
430
+
431
+ # Also handle case where there might be another placeholder or old value
432
+ import re
433
+
434
+ pattern = r'records\s*=\s*\[\s*"[^"]*"\s*\]'
435
+ updated_tf = re.sub(pattern, f'records = ["{external_ip}"]', updated_tf)
436
+
437
+ with open(tf_path, "w") as f:
438
+ f.write(updated_tf.lstrip())
439
+
440
+ click.echo("Updated existing Route 53 record in Terraform configuration")
441
+
442
+ # Apply the updated Terraform configuration
443
+ if not skip_dns and (
444
+ auto_apply_dns
445
+ or click.confirm("Would you like to create the DNS record in Route 53 now?")
446
+ ):
447
+ click.echo("Applying updated Terraform configuration...")
448
+ apply_result = subprocess.run(
449
+ ["terraform", "-chdir=infra", "apply", "-auto-approve"],
450
+ capture_output=True,
451
+ text=True,
452
+ )
453
+
454
+ if apply_result.returncode != 0:
455
+ click.echo("Failed to create DNS record:")
456
+ click.echo(apply_result.stderr)
457
+ else:
458
+ click.echo("DNS record created successfully!")
459
+
460
+ # Get the DNS name from Terraform output
461
+ dns_output = subprocess.run(
462
+ ["terraform", "-chdir=infra", "output", "tinybird_dns"],
463
+ capture_output=True,
464
+ text=True,
465
+ )
466
+
467
+ if dns_output.returncode == 0:
468
+ dns_name = dns_output.stdout.strip().replace('"', "")
469
+ click.echo(f"\nYour Tinybird instance is now available at: https://{dns_name}")
470
+ else:
471
+ click.echo(
472
+ f"\nYour Tinybird instance should be available at: https://tinybird.{domain}"
473
+ )
@@ -17,6 +17,7 @@ TB_LOCAL_HOST = f"http://localhost:{TB_LOCAL_PORT}"
17
17
 
18
18
  async def get_tinybird_local_client(config_obj: Dict[str, Any], build: bool = False, staging: bool = False) -> TinyB:
19
19
  """Get a Tinybird client connected to the local environment."""
20
+
20
21
  config = await get_tinybird_local_config(config_obj, build=build)
21
22
  return config.get_client(host=TB_LOCAL_HOST, staging=staging)
22
23
 
@@ -40,9 +41,8 @@ async def get_tinybird_local_config(config_obj: Dict[str, Any], build: bool = Fa
40
41
  default_token = tokens["workspace_admin_token"]
41
42
  # Create a new workspace if path is provided. This is used to isolate the build in a different workspace.
42
43
  if path:
43
- folder_hash = hashlib.sha256(path.encode()).hexdigest()
44
44
  user_client = config.get_client(host=TB_LOCAL_HOST, token=user_token)
45
- ws_name = f"Tinybird_Local_Build_{folder_hash}" if build else config.get("name") or config_obj.get("name")
45
+ ws_name = get_build_workspace_name(path) if build else config.get("name") or config_obj.get("name")
46
46
  if not ws_name:
47
47
  raise AuthNoTokenException()
48
48
 
@@ -74,3 +74,8 @@ async def get_tinybird_local_config(config_obj: Dict[str, Any], build: bool = Fa
74
74
 
75
75
  config.set_user_token(user_token)
76
76
  return config
77
+
78
+
79
+ def get_build_workspace_name(path: str) -> str:
80
+ folder_hash = hashlib.sha256(path.encode()).hexdigest()
81
+ return f"Tinybird_Local_Build_{folder_hash}"
@@ -0,0 +1,42 @@
1
+ import webbrowser
2
+
3
+ import click
4
+ from click import Context
5
+
6
+ from tinybird.tb.modules.cli import cli
7
+ from tinybird.tb.modules.common import coro, get_ui_url
8
+ from tinybird.tb.modules.exceptions import CLIException
9
+ from tinybird.tb.modules.feedback_manager import FeedbackManager
10
+ from tinybird.tb.modules.local_common import get_build_workspace_name
11
+
12
+
13
+ @cli.command()
14
+ @click.option(
15
+ "--workspace",
16
+ help="Set the workspace you want to open. If unset, your current workspace will be used.",
17
+ )
18
+ @click.pass_context
19
+ @coro
20
+ async def open(ctx: Context, workspace: str):
21
+ """Open workspace in the browser."""
22
+
23
+ config = ctx.ensure_object(dict)["config"]
24
+ client = ctx.ensure_object(dict)["client"]
25
+ env = ctx.ensure_object(dict)["env"]
26
+
27
+ url_host = get_ui_url(client.host)
28
+
29
+ if not workspace:
30
+ workspace = get_build_workspace_name(config.get("path")) if env == "build" else config.get("name")
31
+
32
+ if not workspace:
33
+ raise CLIException(
34
+ FeedbackManager.error(
35
+ message="No workspace found. Run 'tb login' first or pass a workspace using the --workspace parameter"
36
+ )
37
+ )
38
+
39
+ click.echo(FeedbackManager.highlight(message=f"» Opening workspace {workspace} in the browser"))
40
+
41
+ auth_url = f"{url_host}/{workspace}"
42
+ webbrowser.open(auth_url)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: tinybird
3
- Version: 0.0.1.dev93
3
+ Version: 0.0.1.dev95
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -41,6 +41,7 @@ tinybird/tb/modules/endpoint.py
41
41
  tinybird/tb/modules/exceptions.py
42
42
  tinybird/tb/modules/feedback_manager.py
43
43
  tinybird/tb/modules/fmt.py
44
+ tinybird/tb/modules/infra.py
44
45
  tinybird/tb/modules/job.py
45
46
  tinybird/tb/modules/llm.py
46
47
  tinybird/tb/modules/llm_utils.py
@@ -50,6 +51,7 @@ tinybird/tb/modules/login.py
50
51
  tinybird/tb/modules/logout.py
51
52
  tinybird/tb/modules/materialization.py
52
53
  tinybird/tb/modules/mock.py
54
+ tinybird/tb/modules/open.py
53
55
  tinybird/tb/modules/pipe.py
54
56
  tinybird/tb/modules/playground.py
55
57
  tinybird/tb/modules/project.py
File without changes