tinybird 0.0.1.dev94__py3-none-any.whl → 0.0.1.dev96__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

tinybird/prompts.py CHANGED
@@ -419,13 +419,15 @@ You are a Tinybird expert. You will be given a prompt to generate new or update
419
419
  {pipe_example}
420
420
  {copy_pipe_instructions}
421
421
  {materialized_pipe_instructions}
422
+ {connection_instructions}
423
+ {connection_example}
422
424
 
423
425
  {feedback_history}
424
426
 
425
427
  Use the following format to generate the response and do not wrap it in any other text, including the <response> tag.
426
428
  <response>
427
429
  <resource>
428
- <type>[datasource or pipe]</type>
430
+ <type>[datasource or pipe or connection]</type>
429
431
  <name>[resource name here]</name>
430
432
  <content>[resource content here]</content>
431
433
  </resource>
@@ -440,6 +442,8 @@ Use the following format to generate the response and do not wrap it in any othe
440
442
  pipe_example=pipe_example,
441
443
  copy_pipe_instructions=copy_pipe_instructions,
442
444
  materialized_pipe_instructions=materialized_pipe_instructions,
445
+ connection_instructions=connection_instructions,
446
+ connection_example=connection_example,
443
447
  feedback_history=feedback_history,
444
448
  )
445
449
 
@@ -672,6 +676,25 @@ ENGINE_SORTING_KEY "date, dimension_1, dimension_2, ..."
672
676
  </target_datasource_content>
673
677
  """
674
678
 
679
+ connection_instructions = """
680
+ <connection_file_instructions>
681
+ - Content cannot be empty.
682
+ - The connection names must be unique.
683
+ - No indentation is allowed for property names
684
+ - We only support kafka connections for now
685
+ </connection_file_instructions>
686
+ """
687
+
688
+ connection_example = """
689
+ <connection_content>
690
+ TYPE kafka
691
+ KAFKA_BOOTSTRAP_SERVERS {{ tb_secret("PRODUCTION_KAFKA_SERVERS", "localhost:9092") }}
692
+ KAFKA_SECURITY_PROTOCOL SASL_SSL
693
+ KAFKA_SASL_MECHANISM PLAIN
694
+ KAFKA_KEY {{ tb_secret("PRODUCTION_KAFKA_USERNAME", "") }}
695
+ KAFKA_SECRET {{ tb_secret("PRODUCTION_KAFKA_PASSWORD", "") }}
696
+ </connection_content>
697
+ """
675
698
 
676
699
  datasource_instructions = """
677
700
  <datasource_file_instructions>
@@ -805,6 +828,7 @@ When you need to work with resources or data in cloud, add always the --cloud fl
805
828
  </command_calling>
806
829
  <development_instructions>
807
830
  - When asking to create a tinybird data project, if the needed folders are not already created, use the following structure:
831
+ ├── connections
808
832
  ├── copies
809
833
  ├── datasources
810
834
  ├── endpoints
@@ -838,6 +862,8 @@ Follow these instructions when creating or updating .pipe files:
838
862
  {pipe_example}
839
863
  {copy_pipe_instructions}
840
864
  {materialized_pipe_instructions}
865
+ {connection_instructions}
866
+ {connection_example}
841
867
  </pipe_file_instructions>
842
868
  <test_file_instructions>
843
869
  Follow these instructions when creating or updating .yaml files for tests:
@@ -858,6 +884,8 @@ Follow these instructions when evolving a datasource schema:
858
884
  materialized_pipe_instructions=materialized_pipe_instructions,
859
885
  test_instructions=test_instructions,
860
886
  deployment_instructions=deployment_instructions,
887
+ connection_instructions=connection_instructions,
888
+ connection_example=connection_example,
861
889
  )
862
890
 
863
891
 
tinybird/sql_template.py CHANGED
@@ -2399,3 +2399,87 @@ def extract_variables_from_sql(sql: str, params: List[Dict[str, Any]]) -> Dict[s
2399
2399
  return {}
2400
2400
 
2401
2401
  return defaults
2402
+
2403
+
2404
+ def render_template_with_secrets(name: str, content: str, secrets: Optional[Dict[str, str]] = None) -> str:
2405
+ """Renders a template with secrets, allowing for default values.
2406
+
2407
+ Args:
2408
+ name: The name of the template
2409
+ content: The template content
2410
+ secrets: A dictionary mapping secret names to their values
2411
+
2412
+ Returns:
2413
+ The rendered template
2414
+
2415
+ Examples:
2416
+ >>> render_template_with_secrets(
2417
+ ... "my_kafka_connection",
2418
+ ... "KAFKA_BOOTSTRAP_SERVERS {{ tb_secret('PRODUCTION_KAFKA_SERVERS', 'localhost:9092') }}",
2419
+ ... secrets = {'PRODUCTION_KAFKA_SERVERS': 'server1:9092,server2:9092'}
2420
+ ... )
2421
+ 'KAFKA_BOOTSTRAP_SERVERS server1:9092,server2:9092'
2422
+
2423
+ >>> render_template_with_secrets(
2424
+ ... "my_kafka_connection",
2425
+ ... "KAFKA_BOOTSTRAP_SERVERS {{ tb_secret('MISSING_SECRET', 'localhost:9092') }}",
2426
+ ... secrets = {}
2427
+ ... )
2428
+ 'KAFKA_BOOTSTRAP_SERVERS localhost:9092'
2429
+
2430
+ >>> render_template_with_secrets(
2431
+ ... "my_kafka_connection",
2432
+ ... "KAFKA_BOOTSTRAP_SERVERS {{ tb_secret('MISSING_SECRET') }}",
2433
+ ... secrets = {}
2434
+ ... )
2435
+ Traceback (most recent call last):
2436
+ ...
2437
+ tinybird.sql_template.SQLTemplateException: Template Syntax Error: Cannot access secret 'MISSING_SECRET'. Check the secret exists in the Workspace and the token has the required scope.
2438
+ """
2439
+ if not secrets:
2440
+ secrets = {}
2441
+
2442
+ def tb_secret(secret_name: str, default: Optional[str] = None) -> str:
2443
+ """Get a secret value with an optional default.
2444
+
2445
+ Args:
2446
+ secret_name: The name of the secret to retrieve
2447
+ default: The default value to use if the secret is not found
2448
+
2449
+ Returns:
2450
+ The secret value or default
2451
+
2452
+ Raises:
2453
+ SQLTemplateException: If the secret is not found and no default is provided
2454
+ """
2455
+ if secret_name in secrets:
2456
+ return secrets[secret_name]
2457
+ elif default is not None:
2458
+ return default
2459
+ else:
2460
+ raise SQLTemplateException(
2461
+ f"Cannot access secret '{secret_name}'. Check the secret exists in the Workspace and the token has the required scope."
2462
+ )
2463
+
2464
+ # Create the template
2465
+ t = Template(content, name=name)
2466
+
2467
+ try:
2468
+ # Create namespace with our tb_secret function
2469
+ namespace = {"tb_secret": tb_secret}
2470
+
2471
+ # Generate the template without all the extra processing
2472
+ # This directly uses the underlying _generate method of the Template class
2473
+ result = t.generate(**namespace)
2474
+
2475
+ # Convert the result to string
2476
+ if isinstance(result, bytes):
2477
+ return result.decode("utf-8")
2478
+
2479
+ return str(result)
2480
+ except SQLTemplateCustomError as e:
2481
+ raise e
2482
+ except SQLTemplateException as e:
2483
+ raise e
2484
+ except Exception as e:
2485
+ raise SQLTemplateException(f"Error rendering template with secrets: {str(e)}")
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.dev94'
8
- __revision__ = '97387e2'
7
+ __version__ = '0.0.1.dev96'
8
+ __revision__ = 'f1cd799'
tinybird/tb/cli.py CHANGED
@@ -15,6 +15,7 @@ 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
@@ -97,6 +97,8 @@ def build_project(project: Project, tb_client: TinyB, file_changed: Optional[str
97
97
  build = result.get("build")
98
98
  datasources = build.get("new_datasource_names", [])
99
99
  pipes = build.get("new_pipe_names", [])
100
+ connections = build.get("new_data_connector_names", [])
101
+
100
102
  if not file_changed:
101
103
  for ds in datasources:
102
104
  ds_path_str: Optional[str] = next(
@@ -115,6 +117,15 @@ def build_project(project: Project, tb_client: TinyB, file_changed: Optional[str
115
117
  pipe_path_str = pipe_path_str.replace(f"{project.folder}/", "")
116
118
  click.echo(FeedbackManager.info(message=f"✓ {pipe_path_str} created"))
117
119
 
120
+ for connection in connections:
121
+ connection_name = connection
122
+ connection_path_str: Optional[str] = next(
123
+ (p for p in project_files if p.endswith(connection_name + ".connection")), None
124
+ )
125
+ if connection_path_str:
126
+ connection_path_str = connection_path_str.replace(f"{project.folder}/", "")
127
+ click.echo(FeedbackManager.info(message=f"✓ {connection_path_str} created"))
128
+
118
129
  try:
119
130
  for filename in project_files:
120
131
  if filename.endswith(".datasource"):
@@ -31,6 +31,10 @@ on:
31
31
 
32
32
  concurrency: ${{! github.workflow }}-${{! github.event.pull_request.number }}
33
33
 
34
+ env:
35
+ TINYBIRD_HOST: ${{ secrets.TINYBIRD_HOST }}
36
+ TINYBIRD_TOKEN: ${{ secrets.TINYBIRD_TOKEN }}
37
+
34
38
  jobs:
35
39
  ci:
36
40
  runs-on: ubuntu-latest
@@ -50,6 +54,8 @@ jobs:
50
54
  run: tb build
51
55
  - name: Test project
52
56
  run: tb test run
57
+ - name: Deployment check
58
+ run: tb --cloud --host ${{ TINYBIRD_HOST }} --token ${{ TINYBIRD_TOKEN }} deploy --check
53
59
  """
54
60
 
55
61
 
@@ -82,6 +88,7 @@ tinybird_ci_workflow:
82
88
  - cd $CI_PROJECT_DIR/{{ data_project_dir }}
83
89
  - tb build
84
90
  - tb test run
91
+ - tb --cloud --host ${{ TINYBIRD_HOST }} --token ${{ TINYBIRD_TOKEN }} deploy --check
85
92
  services:
86
93
  - name: tinybirdco/tinybird-local:beta
87
94
  alias: tinybird-local
@@ -7,8 +7,11 @@ import json
7
7
  import logging
8
8
  import os
9
9
  import sys
10
+ from os import getcwd
11
+ from pathlib import Path
10
12
  from typing import Any, Callable, Dict, List, Optional, Tuple, Union
11
13
 
14
+ import aiofiles
12
15
  import click
13
16
  import humanfriendly
14
17
  from click import Context
@@ -18,7 +21,6 @@ from tinybird.client import (
18
21
  AuthNoTokenException,
19
22
  TinyB,
20
23
  )
21
- from tinybird.config import get_config
22
24
  from tinybird.tb import __cli__
23
25
  from tinybird.tb.modules.common import (
24
26
  CatchAuthExceptions,
@@ -53,6 +55,7 @@ VERSION = f"{__cli__.__version__} (rev {__cli__.__revision__})"
53
55
  help="Prints internal representation, can be combined with any command to get more information.",
54
56
  )
55
57
  @click.option("--token", help="Use auth token, defaults to TB_TOKEN envvar, then to the .tinyb file.")
58
+ @click.option("--user-token", help="Use user token, defaults to TB_USER_TOKEN envvar, then to the .tinyb file.")
56
59
  @click.option("--host", help="Use custom host, defaults to TB_HOST envvar, then to https://api.tinybird.co")
57
60
  @click.option("--show-tokens", is_flag=True, default=False, help="Enable the output of tokens.")
58
61
  @click.option("--cloud/--local", is_flag=True, default=False, help="Run against cloud or local.")
@@ -65,6 +68,7 @@ async def cli(
65
68
  ctx: Context,
66
69
  debug: bool,
67
70
  token: str,
71
+ user_token: str,
68
72
  host: str,
69
73
  show_tokens: bool,
70
74
  cloud: bool,
@@ -92,7 +96,9 @@ async def cli(
92
96
  config_temp.set_token(token)
93
97
  if host:
94
98
  config_temp.set_host(host)
95
- if token or host:
99
+ if user_token:
100
+ config_temp.set_user_token(user_token)
101
+ if token or host or user_token:
96
102
  await try_update_config_with_remote(config_temp, auto_persist=False, raise_on_errors=False)
97
103
 
98
104
  # Overwrite token and host with env vars manually, without resorting to click.
@@ -104,8 +110,10 @@ async def cli(
104
110
  token = os.environ.get("TB_TOKEN", "")
105
111
  if not host and "TB_HOST" in os.environ:
106
112
  host = os.environ.get("TB_HOST", "")
113
+ if not user_token and "TB_USER_TOKEN" in os.environ:
114
+ user_token = os.environ.get("TB_USER_TOKEN", "")
107
115
 
108
- config = await get_config(host, token, config_file=config_temp._path)
116
+ config = await get_config(host, token, user_token=user_token, config_file=config_temp._path)
109
117
  client = _get_tb_client(config.get("token", None), config["host"])
110
118
  folder = os.path.join(config_temp._path.replace(".tinyb", ""), config.get("cwd", os.getcwd()))
111
119
  project = Project(folder=folder)
@@ -398,7 +406,7 @@ async def create_ctx_client(ctx: Context, config: Dict[str, Any], cloud: bool, b
398
406
  if command in commands_without_ctx_client:
399
407
  return None
400
408
 
401
- commands_always_cloud = ["pull", "playground"]
409
+ commands_always_cloud = ["pull", "playground", "infra"]
402
410
  commands_always_build = ["build", "test", "dev", "create"]
403
411
  commands_always_local: List[str] = []
404
412
  if (
@@ -422,3 +430,34 @@ def get_target_env(cloud: bool, build: bool) -> str:
422
430
  if build:
423
431
  return "build"
424
432
  return "local"
433
+
434
+
435
+ async def get_config(
436
+ host: str,
437
+ token: Optional[str],
438
+ user_token: Optional[str],
439
+ semver: Optional[str] = None,
440
+ config_file: Optional[str] = None,
441
+ ) -> Dict[str, Any]:
442
+ if host:
443
+ host = host.rstrip("/")
444
+
445
+ config = {}
446
+ try:
447
+ async with aiofiles.open(config_file or Path(getcwd()) / ".tinyb") as file:
448
+ res = await file.read()
449
+ config = json.loads(res)
450
+ except OSError:
451
+ pass
452
+ except json.decoder.JSONDecodeError:
453
+ click.echo(FeedbackManager.error_load_file_config(config_file=config_file))
454
+ return config
455
+
456
+ config["token_passed"] = token
457
+ config["token"] = token or config.get("token", None)
458
+ config["user_token"] = user_token or config.get("user_token", None)
459
+ config["semver"] = semver or config.get("semver", None)
460
+ config["host"] = host or config.get("host", "https://api.europe-west2.gcp.tinybird.co")
461
+ config["workspaces"] = config.get("workspaces", [])
462
+ config["cwd"] = config.get("cwd", getcwd())
463
+ return config
@@ -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:
@@ -9,7 +9,7 @@ from tinybird.client import TinyB
9
9
  from tinybird.prompts import create_prompt, mock_prompt, rules_prompt
10
10
  from tinybird.tb.modules.cicd import init_cicd
11
11
  from tinybird.tb.modules.cli import cli
12
- from tinybird.tb.modules.common import _generate_datafile, check_user_token_with_client, coro, generate_datafile
12
+ from tinybird.tb.modules.common import _generate_datafile, coro, generate_datafile
13
13
  from tinybird.tb.modules.config import CLIConfig
14
14
  from tinybird.tb.modules.datafile.fixture import persist_fixture
15
15
  from tinybird.tb.modules.exceptions import CLIException
@@ -44,6 +44,7 @@ async def create(
44
44
  local_client: TinyB = ctx.ensure_object(dict)["client"]
45
45
  project: Project = ctx.ensure_object(dict)["project"]
46
46
  config = CLIConfig.get_project_config()
47
+ ctx_config = ctx.ensure_object(dict)["config"]
47
48
 
48
49
  # If folder is provided, rewrite the config and project folder
49
50
  if folder:
@@ -65,15 +66,14 @@ async def create(
65
66
  folder_path.mkdir()
66
67
 
67
68
  try:
68
- tb_client = config.get_client()
69
+ tb_client = config.get_client(token=ctx_config.get("token"), host=ctx_config.get("host"))
69
70
  user_token: Optional[str] = None
70
71
  created_something = False
71
72
  if prompt:
72
73
  try:
73
- user_token = config.get_user_token()
74
+ user_token = ctx_config.get("user_token")
74
75
  if not user_token:
75
76
  raise CLIException("No user token found")
76
- await check_user_token_with_client(tb_client, token=user_token)
77
77
  except Exception as e:
78
78
  click.echo(
79
79
  FeedbackManager.error(
@@ -144,7 +144,7 @@ async def create(
144
144
  click.echo(FeedbackManager.error(message=f"Error: {str(e)}"))
145
145
 
146
146
 
147
- PROJECT_PATHS = ("datasources", "endpoints", "materializations", "copies", "pipes", "fixtures", "tests")
147
+ PROJECT_PATHS = ("datasources", "endpoints", "materializations", "copies", "pipes", "fixtures", "tests", "connections")
148
148
 
149
149
 
150
150
  def validate_project_structure(folder: str) -> bool:
@@ -248,6 +248,7 @@ TYPE ENDPOINT
248
248
  resources = parse_xml(result, "resource")
249
249
  datasources = []
250
250
  pipes = []
251
+ connections = []
251
252
  for resource_xml in resources:
252
253
  resource_type = extract_xml(resource_xml, "type")
253
254
  name = extract_xml(resource_xml, "name")
@@ -260,6 +261,8 @@ TYPE ENDPOINT
260
261
  datasources.append(resource)
261
262
  elif resource_type.lower() == "pipe":
262
263
  pipes.append(resource)
264
+ elif resource_type.lower() == "connection":
265
+ connections.append(resource)
263
266
 
264
267
  for ds in datasources:
265
268
  content = ds["content"].replace("```", "")
@@ -279,6 +282,12 @@ TYPE ENDPOINT
279
282
  generate_pipe_file(pipe["name"], content, folder)
280
283
  created_any_resource = True
281
284
 
285
+ for conn in connections:
286
+ content = conn["content"].replace("```", "")
287
+ filename = f"{conn['name']}.connection"
288
+ generate_connection_file(conn["name"], content, folder)
289
+ created_any_resource = True
290
+
282
291
  return result, created_any_resource
283
292
 
284
293
 
@@ -333,6 +342,17 @@ def generate_pipe_file(name: str, content: str, folder: str) -> Path:
333
342
  return f.relative_to(folder)
334
343
 
335
344
 
345
+ def generate_connection_file(name: str, content: str, folder: str) -> Path:
346
+ base = Path(folder) / "connections"
347
+ if not base.exists():
348
+ base.mkdir()
349
+ f = base / (f"{name}.connection")
350
+ with open(f"{f}", "w") as file:
351
+ file.write(content)
352
+ click.echo(FeedbackManager.info_file_created(file=f.relative_to(folder)))
353
+ return f.relative_to(folder)
354
+
355
+
336
356
  def create_rules(folder: str, source: str, agent: str):
337
357
  if agent == "cursor":
338
358
  extension = ".cursorrules"
@@ -115,6 +115,7 @@ class DataFileExtensions:
115
115
  PIPE = ".pipe"
116
116
  DATASOURCE = ".datasource"
117
117
  INCL = ".incl"
118
+ CONNECTION = ".connection"
118
119
 
119
120
 
120
121
  class CopyModes:
@@ -241,6 +242,11 @@ class Datafile:
241
242
  raise DatafileValidationError(f"Materialized node {repr(node['name'])} missing target datasource")
242
243
  if node.get("type", "").lower() == PipeNodeTypes.COPY:
243
244
  self.validate_copy_node(node)
245
+ for token in self.tokens:
246
+ if token["permission"].upper() != "READ":
247
+ raise DatafileValidationError(
248
+ f"Invalid permission {token['permission']} for token {token['token_name']}. Only READ is allowed for pipes"
249
+ )
244
250
  elif self.kind == DatafileKind.datasource:
245
251
  # TODO(eclbg):
246
252
  # [x] Just one node
@@ -253,6 +259,11 @@ class Datafile:
253
259
  node = self.nodes[0]
254
260
  if "schema" not in node:
255
261
  raise DatafileValidationError("SCHEMA is mandatory")
262
+ for token in self.tokens:
263
+ if token["permission"].upper() not in {"READ", "APPEND"}:
264
+ raise DatafileValidationError(
265
+ f"Invalid permission {token['permission']} for token {token['token_name']}. Only READ and APPEND are allowed for datasources"
266
+ )
256
267
  else:
257
268
  # We cannot validate a datafile whose kind is unknown
258
269
  pass
@@ -1274,16 +1285,28 @@ def parse(
1274
1285
 
1275
1286
  @multiline_not_supported
1276
1287
  def add_token(*args: str, **kwargs: Any) -> None: # token_name, permissions):
1277
- # lineno = kwargs["lineno"]
1288
+ lineno = kwargs["lineno"]
1278
1289
  if len(args) < 2:
1279
1290
  raise DatafileSyntaxError(
1280
- message='TOKEN takes two params: token name and permissions e.g TOKEN "read api token" READ',
1291
+ message='TOKEN takes two params: token name and permission e.g TOKEN "read api token" READ',
1281
1292
  lineno=lineno,
1282
1293
  pos=1,
1283
1294
  )
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]})
1295
+ if len(args) > 2:
1296
+ raise DatafileSyntaxError(
1297
+ f"Invalid number of arguments for TOKEN command: {len(args)}. Expected 2 arguments: token name and permission",
1298
+ lineno=lineno,
1299
+ pos=len("token") + len(args[0]) + 3, # Naive handling of whitespace. Assuming there's 2
1300
+ )
1301
+ permission = args[1]
1302
+ if permission.upper() not in ["READ", "APPEND"]:
1303
+ raise DatafileSyntaxError(
1304
+ f"Invalid permission: {permission}. Only READ and APPEND are supported",
1305
+ lineno=lineno,
1306
+ pos=len("token") + len(args[0]) + 3, # Naive handling of whitespace. Assuming there's 2
1307
+ )
1308
+ token_name = _unquote(args[0])
1309
+ doc.tokens.append({"token_name": token_name, "permission": permission.upper()})
1287
1310
 
1288
1311
  @not_supported_yet()
1289
1312
  def include(*args: str, **kwargs: Any) -> None:
@@ -1678,6 +1701,7 @@ def get_project_filenames(folder: str, with_vendor=False) -> List[str]:
1678
1701
  f"{folder}/sinks/*.pipe",
1679
1702
  f"{folder}/copies/*.pipe",
1680
1703
  f"{folder}/playgrounds/*.pipe",
1704
+ f"{folder}/connections/*.connection",
1681
1705
  ]
1682
1706
  if with_vendor:
1683
1707
  folders.append(f"{folder}/vendor/**/**/*.datasource")
@@ -1,5 +1,5 @@
1
1
  import os
2
- from typing import Optional
2
+ from typing import List, Optional
3
3
 
4
4
  import click
5
5
 
@@ -21,6 +21,7 @@ def parse_datasource(
21
21
  skip_eval: bool = False,
22
22
  hide_folders: bool = False,
23
23
  add_context_to_datafile_syntax_errors: bool = True,
24
+ secrets: Optional[List[str]] = None,
24
25
  ) -> Datafile:
25
26
  basepath = ""
26
27
  if not content:
@@ -1,5 +1,5 @@
1
1
  import os
2
- from typing import Optional
2
+ from typing import List, Optional
3
3
 
4
4
  import click
5
5
 
@@ -23,6 +23,7 @@ def parse_pipe(
23
23
  skip_eval: bool = False,
24
24
  hide_folders: bool = False,
25
25
  add_context_to_datafile_syntax_errors: bool = True,
26
+ secrets: Optional[List[str]] = None,
26
27
  ) -> Datafile:
27
28
  basepath = ""
28
29
  if not content:
@@ -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,507 @@
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, hidden=True)
159
+ @click.pass_context
160
+ def infra(ctx: Context) -> None:
161
+ """Infra commands."""
162
+
163
+
164
+ @infra.command(name="ls", hidden=True)
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(apply_result.stdout)
459
+ click.echo("DNS record created successfully!")
460
+
461
+ # Get the DNS name from Terraform output
462
+ dns_output = subprocess.run(
463
+ ["terraform", "-chdir=infra", "output", "tinybird_dns"],
464
+ capture_output=True,
465
+ text=True,
466
+ )
467
+
468
+ if dns_output.returncode == 0:
469
+ dns_name = dns_output.stdout.strip().replace('"', "")
470
+ click.echo("\nDNS record created successfully!")
471
+ click.echo(
472
+ "\nWaiting up to 5 minutes for HTTPS endpoint to become available..."
473
+ )
474
+
475
+ import time
476
+
477
+ import requests
478
+
479
+ max_attempts = 30 # 30 attempts * 10 seconds = 5 minutes
480
+ attempt = 0
481
+ while attempt < max_attempts:
482
+ attempt += 1
483
+ try:
484
+ response = requests.get(
485
+ f"https://{dns_name}", allow_redirects=False, timeout=5
486
+ )
487
+ response.raise_for_status()
488
+ click.echo("\n✅ HTTPS endpoint is now accessible!")
489
+ break
490
+ except requests.RequestException:
491
+ if attempt == max_attempts:
492
+ click.echo("\n⚠️ HTTPS endpoint not accessible after 5 minutes")
493
+ click.echo(
494
+ " This might be due to DNS propagation or the Load Balancer provisioning delays"
495
+ )
496
+ click.echo(
497
+ " Please try accessing the URL manually in a few minutes"
498
+ )
499
+ else:
500
+ click.echo(
501
+ f"Attempt {attempt}/{max_attempts}: Not ready yet, waiting 10 seconds..."
502
+ )
503
+ time.sleep(10)
504
+ else:
505
+ click.echo(
506
+ f"\nYour Tinybird instance should be available at: https://tinybird.{domain}"
507
+ )
@@ -6,7 +6,7 @@ import click
6
6
  from tinybird.client import TinyB
7
7
  from tinybird.prompts import mock_prompt
8
8
  from tinybird.tb.modules.cli import cli
9
- from tinybird.tb.modules.common import CLIException, check_user_token_with_client, coro, push_data
9
+ from tinybird.tb.modules.common import CLIException, coro, push_data
10
10
  from tinybird.tb.modules.config import CLIConfig
11
11
  from tinybird.tb.modules.datafile.fixture import persist_fixture, persist_fixture_sql
12
12
  from tinybird.tb.modules.feedback_manager import FeedbackManager
@@ -39,6 +39,7 @@ async def mock(ctx: click.Context, datasource: str, rows: int, prompt: str) -> N
39
39
  try:
40
40
  tb_client: TinyB = ctx.ensure_object(dict)["client"]
41
41
  project: Project = ctx.ensure_object(dict)["project"]
42
+ ctx_config = ctx.ensure_object(dict)["config"]
42
43
  env = ctx.ensure_object(dict)["env"]
43
44
  datasource_path = Path(datasource)
44
45
  datasource_name = datasource
@@ -56,13 +57,12 @@ async def mock(ctx: click.Context, datasource: str, rows: int, prompt: str) -> N
56
57
 
57
58
  datasource_content = datasource_path.read_text()
58
59
  config = CLIConfig.get_project_config()
59
- user_client = config.get_client()
60
- user_token = config.get_user_token()
60
+ user_client = config.get_client(token=ctx_config.get("token"), host=ctx_config.get("host"))
61
+ user_token = ctx_config.get("user_token")
61
62
 
62
63
  try:
63
64
  if not user_token:
64
65
  raise CLIException("No user token found")
65
- await check_user_token_with_client(user_client, token=user_token)
66
66
  except Exception:
67
67
  click.echo(FeedbackManager.error(message="This action requires authentication. Run 'tb login' first."))
68
68
  return
@@ -51,6 +51,10 @@ class Project:
51
51
  def pipes(self) -> List[str]:
52
52
  return sorted([Path(f).stem for f in glob.glob(f"{self.path}/**/*.pipe", recursive=False)])
53
53
 
54
+ @property
55
+ def connections(self) -> List[str]:
56
+ return sorted([Path(f).stem for f in glob.glob(f"{self.path}/**/*.connection", recursive=False)])
57
+
54
58
  def get_pipe_datafile(self, filename: str) -> Optional[Datafile]:
55
59
  try:
56
60
  return parse_pipe(filename)
@@ -22,7 +22,7 @@ from tinybird.tb.modules.shell import Shell
22
22
 
23
23
 
24
24
  class WatchProjectHandler(PatternMatchingEventHandler):
25
- valid_extensions = [".datasource", ".pipe", ".ndjson", ".sql"]
25
+ valid_extensions = [".datasource", ".pipe", "connection", ".ndjson", ".sql"]
26
26
 
27
27
  def __init__(self, shell: Shell, project: Project, process: Callable):
28
28
  self.shell = shell
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: tinybird
3
- Version: 0.0.1.dev94
3
+ Version: 0.0.1.dev96
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -6,32 +6,33 @@ tinybird/context.py,sha256=FfqYfrGX_I7PKGTQo93utaKPDNVYWelg4Hsp3evX5wM,1291
6
6
  tinybird/datatypes.py,sha256=XNypumfqNjsvLJ5iNXnbVHRvAJe0aQwI3lS6Cxox-e0,10979
7
7
  tinybird/feedback_manager.py,sha256=YSjtFDJvc8y66j2J0iIkb3SVzDdYAJbzFL-JPQ26pak,68761
8
8
  tinybird/git_settings.py,sha256=Sw_8rGmribEFJ4Z_6idrVytxpFYk7ez8ei0qHULzs3E,3934
9
- tinybird/prompts.py,sha256=rDIrkK5S1NJ0bIFjULsCYnGApv9Vc0jmR4tdRuJhM8c,32953
9
+ tinybird/prompts.py,sha256=syxUvbdkzxIrIK8pkS-Y7nzBx7DMS2Z9HaiXx7AjvnQ,33912
10
10
  tinybird/sql.py,sha256=LBi74GxhNAYTb6m2-KNGpAkguSKh7rcvBbERbE7nalA,46195
11
- tinybird/sql_template.py,sha256=1qO6FaEe9rdLAKIORZsNk53XklLeP6oLnMhlw0PBUcI,96091
11
+ tinybird/sql_template.py,sha256=VH8io4n5eP2z6TEw111d8hcA9FKQerFjprKKCW2MODw,99127
12
12
  tinybird/sql_template_fmt.py,sha256=KUHdj5rYCYm_rKKdXYSJAE9vIyXUQLB0YSZnUXHeBlY,10196
13
13
  tinybird/sql_toolset.py,sha256=32SNvxRFKQYWTvYPMJ_u3ukcd1hKZyEqx8T2cv2412w,14697
14
14
  tinybird/syncasync.py,sha256=IPnOx6lMbf9SNddN1eBtssg8vCLHMt76SuZ6YNYm-Yk,27761
15
15
  tinybird/tornado_template.py,sha256=jjNVDMnkYFWXflmT8KU_Ssbo5vR8KQq3EJMk5vYgXRw,41959
16
16
  tinybird/ch_utils/constants.py,sha256=aYvg2C_WxYWsnqPdZB1ZFoIr8ZY-XjUXYyHKE9Ansj0,3890
17
17
  tinybird/ch_utils/engine.py,sha256=BZuPM7MFS7vaEKK5tOMR2bwSAgJudPrJt27uVEwZmTY,40512
18
- tinybird/tb/__cli__.py,sha256=NDM11e05WwpBFqDae6OVPT3I2exwaDoMuTDk7bU6M7Q,251
19
- tinybird/tb/cli.py,sha256=D_SjS5SU0eTcsKs06I1W39vxQTUEt2TOIYpWAWwYQqA,1102
18
+ tinybird/tb/__cli__.py,sha256=quTbjPYY2p2amkeaFu_wMFFcJ0hoA3moA2Jgy9XuEeo,251
19
+ tinybird/tb/cli.py,sha256=H_HaZhkimKgkryYXpBjHfY9Qtg-ZORiONU3psDNpzDk,1135
20
20
  tinybird/tb/modules/auth.py,sha256=L1IatO2arRSzys3t8px8xVt8uPWUL5EVD0sFzAV_uVU,9022
21
- tinybird/tb/modules/build.py,sha256=-lRGBxKtuipmyl3pmiGcfp67fH1Ed-COfHAZKdgLIWo,10483
22
- tinybird/tb/modules/cicd.py,sha256=T0lb9u_bDdTUVe8TwNNb1qQ5KnSPHMVjqPfKF4BBNBw,5347
23
- tinybird/tb/modules/cli.py,sha256=-xZ6-yrbKxcA-ou7K02H6gsMeZuvWsuEMNSleU_G9Fc,16467
24
- tinybird/tb/modules/common.py,sha256=Qrc-fOyBOO2Z57pZyvnOQaaLToEgj13ZR1s2LmgJB9w,81246
21
+ tinybird/tb/modules/build.py,sha256=h5drdmDFX8NHts9dA2Zepao7KSgMAl3DZGyFufVZP78,11085
22
+ tinybird/tb/modules/cicd.py,sha256=SesVtrs7WlP1KUlSSpoDA9TFP_3gUDKlIwWTMINAY00,5665
23
+ tinybird/tb/modules/cli.py,sha256=j6j9CKx4YnsRTLYdE47K8ilkoj-Xf9lq7Wm2LVgQ6Lc,17888
24
+ tinybird/tb/modules/common.py,sha256=EU33c_HKU8yCrj4qfbDTHFZwHEpzZ5vBmZcPxqT15DQ,81307
25
25
  tinybird/tb/modules/config.py,sha256=BVZg-4f_R3vJTwCChXY2AXaH67SRk62xoP_IymquosI,11404
26
26
  tinybird/tb/modules/connection.py,sha256=WKeDxbTpSsQ1PUmsT730g3S5RT2PtR5mPpVEanD1nbM,3933
27
27
  tinybird/tb/modules/copy.py,sha256=MAVqKip8_QhOYq99U_XuqSO6hCLJEh5sFtbhcXtI3SI,5802
28
- tinybird/tb/modules/create.py,sha256=KjotVfIQLfcPyQBykTHnPLn4ikrm6qqeMcbRE1d-6Jo,13280
28
+ tinybird/tb/modules/create.py,sha256=VvY5C0GNM_j3ZPK4ba8yq3cephJ356PaAsBWR-zoPXs,14054
29
29
  tinybird/tb/modules/datasource.py,sha256=dNCK9iCR2xPLfwqqwg2ixyE6NuoVEiJU2mBZBmOYrVY,16906
30
- tinybird/tb/modules/deployment.py,sha256=ZX9fTxenVN0oCbJTa_5Ap1bmyqT8zt9PMBmBP949Ag4,18109
30
+ tinybird/tb/modules/deployment.py,sha256=KMpIahNGUdea3KcH0jTMEnzQ-7zg7M4esd2zTfvaC-k,19337
31
31
  tinybird/tb/modules/endpoint.py,sha256=EhVoGAXsFz-83Fiwj1gI-I73iRRvL49d0W81un7hvPE,12080
32
32
  tinybird/tb/modules/exceptions.py,sha256=4A2sSjCEqKUMqpP3WI00zouCWW4uLaghXXLZBSw04mY,3363
33
33
  tinybird/tb/modules/feedback_manager.py,sha256=7nNiOx7OMebiheLED1r0d75SbuXCNxyBmF4e20rCBNc,69511
34
34
  tinybird/tb/modules/fmt.py,sha256=qpf9APqKTKL2uphNgdbj4OMVyLkAxZn6dn4eHF99L5g,3553
35
+ tinybird/tb/modules/infra.py,sha256=TeeCcJQ_9fm_sDfZnZ8E2UaEOKaBzRHGm5KMKL01O48,20375
35
36
  tinybird/tb/modules/job.py,sha256=956Pj8BEEsiD2GZsV9RKKVM3I_CveOLgS82lykO5ukk,2963
36
37
  tinybird/tb/modules/llm.py,sha256=AC0VSphTOM2t-v1_3NLvNN_FIbgMo4dTyMqIv5nniPo,835
37
38
  tinybird/tb/modules/llm_utils.py,sha256=nS9r4FAElJw8yXtmdYrx-rtI2zXR8qXfi1QqUDCfxvg,3469
@@ -40,11 +41,11 @@ tinybird/tb/modules/local_common.py,sha256=Uty8vhn4FxRASqcMldpbadKcDiOeLx4PK01Nk
40
41
  tinybird/tb/modules/login.py,sha256=NB-evr7b00ChKPulX7c8YLN3EX6cr0CwALN0wroAq3o,6147
41
42
  tinybird/tb/modules/logout.py,sha256=ULooy1cDBD02-r7voZmhV7udA0ML5tVuflJyShrh56Y,1022
42
43
  tinybird/tb/modules/materialization.py,sha256=r8Q9HXcYEmfrEzP4WpiasCKDJdSkTPaAKJtZMoJKhi8,5749
43
- tinybird/tb/modules/mock.py,sha256=3q4i6CXKcS-zsgevbN_zpAP4AnB9_WIVxmVSJV3FNPQ,3881
44
+ tinybird/tb/modules/mock.py,sha256=9VKlp2bO2NsRgqF03SrFv_8OvAoHeRcOU89TiBRFfqY,3891
44
45
  tinybird/tb/modules/open.py,sha256=s3eJLFtF6OnXX5OLZzBz58dYaG-TGDCYFSJHttm919g,1317
45
46
  tinybird/tb/modules/pipe.py,sha256=gcLz0qHgwKDLsWFY3yFLO9a0ETAV1dFbI8YeLHi9460,2429
46
47
  tinybird/tb/modules/playground.py,sha256=bN0whphoSO6p1_u3b6OAUoc3ieG5Cl3qNXwt2HcUOp8,4834
47
- tinybird/tb/modules/project.py,sha256=ei0TIAuRksdV2g2FJqByuV4DPyivQGrZ42z_eQDNBgI,2963
48
+ tinybird/tb/modules/project.py,sha256=Jpoi-3ybIixN8bHCqOMnuaKByXjrdN_Gvlpa24L-e4U,3124
48
49
  tinybird/tb/modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
49
50
  tinybird/tb/modules/secret.py,sha256=xxzfKxfFN7GORib1WslCaFDHt_dgnjmfOewyptPU_VM,2820
50
51
  tinybird/tb/modules/shell.py,sha256=a98W4L4gfrmxEyybtu6S4ENXrBYtgNASB5e_evuXQvI,13936
@@ -53,22 +54,22 @@ tinybird/tb/modules/tag.py,sha256=anPmMUBc-TbFovlpFi8GPkKA18y7Y0GczMsMms5TZsU,35
53
54
  tinybird/tb/modules/telemetry.py,sha256=Hh2Io8ZPROSunbOLuMvuIFU4TqwWPmQTqal4WS09K1A,10449
54
55
  tinybird/tb/modules/test.py,sha256=FUU-drY8mdjNoKsw16O5ZqvYvZqzycrZBEpSwbhGDUE,11456
55
56
  tinybird/tb/modules/token.py,sha256=OhqLFpCHVfYeBCxJ0n7n2qoho9E9eGcUfHgL7R1MUVQ,13485
56
- tinybird/tb/modules/watch.py,sha256=qMQhewRSso1AFSEFLuyeyGFA8Lxf9ccYJxmVdPU1BgM,8808
57
+ tinybird/tb/modules/watch.py,sha256=poNJOUNDESDNn80H2dHvE6X6pIu-t9MZFi59_TxVN2U,8822
57
58
  tinybird/tb/modules/workspace.py,sha256=SYkEULv_Gg8FhnAnZspengzyT5N4w0wjsvWWZ3vy3Ho,7753
58
59
  tinybird/tb/modules/workspace_members.py,sha256=Vb5XEaKmkfONyfg2MS5EcpwolMvv7GLwFS5m2EuobT8,8726
59
60
  tinybird/tb/modules/datafile/build.py,sha256=seGFSvmgyRrAM1-icsKBkuog3WccfGUYFTPT-xoA5W8,50940
60
61
  tinybird/tb/modules/datafile/build_common.py,sha256=rT7VJ5mnQ68R_8US91DAtkusfvjWuG_NObOzNgtN_ko,4562
61
62
  tinybird/tb/modules/datafile/build_datasource.py,sha256=VjxaKKLZhPYt3XHOyMmfoqEAWAPI5D78T-8FOaN77MY,17355
62
63
  tinybird/tb/modules/datafile/build_pipe.py,sha256=Tf49kZmXub45qGcePFfqGO7p-FH5eYM46DtVI3AQJEc,11358
63
- tinybird/tb/modules/datafile/common.py,sha256=J9hXDdi27Quc0AkzoS2ChlEYwDBDePcW2sZ1qruZKdY,80091
64
+ tinybird/tb/modules/datafile/common.py,sha256=i9Gvhz3JiR58MRBcYZDwqTqamQOj-46TnHU8Hm8bqmg,81399
64
65
  tinybird/tb/modules/datafile/diff.py,sha256=-0J7PsBO64T7LOZSkZ4ZFHHCPvT7cKItnJkbz2PkndU,6754
65
66
  tinybird/tb/modules/datafile/exceptions.py,sha256=8rw2umdZjtby85QbuRKFO5ETz_eRHwUY5l7eHsy1wnI,556
66
67
  tinybird/tb/modules/datafile/fixture.py,sha256=si-9LB-LdKQSWDtVW82xDrHtFfko5bgBG1cvjqqrcPU,1064
67
68
  tinybird/tb/modules/datafile/format_common.py,sha256=WaNV4tXrQU5gjV6MJP-5TGqg_Bre6ilNS8emvFl-X3c,1967
68
69
  tinybird/tb/modules/datafile/format_datasource.py,sha256=gpRsGnDEMxEo0pIlEHXKvyuwKIpqJJUCN9JRSiDYs_4,6156
69
70
  tinybird/tb/modules/datafile/format_pipe.py,sha256=58iSTrJ5lg-IsbpX8TQumQTuZ6UIotMsCIkNJd1M-pM,7418
70
- tinybird/tb/modules/datafile/parse_datasource.py,sha256=kk35PzesoJOd0LKjYp4kOyCwq4Qo4TiZnoI9qcXjB4k,1519
71
- tinybird/tb/modules/datafile/parse_pipe.py,sha256=snoy8Ac_Sat7LIXLAKzxjJSl2-TKg9FaZTooxrx6muE,3420
71
+ tinybird/tb/modules/datafile/parse_datasource.py,sha256=BNof0FnaIVZUG5ORFtZSw-gUmWID4o2ZQLVgfVIuHRI,1566
72
+ tinybird/tb/modules/datafile/parse_pipe.py,sha256=tBjh3-I0iq7JdtB84RPYFrUlUOF2ZYWgQ_mwW5SPgmI,3467
72
73
  tinybird/tb/modules/datafile/pipe_checker.py,sha256=LnDLGIHLJ3N7qHb2ptEbPr8CoczNfGwpjOY8EMdxfHQ,24649
73
74
  tinybird/tb/modules/datafile/playground.py,sha256=mVQNSLCXpBhupI3iJqRDdE7BJtkr8JjVhHxav3pYV2E,56533
74
75
  tinybird/tb/modules/datafile/pull.py,sha256=vcjMUbjnZ9XQMGmL33J3ElpbXBTat8Yzp-haeDggZd4,5967
@@ -80,8 +81,8 @@ tinybird/tb_cli_modules/config.py,sha256=IsgdtFRnUrkY8-Zo32lmk6O7u3bHie1QCxLwgp4
80
81
  tinybird/tb_cli_modules/exceptions.py,sha256=pmucP4kTF4irIt7dXiG-FcnI-o3mvDusPmch1L8RCWk,3367
81
82
  tinybird/tb_cli_modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
82
83
  tinybird/tb_cli_modules/telemetry.py,sha256=Hh2Io8ZPROSunbOLuMvuIFU4TqwWPmQTqal4WS09K1A,10449
83
- tinybird-0.0.1.dev94.dist-info/METADATA,sha256=7WmXBgGh3t5FjEws21d7CibjcqSWh13R2_gxP1KUbIc,2585
84
- tinybird-0.0.1.dev94.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
85
- tinybird-0.0.1.dev94.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
86
- tinybird-0.0.1.dev94.dist-info/top_level.txt,sha256=VqqqEmkAy7UNaD8-V51FCoMMWXjLUlR0IstvK7tJYVY,54
87
- tinybird-0.0.1.dev94.dist-info/RECORD,,
84
+ tinybird-0.0.1.dev96.dist-info/METADATA,sha256=92bDicSZDCfaVpfRIa9lJaibh_qH1nEIxEd7UVvCDEQ,2585
85
+ tinybird-0.0.1.dev96.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
86
+ tinybird-0.0.1.dev96.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
87
+ tinybird-0.0.1.dev96.dist-info/top_level.txt,sha256=VqqqEmkAy7UNaD8-V51FCoMMWXjLUlR0IstvK7tJYVY,54
88
+ tinybird-0.0.1.dev96.dist-info/RECORD,,