tinybird 0.0.1.dev99__py3-none-any.whl → 0.0.1.dev101__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/client.py CHANGED
@@ -915,6 +915,16 @@ class TinyB:
915
915
  }
916
916
  return await self._req(f"/v1/infra?{urlencode(params)}", method="POST")
917
917
 
918
+ async def infra_update(self, infra_id: str, organization_id: str, name: str, host: str) -> Dict[str, Any]:
919
+ params = {
920
+ "organization_id": organization_id,
921
+ }
922
+ if name:
923
+ params["name"] = name
924
+ if host:
925
+ params["host"] = host
926
+ return await self._req(f"/v1/infra/{infra_id}?{urlencode(params)}", method="PUT")
927
+
918
928
  async def infra_list(self, organization_id: str) -> List[Dict[str, Any]]:
919
929
  data = await self._req(f"/v1/infra?organization_id={organization_id}")
920
930
  return data.get("infras", [])
tinybird/sql.py CHANGED
@@ -174,7 +174,7 @@ def try_to_fix_nullable_in_simple_aggregating_function(t: str) -> Optional[str]:
174
174
  return result
175
175
 
176
176
 
177
- def schema_to_sql_columns(schema: List[Dict[str, Any]]) -> List[str]:
177
+ def schema_to_sql_columns(schema: List[Dict[str, Any]], skip_jsonpaths: bool = False) -> List[str]:
178
178
  """return an array with each column in SQL
179
179
  >>> schema_to_sql_columns([{'name': 'temperature', 'type': 'Float32', 'codec': None, 'default_value': None, 'nullable': False, 'normalized_name': 'temperature'}, {'name': 'temperature_delta', 'type': 'Float32', 'codec': 'CODEC(Delta(4), LZ4))', 'default_value': 'MATERIALIZED temperature', 'nullable': False, 'normalized_name': 'temperature_delta'}])
180
180
  ['`temperature` Float32', '`temperature_delta` Float32 MATERIALIZED temperature CODEC(Delta(4), LZ4))']
@@ -198,7 +198,7 @@ def schema_to_sql_columns(schema: List[Dict[str, Any]]) -> List[str]:
198
198
  else:
199
199
  _type = x["type"]
200
200
  parts = [col_name(name, backquotes=True), _type]
201
- if x.get("jsonpath", None):
201
+ if x.get("jsonpath", None) and not skip_jsonpaths:
202
202
  parts.append(f"`json:{x['jsonpath']}`")
203
203
  if "default_value" in x and x["default_value"] not in ("", None):
204
204
  parts.append(x["default_value"])
tinybird/sql_toolset.py CHANGED
@@ -10,6 +10,9 @@ from toposort import toposort
10
10
 
11
11
  from tinybird.ch_utils.constants import COPY_ENABLED_TABLE_FUNCTIONS, ENABLED_TABLE_FUNCTIONS
12
12
 
13
+ # VALID_REMOTE is used to explicitly vet queries sent to sql_toolset. In this module, when a table used in a query has
14
+ # VALID_REMOTE in the database portion (as in (VALID_REMOTE, "select * from cluster(tinybird, public.t_blabla)"), the
15
+ # query is not blocked, even if the rhs (the "select * ..." bit) is not found in the various collections of allowed tables.
13
16
  VALID_REMOTE = "VALID_REMOTE"
14
17
 
15
18
 
@@ -351,14 +354,24 @@ def is_invalid_resource(
351
354
  valid_tables: Optional[Set[Tuple[str, str]]] = None,
352
355
  ) -> bool:
353
356
  return is_invalid_resource_from_other_workspace(
354
- r, database, default_database, _replaced_with
357
+ r, database, default_database, _replaced_with, valid_tables
355
358
  ) or is_invalid_resource_from_current_workspace(r, database, default_database, _replaced_with, valid_tables)
356
359
 
357
360
 
358
361
  def is_invalid_resource_from_other_workspace(
359
- r: Tuple[str, str], database: str, default_database: str, _replaced_with: Set[Tuple[str, str]]
362
+ r: Tuple[str, str],
363
+ database: str,
364
+ default_database: str,
365
+ _replaced_with: Set[Tuple[str, str]],
366
+ valid_tables: Optional[Set[Tuple[str, str]]],
360
367
  ) -> bool:
361
- return database not in [default_database, "tinybird", VALID_REMOTE] and r not in _replaced_with
368
+ # return database not in [default_database, "tinybird", VALID_REMOTE] and r not in _replaced_with
369
+ return bool(
370
+ database not in [default_database, "tinybird", VALID_REMOTE]
371
+ and valid_tables
372
+ and r not in valid_tables
373
+ and r not in _replaced_with
374
+ )
362
375
 
363
376
 
364
377
  def is_invalid_resource_from_current_workspace(
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.dev99'
8
- __revision__ = '1e30c94'
7
+ __version__ = '0.0.1.dev101'
8
+ __revision__ = '5dc0d4b'
@@ -183,6 +183,15 @@ class DatafileKind(Enum):
183
183
  return extension_map[extension]
184
184
 
185
185
 
186
+ KAFKA_PARAMS = {
187
+ "kafka_connection_name",
188
+ "kafka_topic",
189
+ "kafka_group_id",
190
+ }
191
+
192
+ REQUIRED_KAFKA_PARAMS = KAFKA_PARAMS
193
+
194
+
186
195
  class Datafile:
187
196
  def __init__(self) -> None:
188
197
  self.maintainer: Optional[str] = None
@@ -248,9 +257,10 @@ class Datafile:
248
257
  f"Invalid permission {token['permission']} for token {token['token_name']}. Only READ is allowed for pipes"
249
258
  )
250
259
  elif self.kind == DatafileKind.datasource:
251
- # TODO(eclbg):
252
260
  # [x] Just one node
253
261
  # [x] Engine is present
262
+ # [x] Token permissions are valid
263
+ # [x] If it's a kafka datasource, all required kafka params are present
254
264
  # [ ] ...
255
265
  if len(self.nodes) > 1:
256
266
  # Our users are not aware of data source data files being a single-node data file, hence this error
@@ -259,11 +269,20 @@ class Datafile:
259
269
  node = self.nodes[0]
260
270
  if "schema" not in node:
261
271
  raise DatafileValidationError("SCHEMA is mandatory")
272
+ # Validate token permissions
262
273
  for token in self.tokens:
263
274
  if token["permission"].upper() not in {"READ", "APPEND"}:
264
275
  raise DatafileValidationError(
265
276
  f"Invalid permission {token['permission']} for token {token['token_name']}. Only READ and APPEND are allowed for datasources"
266
277
  )
278
+ # Validate Kafka params
279
+ if any(param in node for param in KAFKA_PARAMS) and not all(
280
+ param in node for param in REQUIRED_KAFKA_PARAMS
281
+ ):
282
+ missing = [param for param in REQUIRED_KAFKA_PARAMS if param not in node]
283
+ raise DatafileValidationError(
284
+ f"Some Kafka params have been provided, but the following required ones are missing: {missing}"
285
+ )
267
286
  else:
268
287
  # We cannot validate a datafile whose kind is unknown
269
288
  pass
@@ -1427,6 +1446,7 @@ def parse(
1427
1446
  "include": include,
1428
1447
  "sql": sql("sql"),
1429
1448
  "version": version,
1449
+ # TODO(eclbg): We should decide on a single place to define the kafka params. Definitely not here.
1430
1450
  "kafka_connection_name": assign_var("kafka_connection_name"),
1431
1451
  "kafka_topic": assign_var("kafka_topic"),
1432
1452
  "kafka_group_id": assign_var("kafka_group_id"),
@@ -1,9 +1,13 @@
1
1
  import json
2
+ import subprocess
3
+ import time
2
4
  import uuid
3
5
  from pathlib import Path
4
6
  from typing import Optional
5
7
 
6
8
  import click
9
+ import pyperclip
10
+ import requests
7
11
  from click import Context
8
12
 
9
13
  from tinybird.client import TinyB
@@ -40,28 +44,50 @@ metadata:
40
44
  namespace: %(namespace)s
41
45
  labels:
42
46
  name: tinybird
43
- annotations:
44
- service.beta.kubernetes.io/aws-load-balancer-type: 'external'
45
- service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: 'ip'
46
- service.beta.kubernetes.io/aws-load-balancer-backend-protocol: 'tcp'
47
- service.beta.kubernetes.io/aws-load-balancer-scheme: 'internet-facing'
48
- service.beta.kubernetes.io/aws-load-balancer-ssl-negotiation-policy: 'ELBSecurityPolicy-TLS13-1-2-2021-06'
49
- service.beta.kubernetes.io/aws-load-balancer-ssl-cert: '%(cert_arn)s'
50
47
  spec:
51
- type: LoadBalancer
48
+ type: ClusterIP
52
49
  ports:
53
- - port: 443
50
+ - port: 80
54
51
  targetPort: http
55
52
  protocol: TCP
56
- name: https
53
+ name: http
57
54
  selector:
58
55
  name: tinybird
59
56
  ---
57
+ apiVersion: networking.k8s.io/v1
58
+ kind: Ingress
59
+ metadata:
60
+ namespace: %(namespace)s
61
+ name: tinybird
62
+ annotations:
63
+ alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
64
+ alb.ingress.kubernetes.io/scheme: internet-facing
65
+ alb.ingress.kubernetes.io/ssl-policy: ELBSecurityPolicy-TLS13-1-2-2021-06
66
+ alb.ingress.kubernetes.io/ssl-redirect: '443'
67
+ alb.ingress.kubernetes.io/target-type: 'ip'
68
+ alb.ingress.kubernetes.io/load-balancer-name: %(namespace)s
69
+ alb.ingress.kubernetes.io/success-codes: '200,301,302'
70
+ spec:
71
+ ingressClassName: alb
72
+ tls:
73
+ - hosts:
74
+ - %(full_dns_name)s
75
+ rules:
76
+ - http:
77
+ paths:
78
+ - path: /
79
+ pathType: Prefix
80
+ backend:
81
+ service:
82
+ name: tinybird
83
+ port:
84
+ name: http
85
+ ---
60
86
  apiVersion: apps/v1
61
87
  kind: StatefulSet
62
88
  metadata:
63
89
  name: tinybird
64
- namespace: %(namespace)s
90
+ namespace: %(namespace)s
65
91
  spec:
66
92
  serviceName: "tinybird"
67
93
  replicas: 1
@@ -85,6 +111,12 @@ spec:
85
111
  env:
86
112
  - name: TB_INFRA_TOKEN
87
113
  value: "%(infra_token)s"
114
+ - name: TB_INFRA_WORKSPACE
115
+ value: "%(infra_workspace)s"
116
+ - name: TB_INFRA_ORGANIZATION
117
+ value: "%(infra_organization)s"
118
+ - name: TB_INFRA_USER
119
+ value: "%(infra_user)s"
88
120
  volumeMounts:
89
121
  - name: clickhouse-data
90
122
  mountPath: /var/lib/clickhouse
@@ -115,12 +147,12 @@ provider "aws" {
115
147
 
116
148
  # Get the hosted zone data
117
149
  data "aws_route53_zone" "selected" {
118
- name = "%(domain)s"
150
+ name = "%(dns_zone_name)s"
119
151
  }
120
152
 
121
153
  # Create ACM certificate
122
154
  resource "aws_acm_certificate" "cert" {
123
- domain_name = "*.${data.aws_route53_zone.selected.name}"
155
+ domain_name = "%(dns_record)s.${data.aws_route53_zone.selected.name}"
124
156
  validation_method = "DNS"
125
157
  subject_alternative_names = [data.aws_route53_zone.selected.name]
126
158
 
@@ -152,21 +184,23 @@ resource "aws_acm_certificate_validation" "cert" {
152
184
  certificate_arn = aws_acm_certificate.cert.arn
153
185
  validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn]
154
186
  }
155
-
156
- output "certificate_arn" {
157
- description = "The ARN of the ACM certificate"
158
- value = aws_acm_certificate.cert.arn
159
- }
160
187
  """
161
188
 
162
189
  TERRAFORM_SECOND_TEMPLATE = """
163
190
  # Create Route 53 record for the load balancer
191
+ data "aws_alb" "tinybird" {
192
+ name = "%(namespace)s"
193
+ }
194
+
164
195
  resource "aws_route53_record" "tinybird" {
165
196
  zone_id = data.aws_route53_zone.selected.zone_id
166
197
  name = "%(full_dns_name)s"
167
- type = "CNAME"
168
- ttl = 300
169
- records = ["%(external_ip)s"]
198
+ type = "A"
199
+ alias {
200
+ name = data.aws_alb.tinybird.dns_name
201
+ zone_id = data.aws_alb.tinybird.zone_id
202
+ evaluate_target_health = true
203
+ }
170
204
  }
171
205
 
172
206
  output "tinybird_dns" {
@@ -184,46 +218,42 @@ def infra(ctx: Context) -> None:
184
218
 
185
219
  @infra.command(name="init")
186
220
  @click.option("--name", type=str, help="Name for identifying the self-managed infrastructure in Tinybird")
187
- @click.option("--provider", default="aws", type=str, help="Infrastructure provider (aws, gcp, azure)")
188
- @click.option("--region", type=str, help="AWS region (for AWS provider)")
189
- @click.option("--domain", type=str, help="Route53 domain name (for AWS provider)")
190
- @click.option("--namespace", type=str, help="Kubernetes namespace for deployment")
191
- @click.option("--dns-record", type=str, help="DNS record name to create (without domain, e.g. 'tinybird')")
192
- @click.option(
193
- "--auto-apply-terraform", is_flag=True, help="Automatically apply Terraform configuration without prompting"
194
- )
195
- @click.option("--auto-apply-dns", is_flag=True, help="Automatically apply DNS configuration without prompting")
221
+ @click.option("--provider", type=str, help="Infrastructure provider. Possible values are: aws, gcp, azure)")
222
+ @click.option("--region", type=str, help="AWS region, when using aws as the provider")
223
+ @click.option("--dns-zone-name", type=str, help="DNS zone name")
224
+ @click.option("--namespace", type=str, help="Kubernetes namespace for the deployment")
225
+ @click.option("--dns-record", type=str, help="DNS record name to create, without domain. For example, 'tinybird')")
226
+ @click.option("--storage-class", type=str, help="Storage class for the k8s StatefulSet")
196
227
  @click.option(
197
- "--auto-apply-kubectl", is_flag=True, help="Automatically apply Kubernetes configuration without prompting"
228
+ "--auto-apply", is_flag=True, help="Automatically apply Terraform and kubectl configuration without prompting"
198
229
  )
199
- @click.option("--skip-terraform", is_flag=True, help="Skip Terraform configuration and application")
200
- @click.option("--skip-kubectl", is_flag=True, help="Skip Kubernetes configuration and application")
201
- @click.option("--skip-dns", is_flag=True, help="Skip DNS configuration and application")
230
+ @click.option("--skip-apply", is_flag=True, help="Skip Terraform and kubectl configuration and application")
202
231
  @click.pass_context
203
232
  def infra_init(
204
233
  ctx: Context,
205
234
  name: str,
206
235
  provider: str,
207
236
  region: Optional[str] = None,
208
- domain: Optional[str] = None,
237
+ dns_zone_name: Optional[str] = None,
209
238
  namespace: Optional[str] = None,
210
239
  dns_record: Optional[str] = None,
211
240
  storage_class: Optional[str] = None,
212
- auto_apply_terraform: bool = False,
213
- auto_apply_dns: bool = False,
214
- auto_apply_kubectl: bool = False,
215
- skip_terraform: bool = False,
216
- skip_kubectl: bool = False,
217
- skip_dns: bool = False,
241
+ auto_apply: bool = False,
242
+ skip_apply: bool = False,
218
243
  ) -> None:
219
244
  """Init infra"""
245
+ # Check if provider is specified
246
+ if not provider:
247
+ click.echo("Error: --provider option is required. Specify a provider. Possible values are: aws, gcp, azure.")
248
+ return
249
+
220
250
  # AWS-specific Terraform template creation
221
251
  if provider.lower() != "aws":
222
252
  click.echo("Provider not supported yet.")
223
253
  return
224
254
 
225
255
  # Create infra directory if it doesn't exist
226
- infra_dir = Path("infra")
256
+ infra_dir = Path(f"infra/{provider}")
227
257
  infra_dir.mkdir(exist_ok=True)
228
258
  yaml_path = infra_dir / "k8s.yaml"
229
259
  tf_path = infra_dir / "main.tf"
@@ -237,34 +267,34 @@ def infra_init(
237
267
  config = json.load(f)
238
268
  click.echo("Loaded existing configuration from config.json")
239
269
  except json.JSONDecodeError:
240
- click.echo("Warning: Could not parse existing config.json, will create a new one")
270
+ click.echo("Warning: Could not parse existing config.json. Creating a new file...")
241
271
 
242
272
  # Generate a random ID for default values
243
273
  random_id = str(uuid.uuid4())[:8]
244
274
 
245
275
  # Get or prompt for configuration values
246
- name = name or click.prompt("Enter name", type=str)
247
- region = region or config.get("region") or click.prompt("Enter aws region", default="us-east-1", type=str)
248
- domain = domain or config.get("domain") or click.prompt("Enter route 53 domain name", type=str)
276
+ name = name or click.prompt("Enter the name for your self-managed region", type=str)
277
+ region = region or config.get("region") or click.prompt("Enter the AWS region", default="us-east-1", type=str)
278
+ dns_zone_name = dns_zone_name or config.get("dns_zone_name") or click.prompt("Enter the DNS zone name", type=str)
249
279
  namespace = (
250
280
  namespace
251
281
  or config.get("namespace")
252
- or click.prompt("Enter namespace name", default=f"tinybird-{random_id}", type=str)
282
+ or click.prompt("Enter the Kubernetes namespace", default=f"tinybird-{random_id}", type=str)
253
283
  )
254
284
  dns_record = (
255
285
  dns_record
256
286
  or config.get("dns_record")
257
- or click.prompt("Enter DNS record name (without domain)", default=f"tinybird-{random_id}", type=str)
287
+ or click.prompt("Enter the DNS record name, without domain", default=f"tinybird-{random_id}", type=str)
258
288
  )
259
289
  storage_class = config.get("storage_class") or click.prompt(
260
- "Enter storage class", default="gp3-encrypted", type=str
290
+ "Enter the Kubernetes storage class", default="gp3-encrypted", type=str
261
291
  )
262
292
 
263
293
  # Save configuration
264
294
  config = {
265
295
  "provider": provider,
266
296
  "region": region,
267
- "domain": domain,
297
+ "dns_zone_name": dns_zone_name,
268
298
  "namespace": namespace,
269
299
  "dns_record": dns_record,
270
300
  "storage_class": storage_class,
@@ -280,30 +310,48 @@ def infra_init(
280
310
  user_client = cli_config.get_client(token=cli_config.get_user_token() or "")
281
311
  user_workspaces = async_to_sync(user_client.user_workspaces_with_organization)()
282
312
  admin_org_id = user_workspaces.get("organization_id")
313
+ admin_org_name = user_workspaces.get("organization_name", "")
283
314
  infras = async_to_sync(client.infra_list)(organization_id=admin_org_id)
284
315
  infra = next((infra for infra in infras if infra["name"] == name), None)
285
316
  if not infra:
286
317
  click.echo(FeedbackManager.highlight(message=f"\n» Creating infrastructure '{name}' in Tinybird..."))
287
- host = f"https://{dns_record}.{domain}"
318
+ host = f"https://{dns_record}.{dns_zone_name}"
288
319
  infra = async_to_sync(client.infra_create)(organization_id=admin_org_id, name=name, host=host)
289
320
 
290
321
  infra_token = infra["token"]
291
322
 
292
323
  # Write the Terraform template
293
- terraform_content = TERRAFORM_FIRST_TEMPLATE % {"aws_region": region, "domain": domain}
324
+ terraform_content = TERRAFORM_FIRST_TEMPLATE % {
325
+ "aws_region": region,
326
+ "dns_zone_name": dns_zone_name,
327
+ "dns_record": dns_record,
328
+ }
294
329
 
295
330
  with open(tf_path, "w") as f:
296
331
  f.write(terraform_content.lstrip())
297
332
 
298
- click.echo(f"Creating Terraform configuration in {tf_path}")
333
+ click.echo(f"Created Terraform configuration in {tf_path}")
299
334
 
300
- # Apply Terraform configuration if user confirms
301
- if not skip_terraform:
302
- import subprocess
335
+ new_content = K8S_YML % {
336
+ "namespace": namespace,
337
+ "storage_class": storage_class,
338
+ "full_dns_name": f"{dns_record}.{dns_zone_name}",
339
+ "infra_token": infra_token,
340
+ "infra_workspace": cli_config.get("name", ""),
341
+ "infra_organization": admin_org_name,
342
+ "infra_user": cli_config.get_user_email() or "",
343
+ }
344
+
345
+ with open(yaml_path, "w") as f:
346
+ f.write(new_content.lstrip())
347
+
348
+ click.echo(f"Created Kubernetes configuration in {yaml_path}")
303
349
 
350
+ # Apply Terraform configuration if user confirms
351
+ if not skip_apply:
304
352
  # Initialize Terraform
305
353
  click.echo("Initializing Terraform...")
306
- init_result = subprocess.run(["terraform", "-chdir=infra", "init"], capture_output=True, text=True)
354
+ init_result = subprocess.run(["terraform", f"-chdir={infra_dir}", "init"], capture_output=True, text=True)
307
355
 
308
356
  if init_result.returncode != 0:
309
357
  click.echo("Terraform initialization failed:")
@@ -312,7 +360,7 @@ def infra_init(
312
360
 
313
361
  # Run terraform plan first
314
362
  click.echo("\nRunning Terraform plan...\n")
315
- plan_result = subprocess.run(["terraform", "-chdir=infra", "plan"], capture_output=True, text=True)
363
+ plan_result = subprocess.run(["terraform", f"-chdir={infra_dir}", "plan"], capture_output=True, text=True)
316
364
 
317
365
  if plan_result.returncode != 0:
318
366
  click.echo("Terraform plan failed:")
@@ -322,10 +370,10 @@ def infra_init(
322
370
  click.echo(plan_result.stdout)
323
371
 
324
372
  # Apply Terraform configuration if user confirms
325
- if auto_apply_terraform or click.confirm("Would you like to apply the Terraform configuration now?"):
373
+ if auto_apply or click.confirm("Would you like to apply the Terraform configuration now?"):
326
374
  click.echo("\nApplying Terraform configuration...\n")
327
375
  apply_result = subprocess.run(
328
- ["terraform", "-chdir=infra", "apply", "-auto-approve"], capture_output=True, text=True
376
+ ["terraform", f"-chdir={infra_dir}", "apply", "-auto-approve"], capture_output=True, text=True
329
377
  )
330
378
 
331
379
  if apply_result.returncode != 0:
@@ -335,309 +383,133 @@ def infra_init(
335
383
 
336
384
  click.echo(apply_result.stdout)
337
385
 
338
- # Get the certificate ARN output
339
- output_result = subprocess.run(
340
- ["terraform", "-chdir=infra", "output", "certificate_arn"], capture_output=True, text=True
386
+ # Prompt to apply the k8s configuration
387
+ if not skip_apply and (
388
+ auto_apply or click.confirm("Would you like to apply the Kubernetes configuration now?")
389
+ ):
390
+ # Get current kubectl context
391
+ current_context_result = subprocess.run(
392
+ ["kubectl", "config", "current-context"], capture_output=True, text=True
341
393
  )
342
394
 
343
- if output_result.returncode == 0:
344
- cert_arn = output_result.stdout.strip().replace('"', "")
345
-
346
- new_content = K8S_YML % {
347
- "namespace": namespace,
348
- "cert_arn": cert_arn,
349
- "storage_class": storage_class,
350
- "infra_token": infra_token,
351
- }
352
-
353
- with open(yaml_path, "w") as f:
354
- f.write(new_content.lstrip())
355
-
356
- click.echo(f"Created Kubernetes configuration with certificate ARN in {yaml_path}")
357
-
358
- # Prompt to apply the k8s configuration
359
- if not skip_kubectl and (
360
- auto_apply_kubectl or click.confirm("Would you like to apply the Kubernetes configuration now?")
361
- ):
362
- import subprocess
363
-
364
- # Get current kubectl context
365
- current_context_result = subprocess.run(
366
- ["kubectl", "config", "current-context"], capture_output=True, text=True
367
- )
368
-
369
- current_context = (
370
- current_context_result.stdout.strip() if current_context_result.returncode == 0 else "unknown"
371
- )
372
-
373
- # Get available contexts
374
- contexts_result = subprocess.run(
375
- ["kubectl", "config", "get-contexts", "-o", "name"], capture_output=True, text=True
376
- )
377
-
378
- if contexts_result.returncode != 0:
379
- click.echo("Failed to get kubectl contexts:")
380
- click.echo(contexts_result.stderr)
381
- return
382
-
383
- available_contexts = [
384
- context.strip() for context in contexts_result.stdout.splitlines() if context.strip()
385
- ]
386
-
387
- if not available_contexts:
388
- click.echo("No kubectl contexts found. Please configure kubectl first.")
389
- return
390
-
391
- # Prompt user to select a context
392
- if len(available_contexts) == 1:
393
- selected_context = available_contexts[0]
394
- click.echo(f"Using the only available kubectl context: {selected_context}")
395
- else:
396
- click.echo("\nAvailable kubectl contexts:")
397
- for i, context in enumerate(available_contexts):
398
- marker = " (current)" if context == current_context else ""
399
- click.echo(f" {i + 1}. {context}{marker}")
400
-
401
- click.echo("")
402
- default_index = (
403
- available_contexts.index(current_context) + 1
404
- if current_context in available_contexts
405
- else 1
406
- )
407
-
408
- selected_index = click.prompt(
409
- "Select kubectl context number to apply configuration",
410
- type=click.IntRange(1, len(available_contexts)),
411
- default=default_index,
412
- )
413
-
414
- selected_context = available_contexts[selected_index - 1]
415
- click.echo(f"Selected context: {selected_context}")
416
-
417
- # Apply the configuration to the selected context
418
- click.echo(f"Applying Kubernetes configuration to context '{selected_context}'...")
419
- apply_result = subprocess.run(
420
- ["kubectl", "--context", selected_context, "apply", "-f", str(yaml_path)],
421
- capture_output=True,
422
- text=True,
423
- )
424
-
425
- if apply_result.returncode != 0:
426
- click.echo("Failed to apply Kubernetes configuration:")
427
- click.echo(apply_result.stderr)
428
- else:
429
- click.echo("Kubernetes configuration applied successfully:")
430
- click.echo(apply_result.stdout)
431
-
432
- # Get the namespace from the applied configuration
433
- namespace = None
434
- with open(yaml_path, "r") as f:
435
- for line in f:
436
- if "namespace:" in line and not namespace:
437
- namespace = line.split("namespace:")[1].strip()
438
- break
439
-
440
- if not namespace:
441
- namespace = "tinybird" # Default namespace
442
-
443
- click.echo(f"\nWaiting for load balancer to be provisioned in namespace '{namespace}'...")
444
-
445
- # Wait for the load balancer to get an external IP
446
- max_attempts = 30
447
- attempt = 0
448
- external_ip = None
449
-
450
- while attempt < max_attempts and not external_ip:
451
- attempt += 1
452
-
453
- # Get the service details
454
- get_service_result = subprocess.run(
455
- [
456
- "kubectl",
457
- "--context",
458
- selected_context,
459
- "-n",
460
- namespace,
461
- "get",
462
- "service",
463
- "tinybird",
464
- "-o",
465
- "jsonpath='{.status.loadBalancer.ingress[0].hostname}'",
466
- ],
467
- capture_output=True,
468
- text=True,
469
- )
470
-
471
- if get_service_result.returncode == 0:
472
- potential_ip = get_service_result.stdout.strip().replace("'", "")
473
- if potential_ip and potential_ip != "":
474
- external_ip = potential_ip
475
- break
476
-
477
- click.echo(
478
- f"Attempt {attempt}/{max_attempts}: Load balancer not ready yet, waiting 10 seconds..."
479
- )
480
- import time
481
-
482
- time.sleep(10)
483
-
484
- if external_ip:
485
- click.echo("\nLoad balancer provisioned successfully.")
486
-
487
- # Update the Terraform configuration with the load balancer DNS
488
- if not skip_dns and domain and tf_path.exists():
489
- click.echo("\nUpdating Terraform configuration with load balancer DNS...")
490
-
491
- with open(tf_path, "r") as f:
492
- tf_content = f.read()
493
-
494
- # Check if the Route 53 record already exists in the file
495
- if 'resource "aws_route53_record" "tinybird"' not in tf_content:
496
- # Create the full DNS name
497
- full_dns_name = f"{dns_record}.{domain}"
498
-
499
- # Use in the Terraform template
500
- route53_record = TERRAFORM_SECOND_TEMPLATE % {
501
- "external_ip": external_ip,
502
- "full_dns_name": full_dns_name,
503
- }
395
+ current_context = (
396
+ current_context_result.stdout.strip() if current_context_result.returncode == 0 else "unknown"
397
+ )
398
+ # Get available contexts
399
+ contexts_result = subprocess.run(
400
+ ["kubectl", "config", "get-contexts", "-o", "name"], capture_output=True, text=True
401
+ )
504
402
 
505
- # Append the Route 53 record to the Terraform file
506
- with open(tf_path, "a") as f:
507
- f.write(route53_record.lstrip())
403
+ if contexts_result.returncode != 0:
404
+ click.echo("Failed to get kubectl contexts:")
405
+ click.echo(contexts_result.stderr)
406
+ return
508
407
 
509
- click.echo("Added Route 53 record to Terraform configuration")
510
- else:
511
- # Update the existing Route 53 record
512
- updated_tf = tf_content.replace(
513
- 'records = ["LOAD_BALANCER_DNS_PLACEHOLDER"]', f'records = ["{external_ip}"]'
514
- )
408
+ available_contexts = [context.strip() for context in contexts_result.stdout.splitlines() if context.strip()]
515
409
 
516
- # Also handle case where there might be another placeholder or old value
517
- import re
410
+ if not available_contexts:
411
+ click.echo("No kubectl contexts found. Configure kubectl first.")
412
+ return
518
413
 
519
- pattern = r'records\s*=\s*\[\s*"[^"]*"\s*\]'
520
- updated_tf = re.sub(pattern, f'records = ["{external_ip}"]', updated_tf)
414
+ # Prompt user to select a context
415
+ if len(available_contexts) == 1:
416
+ selected_context = available_contexts[0]
417
+ click.echo(f"Using the only available kubectl context: {selected_context}")
418
+ else:
419
+ click.echo("\nAvailable kubectl contexts:")
420
+ for i, context in enumerate(available_contexts):
421
+ marker = " (current)" if context == current_context else ""
422
+ click.echo(f" {i + 1}. {context}{marker}")
423
+
424
+ click.echo("")
425
+ default_index = (
426
+ available_contexts.index(current_context) + 1 if current_context in available_contexts else 1
427
+ )
428
+
429
+ selected_index = click.prompt(
430
+ "Select kubectl context number to apply configuration",
431
+ type=click.IntRange(1, len(available_contexts)),
432
+ default=default_index,
433
+ )
434
+
435
+ selected_context = available_contexts[selected_index - 1]
436
+ click.echo(f"Selected context: {selected_context}")
437
+
438
+ # Apply the configuration to the selected context
439
+ click.echo(f"Applying Kubernetes configuration to context '{selected_context}'...")
440
+ apply_result = subprocess.run(
441
+ ["kubectl", "--context", selected_context, "apply", "-f", str(yaml_path)],
442
+ capture_output=True,
443
+ text=True,
444
+ )
521
445
 
522
- with open(tf_path, "w") as f:
523
- f.write(updated_tf.lstrip())
446
+ if apply_result.returncode != 0:
447
+ click.echo("Failed to apply Kubernetes configuration:")
448
+ click.echo(apply_result.stderr)
449
+ else:
450
+ click.echo("Kubernetes configuration applied successfully:")
451
+ click.echo(apply_result.stdout)
452
+
453
+ # Get the namespace from the applied configuration
454
+ namespace = None
455
+ with open(yaml_path, "r") as f:
456
+ for line in f:
457
+ if "namespace:" in line and not namespace:
458
+ namespace = line.split("namespace:")[1].strip()
459
+ break
460
+
461
+ if not namespace:
462
+ namespace = "tinybird" # Default namespace
463
+
464
+ click.echo("\nWaiting for load balancer and DNS to be provisioned...")
465
+
466
+ max_attempts = 30 # 30 attempts * 10 seconds = 5 minutes
467
+ endpoint_url = f"https://{dns_record}.{dns_zone_name}"
468
+
469
+ with click.progressbar(
470
+ range(max_attempts),
471
+ label=f"Checking endpoint availability: {endpoint_url}",
472
+ length=max_attempts,
473
+ show_eta=True,
474
+ show_percent=True,
475
+ fill_char="█",
476
+ empty_char="░",
477
+ ) as bar:
478
+ for attempt in bar:
479
+ try:
480
+ response = requests.get(endpoint_url, allow_redirects=False, timeout=5)
481
+ if response.status_code < 400: # Consider any non-error response as success
482
+ click.echo(click.style("\n✅ HTTPS endpoint is now accessible!", fg="green", bold=True))
483
+ break
484
+ except requests.RequestException:
485
+ pass
486
+
487
+ if attempt == max_attempts - 1:
488
+ click.echo(
489
+ click.style(
490
+ "\n⚠️ HTTPS endpoint not accessible after 5 minutes", fg="yellow", bold=True
491
+ )
492
+ )
493
+ click.echo(
494
+ " This might be due to DNS propagation or the Load Balancer provisioning delays"
495
+ )
496
+ click.echo(f" Please try accessing {endpoint_url} manually in a few minutes")
497
+ else:
498
+ time.sleep(10)
524
499
 
525
- click.echo("Updated existing Route 53 record in Terraform configuration")
500
+ if not skip_apply:
501
+ # Print a summary with the endpoint URL
502
+ click.echo("\n" + "=" * 60)
503
+ click.echo("DEPLOYMENT SUMMARY".center(60))
504
+ click.echo("=" * 60)
505
+ click.echo("✅ Load balancer provisioned")
526
506
 
527
- # Run terraform plan for DNS changes
528
- click.echo("\nRunning Terraform plan for DNS changes...\n")
529
- plan_result = subprocess.run(
530
- ["terraform", "-chdir=infra", "plan"], capture_output=True, text=True
531
- )
507
+ click.echo(f"\n🔗 Tinybird is available at: https://{dns_record}.{dns_zone_name}")
532
508
 
533
- if plan_result.returncode != 0:
534
- click.echo("Terraform plan for DNS changes failed:")
535
- click.echo(plan_result.stderr)
536
- else:
537
- click.echo(plan_result.stdout)
538
-
539
- # Apply the updated Terraform configuration
540
- if not skip_dns and (
541
- auto_apply_dns
542
- or click.confirm("Would you like to create the DNS record in Route 53 now?")
543
- ):
544
- click.echo("Applying updated Terraform configuration...")
545
- apply_result = subprocess.run(
546
- ["terraform", "-chdir=infra", "apply", "-auto-approve"],
547
- capture_output=True,
548
- text=True,
549
- )
550
-
551
- if apply_result.returncode != 0:
552
- click.echo("Failed to create DNS record:")
553
- click.echo(apply_result.stderr)
554
- else:
555
- click.echo(apply_result.stdout)
556
- click.echo("DNS record created successfully!")
557
-
558
- # Get the DNS name from Terraform output
559
- dns_output = subprocess.run(
560
- ["terraform", "-chdir=infra", "output", "tinybird_dns"],
561
- capture_output=True,
562
- text=True,
563
- )
564
-
565
- if dns_output.returncode == 0:
566
- dns_name = dns_output.stdout.strip().replace('"', "")
567
- click.echo("\nDNS record created successfully!")
568
- click.echo(
569
- "\nWaiting up to 5 minutes for HTTPS endpoint to become available..."
570
- )
571
-
572
- import time
573
-
574
- import requests
575
-
576
- max_attempts = 30 # 30 attempts * 10 seconds = 5 minutes
577
- attempt = 0
578
- while attempt < max_attempts:
579
- attempt += 1
580
- try:
581
- response = requests.get(
582
- f"https://{dns_name}", allow_redirects=False, timeout=5
583
- )
584
- response.raise_for_status()
585
- click.echo("\n✅ HTTPS endpoint is now accessible!")
586
- break
587
- except requests.RequestException:
588
- if attempt == max_attempts:
589
- click.echo(
590
- "\n⚠️ HTTPS endpoint not accessible after 5 minutes"
591
- )
592
- click.echo(
593
- " This might be due to DNS propagation or the Load Balancer provisioning delays"
594
- )
595
- click.echo(
596
- " Please try accessing the URL manually in a few minutes"
597
- )
598
- else:
599
- click.echo(
600
- f"Attempt {attempt}/{max_attempts}: Not ready yet, waiting 10 seconds..."
601
- )
602
- time.sleep(10)
603
- else:
604
- click.echo(
605
- f"\nYour Tinybird instance should be available at: https://tinybird.{domain}"
606
- )
607
-
608
- # Print a summary with the endpoint URL
609
- click.echo("\n" + "=" * 60)
610
- click.echo("DEPLOYMENT SUMMARY".center(60))
611
- click.echo("=" * 60)
612
-
613
- if not skip_kubectl and external_ip:
614
- click.echo(f"✅ Load balancer provisioned: {external_ip}")
615
-
616
- if not skip_dns and not skip_terraform and domain:
617
- # Try to get the DNS name from Terraform output
618
- dns_output = subprocess.run(
619
- ["terraform", "-chdir=infra", "output", "tinybird_dns"], capture_output=True, text=True
509
+ click.echo(
510
+ "\n📌 Note: It may take a few minutes for DNS to propagate and the HTTPS certificate to be fully provisioned."
620
511
  )
621
512
 
622
- if dns_output.returncode == 0:
623
- dns_name = dns_output.stdout.strip().replace('"', "")
624
- click.echo(f"✅ DNS record created: {dns_name}")
625
- click.echo(f"\n🔗 Tinybird is available at: https://{dns_name}")
626
- else:
627
- # Fallback to constructed DNS name
628
- full_dns_name = f"{dns_record}.{domain}"
629
- click.echo(f"✅ DNS record created: {full_dns_name}")
630
- click.echo(f"\n🔗 Tinybird is available at: https://{full_dns_name}")
631
- elif not skip_kubectl and external_ip:
632
- click.echo(f"\n🔗 Tinybird is available at: https://{external_ip}")
633
- if domain:
634
- click.echo(f"\n📝 Consider creating a DNS record: {dns_record}.{domain} → {external_ip}")
635
-
636
- click.echo(
637
- "\n📌 Note: It may take a few minutes for DNS to propagate and the HTTPS certificate to be fully provisioned."
638
- )
639
- click.echo("=" * 60)
640
-
641
513
 
642
514
  @infra.command(name="rm")
643
515
  @click.argument("name")
@@ -691,3 +563,62 @@ async def infra_ls(ctx: click.Context):
691
563
  click.echo(FeedbackManager.info(message="\n** Infras:"))
692
564
  echo_safe_humanfriendly_tables_format_smart_table(table_human_readable, column_names=columns)
693
565
  click.echo("\n")
566
+
567
+
568
+ @infra.command(name="create")
569
+ @click.option("--name", type=str, help="Name for identifying the self-managed infrastructure in Tinybird")
570
+ @click.option("--host", type=str, help="Host for the infrastructure")
571
+ @click.pass_context
572
+ @coro
573
+ async def infra_create(ctx: click.Context, name: str, host: str):
574
+ """Create a self-managed infrastructure"""
575
+ try:
576
+ client: TinyB = ctx.ensure_object(dict)["client"]
577
+ user_workspaces = await client.user_workspaces_with_organization()
578
+ admin_org_id = user_workspaces.get("organization_id")
579
+ if not admin_org_id:
580
+ raise CLIException("No organization associated to this workspace")
581
+ name = name or click.prompt("Enter name", type=str)
582
+ host = host or click.prompt("Enter host", type=str)
583
+ click.echo(FeedbackManager.highlight(message=f"\n» Creating infrastructure '{name}' in Tinybird..."))
584
+ infra = await client.infra_create(organization_id=admin_org_id, name=name, host=host)
585
+ click.echo(FeedbackManager.success(message=f"\n✓ Infrastructure '{name}' created"))
586
+ pyperclip.copy(infra["token"])
587
+ click.echo(FeedbackManager.info(message="Access token has been copied to your clipboard."))
588
+ click.echo(
589
+ FeedbackManager.info(message="Pass it as an environment variable in your deployment as TB_INFRA_TOKEN")
590
+ )
591
+ except Exception as e:
592
+ click.echo(FeedbackManager.error(message=f"✗ Error: {str(e)}"))
593
+
594
+
595
+ @infra.command(name="update")
596
+ @click.argument("infra_name")
597
+ @click.option("--name", type=str, help="Name for identifying the self-managed infrastructure in Tinybird")
598
+ @click.option("--host", type=str, help="Host for the infrastructure")
599
+ @click.pass_context
600
+ @coro
601
+ async def infra_update(ctx: click.Context, infra_name: str, name: str, host: str):
602
+ """Update a self-managed infrastructure"""
603
+ try:
604
+ client: TinyB = ctx.ensure_object(dict)["client"]
605
+ user_workspaces = await client.user_workspaces_with_organization()
606
+ admin_org_id = user_workspaces.get("organization_id")
607
+ if not admin_org_id:
608
+ raise CLIException("No organization associated to this workspace")
609
+
610
+ if not name and not host:
611
+ click.echo(
612
+ FeedbackManager.warning(message="No name or host provided. Please provide either a name or a host.")
613
+ )
614
+ return
615
+
616
+ if name or host:
617
+ infras = await client.infra_list(organization_id=admin_org_id)
618
+ infra_id = next((infra["id"] for infra in infras if infra["name"] == infra_name), None)
619
+ if not infra_id:
620
+ raise CLIException(f"Infrastructure '{infra_name}' not found")
621
+ click.echo(FeedbackManager.highlight(message=f"\n» Updating infrastructure '{infra_name}' in Tinybird..."))
622
+ await client.infra_update(infra_id=infra_id, organization_id=admin_org_id, name=name, host=host)
623
+ except Exception as e:
624
+ click.echo(FeedbackManager.error(message=f"✗ Error: {str(e)}"))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: tinybird
3
- Version: 0.0.1.dev99
3
+ Version: 0.0.1.dev101
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -1,5 +1,5 @@
1
1
  tinybird/__cli__.py,sha256=esPl5QDTzuQgHe5FuxWLm-fURFigGGwjnYLh9GuWUw4,232
2
- tinybird/client.py,sha256=aDDifEWkVxJcm4WYaw6uUgXhav-e7JUW1N-mgtEjV4A,55087
2
+ tinybird/client.py,sha256=yc2BP2HiQhaUjU-t7nBuZEiJsb2nQMH3vBfOhKCciyU,55468
3
3
  tinybird/config.py,sha256=5UP_UZ2Qtlm5aOH5W7SbtN8r7X-8u3-r853joKqU5zs,6072
4
4
  tinybird/connectors.py,sha256=7Gjms7b5MAaBFGi3xytsJurCylprONpFcYrzp4Fw2Rc,15241
5
5
  tinybird/context.py,sha256=FfqYfrGX_I7PKGTQo93utaKPDNVYWelg4Hsp3evX5wM,1291
@@ -7,15 +7,15 @@ 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
9
  tinybird/prompts.py,sha256=syxUvbdkzxIrIK8pkS-Y7nzBx7DMS2Z9HaiXx7AjvnQ,33912
10
- tinybird/sql.py,sha256=LBi74GxhNAYTb6m2-KNGpAkguSKh7rcvBbERbE7nalA,46195
10
+ tinybird/sql.py,sha256=J35bhdpuu84HW2tiLp-cs_nzkRwPhiy1yPcFhcWMCR4,46248
11
11
  tinybird/sql_template.py,sha256=VH8io4n5eP2z6TEw111d8hcA9FKQerFjprKKCW2MODw,99127
12
12
  tinybird/sql_template_fmt.py,sha256=KUHdj5rYCYm_rKKdXYSJAE9vIyXUQLB0YSZnUXHeBlY,10196
13
- tinybird/sql_toolset.py,sha256=32SNvxRFKQYWTvYPMJ_u3ukcd1hKZyEqx8T2cv2412w,14697
13
+ tinybird/sql_toolset.py,sha256=KORVbNAUTfW1qo3U9oe7Z59xQ0QMsFhB0ji3HzY2JVo,15324
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=x_8CEyPeFA4ID-fNJaTMBbVu0OhEt_vS8s7qeQ6Q7rA,251
18
+ tinybird/tb/__cli__.py,sha256=9fN3V2hgGADGGvafwXcIrRciQG80C2cmQs_OlsP8u-A,252
19
19
  tinybird/tb/cli.py,sha256=H_HaZhkimKgkryYXpBjHfY9Qtg-ZORiONU3psDNpzDk,1135
20
20
  tinybird/tb/modules/auth.py,sha256=L1IatO2arRSzys3t8px8xVt8uPWUL5EVD0sFzAV_uVU,9022
21
21
  tinybird/tb/modules/build.py,sha256=h5drdmDFX8NHts9dA2Zepao7KSgMAl3DZGyFufVZP78,11085
@@ -32,7 +32,7 @@ tinybird/tb/modules/endpoint.py,sha256=EhVoGAXsFz-83Fiwj1gI-I73iRRvL49d0W81un7hv
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=dJcPYN_hpcLpygl4IcAANeP7fUgziwMGqYadWd9IT5g,29105
35
+ tinybird/tb/modules/infra.py,sha256=vb6WmVHFuU1ZhtQMh2wOzRd2x8g7CLt_2gR3EL_L2E4,22867
36
36
  tinybird/tb/modules/job.py,sha256=956Pj8BEEsiD2GZsV9RKKVM3I_CveOLgS82lykO5ukk,2963
37
37
  tinybird/tb/modules/llm.py,sha256=AC0VSphTOM2t-v1_3NLvNN_FIbgMo4dTyMqIv5nniPo,835
38
38
  tinybird/tb/modules/llm_utils.py,sha256=nS9r4FAElJw8yXtmdYrx-rtI2zXR8qXfi1QqUDCfxvg,3469
@@ -61,7 +61,7 @@ tinybird/tb/modules/datafile/build.py,sha256=jhfIJ2xt0N13XsLPe3iMQIyCPApHS13_Df2
61
61
  tinybird/tb/modules/datafile/build_common.py,sha256=rT7VJ5mnQ68R_8US91DAtkusfvjWuG_NObOzNgtN_ko,4562
62
62
  tinybird/tb/modules/datafile/build_datasource.py,sha256=CCU3eQ8Rax9RgHHfbAXDRL6rQ49N35h_GDQnGrUUUzA,17379
63
63
  tinybird/tb/modules/datafile/build_pipe.py,sha256=w-Wd08gZYAEcak9FdBijVfIU2_Wn_PPdgAZddPpoGTo,11382
64
- tinybird/tb/modules/datafile/common.py,sha256=i9Gvhz3JiR58MRBcYZDwqTqamQOj-46TnHU8Hm8bqmg,81399
64
+ tinybird/tb/modules/datafile/common.py,sha256=ZRRx-9EXeHMOz6Lu9Y1TSWELF0ftfUHLLQAANjJZ334,82241
65
65
  tinybird/tb/modules/datafile/diff.py,sha256=-0J7PsBO64T7LOZSkZ4ZFHHCPvT7cKItnJkbz2PkndU,6754
66
66
  tinybird/tb/modules/datafile/exceptions.py,sha256=8rw2umdZjtby85QbuRKFO5ETz_eRHwUY5l7eHsy1wnI,556
67
67
  tinybird/tb/modules/datafile/fixture.py,sha256=si-9LB-LdKQSWDtVW82xDrHtFfko5bgBG1cvjqqrcPU,1064
@@ -81,8 +81,8 @@ tinybird/tb_cli_modules/config.py,sha256=IsgdtFRnUrkY8-Zo32lmk6O7u3bHie1QCxLwgp4
81
81
  tinybird/tb_cli_modules/exceptions.py,sha256=pmucP4kTF4irIt7dXiG-FcnI-o3mvDusPmch1L8RCWk,3367
82
82
  tinybird/tb_cli_modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
83
83
  tinybird/tb_cli_modules/telemetry.py,sha256=Hh2Io8ZPROSunbOLuMvuIFU4TqwWPmQTqal4WS09K1A,10449
84
- tinybird-0.0.1.dev99.dist-info/METADATA,sha256=19bjkN1GWZCSNjE45Gk7lm_slBrHlbHxihZZT0PVeYQ,2585
85
- tinybird-0.0.1.dev99.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
86
- tinybird-0.0.1.dev99.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
87
- tinybird-0.0.1.dev99.dist-info/top_level.txt,sha256=VqqqEmkAy7UNaD8-V51FCoMMWXjLUlR0IstvK7tJYVY,54
88
- tinybird-0.0.1.dev99.dist-info/RECORD,,
84
+ tinybird-0.0.1.dev101.dist-info/METADATA,sha256=73sj_cIfixFVZVrtmkn_MwEYvtkMLO4GYeJCjJQ1KA0,2586
85
+ tinybird-0.0.1.dev101.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
86
+ tinybird-0.0.1.dev101.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
87
+ tinybird-0.0.1.dev101.dist-info/top_level.txt,sha256=VqqqEmkAy7UNaD8-V51FCoMMWXjLUlR0IstvK7tJYVY,54
88
+ tinybird-0.0.1.dev101.dist-info/RECORD,,