tinybird 0.0.1.dev188__py3-none-any.whl → 0.0.1.dev190__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/tb/__cli__.py CHANGED
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
4
4
  __url__ = 'https://www.tinybird.co/docs/forward/commands'
5
5
  __author__ = 'Tinybird'
6
6
  __author_email__ = 'support@tinybird.co'
7
- __version__ = '0.0.1.dev188'
8
- __revision__ = 'f13c1f2'
7
+ __version__ = '0.0.1.dev190'
8
+ __revision__ = '8a00bcf'
tinybird/tb/client.py CHANGED
@@ -1024,11 +1024,7 @@ class TinyB:
1024
1024
  )
1025
1025
 
1026
1026
  async def kafka_list_topics(self, connection_id: str, timeout=5):
1027
- resp = await self._req(
1028
- f"/v0/connectors/{connection_id}/preview?preview_activity=false",
1029
- connect_timeout=timeout,
1030
- request_timeout=timeout,
1031
- )
1027
+ resp = await self._req(f"/v0/connectors/{connection_id}/preview?preview_activity=false", timeout=timeout)
1032
1028
  return [x["topic"] for x in resp["preview"]]
1033
1029
 
1034
1030
  async def get_gcp_service_account_details(self) -> Dict[str, Any]:
@@ -38,6 +38,15 @@ def build(ctx: click.Context, watch: bool) -> None:
38
38
  """
39
39
  project: Project = ctx.ensure_object(dict)["project"]
40
40
  tb_client: TinyB = ctx.ensure_object(dict)["client"]
41
+
42
+ if project.has_deeper_level():
43
+ click.echo(
44
+ FeedbackManager.warning(
45
+ message="Default max_depth is 2, but project has deeper level. "
46
+ "You can change max_depth by running `tb --max-depth <depth> <cmd>`"
47
+ )
48
+ )
49
+
41
50
  click.echo(FeedbackManager.highlight_building_project())
42
51
  process(project=project, tb_client=tb_client, watch=False)
43
52
  if watch:
@@ -424,6 +424,14 @@ async def _analyze(filename: str, client: TinyB, format: str, connector: Optiona
424
424
  return meta, data
425
425
 
426
426
 
427
+ async def analyze_file(filename: str, client: TinyB, format: str):
428
+ meta, data = await _analyze(filename, client, format)
429
+ schema = meta["analysis"]["schema"]
430
+ schema = schema.replace(", ", ",\n ")
431
+ content = f"""DESCRIPTION >\n Generated from {filename}\n\nSCHEMA >\n {schema}"""
432
+ return content
433
+
434
+
427
435
  async def _generate_datafile(
428
436
  filename: str,
429
437
  client: TinyB,
@@ -971,7 +979,7 @@ async def push_data(
971
979
  cb.prev_done = 0 # type: ignore[attr-defined]
972
980
 
973
981
  if not silent:
974
- click.echo(FeedbackManager.gray(message=f"\nImporting data to {datasource_name}..."))
982
+ click.echo(FeedbackManager.highlight(message=f"\ Appending data to {datasource_name}..."))
975
983
 
976
984
  if isinstance(url, list):
977
985
  urls = url
@@ -1882,15 +1890,19 @@ def get_gcs_connection_name(project_folder) -> str:
1882
1890
  return get_connection_name(project_folder=project_folder, connection_type="GCS")
1883
1891
 
1884
1892
 
1885
- def get_connection_name(project_folder: str, connection_type: str) -> str:
1886
- connection_name = None
1893
+ def get_kafka_connection_name(project_folder: str, connection_name: Optional[str] = None) -> str:
1894
+ return get_connection_name(project_folder=project_folder, connection_type="KAFKA", connection_name=connection_name)
1895
+
1896
+
1897
+ def get_connection_name(project_folder: str, connection_type: str, connection_name: Optional[str] = None) -> str:
1887
1898
  valid_pattern = r"^[a-zA-Z][a-zA-Z0-9_]*$"
1888
1899
 
1889
1900
  while not connection_name:
1901
+ short_id = str(uuid.uuid4())[:4]
1890
1902
  connection_name = click.prompt(
1891
- f"🔗 Enter a name for your new Tinybird {connection_type} connection (use alphanumeric characters, and underscores)",
1892
- prompt_suffix="\n> ",
1903
+ "Enter a name (only alphanumeric characters and underscores)",
1893
1904
  show_default=True,
1905
+ default=f"{connection_type.lower()}_{short_id}",
1894
1906
  )
1895
1907
  assert isinstance(connection_name, str)
1896
1908
 
@@ -18,6 +18,7 @@ from tinybird.tb.modules.common import (
18
18
  echo_safe_humanfriendly_tables_format_smart_table,
19
19
  get_gcs_connection_name,
20
20
  get_gcs_svc_account_creds,
21
+ get_kafka_connection_name,
21
22
  get_s3_connection_name,
22
23
  production_aws_iamrole_only,
23
24
  run_aws_iamrole_connection_flow,
@@ -26,6 +27,7 @@ from tinybird.tb.modules.common import (
26
27
  from tinybird.tb.modules.create import (
27
28
  generate_aws_iamrole_connection_file_with_secret,
28
29
  generate_gcs_connection_file_with_secrets,
30
+ generate_kafka_connection_with_secrets,
29
31
  )
30
32
  from tinybird.tb.modules.feedback_manager import FeedbackManager
31
33
  from tinybird.tb.modules.project import Project
@@ -275,3 +277,21 @@ async def connection_create_gcs(ctx: Context) -> None:
275
277
  connection_path=connection_path,
276
278
  )
277
279
  )
280
+
281
+
282
+ @connection_create.command(name="kafka", short_help="Creates a Kafka connection.")
283
+ @click.option("--name", help="The name of the connection")
284
+ @click.pass_context
285
+ @coro
286
+ async def connection_create_kafka(ctx: Context, name: Optional[str] = None) -> None:
287
+ """
288
+ Creates a Kafka connection.
289
+
290
+ \b
291
+ $ tb connection create kafka
292
+ """
293
+ click.echo(FeedbackManager.highlight(message="» Creating Kafka connection..."))
294
+ project: Project = ctx.ensure_object(dict)["project"]
295
+ name = get_kafka_connection_name(project.folder, name)
296
+ await generate_kafka_connection_with_secrets(name=name, folder=project.folder)
297
+ click.echo(FeedbackManager.success(message="✓ Done!"))
@@ -409,10 +409,14 @@ def generate_connection_file(name: str, content: str, folder: str, skip_feedback
409
409
 
410
410
 
411
411
  async def generate_aws_iamrole_connection_file_with_secret(
412
- name: str, service: str, role_arn_secret_name: str, region: str, folder: str
412
+ name: str, service: str, role_arn_secret_name: str, region: str, folder: str, with_default_secret: bool = False
413
413
  ) -> Path:
414
+ if with_default_secret:
415
+ default_secret = ', "arn:aws:iam::123456789012:role/my-role"'
416
+ else:
417
+ default_secret = ""
414
418
  content = f"""TYPE {service}
415
- S3_ARN {{{{ tb_secret("{role_arn_secret_name}") }}}}
419
+ S3_ARN {{{{ tb_secret("{role_arn_secret_name}"{default_secret}) }}}}
416
420
  S3_REGION {region}
417
421
  """
418
422
  file_path = generate_connection_file(name, content, folder, skip_feedback=True)
@@ -483,6 +487,7 @@ async def create_resources_from_data(
483
487
  data: str,
484
488
  project: Project,
485
489
  config: Dict[str, Any],
490
+ skip_pipes: bool = False,
486
491
  ) -> List[Path]:
487
492
  local_client = await get_tinybird_local_client(config)
488
493
  folder_path = project.path
@@ -495,7 +500,7 @@ async def create_resources_from_data(
495
500
  result.append(ds_file)
496
501
  name = ds_file.stem
497
502
  no_pipes = len(project.get_pipe_files()) == 0
498
- if no_pipes:
503
+ if not skip_pipes and no_pipes:
499
504
  pipe_file = generate_pipe_file(
500
505
  f"{name}_endpoint",
501
506
  f"""
@@ -510,7 +515,9 @@ TYPE ENDPOINT
510
515
  return result
511
516
 
512
517
 
513
- async def create_resources_from_url(url: str, project: Project, config: Dict[str, Any]) -> List[Path]:
518
+ async def create_resources_from_url(
519
+ url: str, project: Project, config: Dict[str, Any], skip_pipes: bool = False
520
+ ) -> List[Path]:
514
521
  result: List[Path] = []
515
522
  local_client = await get_tinybird_local_client(config)
516
523
  format = url.split(".")[-1]
@@ -518,7 +525,7 @@ async def create_resources_from_url(url: str, project: Project, config: Dict[str
518
525
  result.append(ds_file)
519
526
  name = ds_file.stem
520
527
  no_pipes = len(project.get_pipe_files()) == 0
521
- if no_pipes:
528
+ if not skip_pipes and no_pipes:
522
529
  pipe_file = generate_pipe_file(
523
530
  f"{name}_endpoint",
524
531
  f"""
@@ -531,3 +538,14 @@ TYPE ENDPOINT
531
538
  )
532
539
  result.append(pipe_file)
533
540
  return result
541
+
542
+
543
+ async def generate_kafka_connection_with_secrets(name: str, folder: str) -> Path:
544
+ content = """TYPE kafka
545
+ KAFKA_BOOTSTRAP_SERVERS {{ tb_secret("KAFKA_SERVERS", "localhost:9092") }}
546
+ KAFKA_SECURITY_PROTOCOL SASL_SSL
547
+ KAFKA_SASL_MECHANISM PLAIN
548
+ KAFKA_KEY {{ tb_secret("KAFKA_USERNAME", "") }}
549
+ KAFKA_SECRET {{ tb_secret("KAFKA_PASSWORD", "") }}
550
+ """
551
+ return generate_connection_file(name, content, folder, skip_feedback=True)
@@ -7,22 +7,37 @@ import asyncio
7
7
  import json
8
8
  import os
9
9
  import re
10
+ import uuid
11
+ from datetime import datetime
12
+ from pathlib import Path
10
13
  from typing import Optional
14
+ from urllib.parse import urlparse
11
15
 
12
16
  import click
13
17
  import humanfriendly
18
+ import requests
14
19
  from click import Context
15
20
 
21
+ from tinybird.syncasync import sync_to_async
16
22
  from tinybird.tb.client import AuthNoTokenException, DoesNotExistException, TinyB
17
23
  from tinybird.tb.modules.cli import cli
18
24
  from tinybird.tb.modules.common import (
19
25
  _analyze,
26
+ analyze_file,
20
27
  coro,
21
28
  echo_safe_humanfriendly_tables_format_smart_table,
22
29
  get_format_from_filename_or_url,
23
30
  load_connector_config,
31
+ normalize_datasource_name,
24
32
  push_data,
25
33
  )
34
+ from tinybird.tb.modules.config import CLIConfig
35
+ from tinybird.tb.modules.create import (
36
+ create_resources_from_prompt,
37
+ generate_aws_iamrole_connection_file_with_secret,
38
+ generate_gcs_connection_file_with_secrets,
39
+ generate_kafka_connection_with_secrets,
40
+ )
26
41
  from tinybird.tb.modules.datafile.common import get_name_version
27
42
  from tinybird.tb.modules.datafile.fixture import persist_fixture
28
43
  from tinybird.tb.modules.exceptions import CLIDatasourceException
@@ -107,33 +122,169 @@ async def datasource_ls(ctx: Context, match: Optional[str], format_: str):
107
122
 
108
123
 
109
124
  @datasource.command(name="append")
110
- @click.argument("datasource_name", required=True)
111
- @click.argument("url", nargs=-1, required=True)
125
+ @click.argument("datasource_name", required=False)
126
+ @click.argument("data", nargs=-1, required=False)
127
+ @click.option("--url", type=str, help="URL to append data from")
128
+ @click.option("--file", type=str, help="Local file to append data from")
129
+ @click.option("--events", type=str, help="Events to append data from")
112
130
  @click.option("--concurrency", help="How many files to submit concurrently", default=1, hidden=True)
113
131
  @click.pass_context
114
132
  @coro
115
133
  async def datasource_append(
116
134
  ctx: Context,
117
135
  datasource_name: str,
118
- url,
136
+ data: Optional[str],
137
+ url: str,
138
+ file: str,
139
+ events: str,
119
140
  concurrency: int,
120
141
  ):
121
142
  """
122
143
  Appends data to an existing data source from URL, local file or a connector
123
144
 
124
- - Load from URL `tb datasource append [datasource_name] https://url_to_csv`
145
+ - Events API: `tb datasource append [datasource_name] --events '{"a":"b, "c":"d"}'`\n
146
+ - Local File: `tb datasource append [datasource_name] --file /path/to/local/file`\n
147
+ - Remote URL: `tb datasource append [datasource_name] --url https://url_to_csv`\n
148
+ - Kafka, S3 and GCS: https://www.tinybird.co/docs/forward/get-data-in/connectors\n
125
149
 
126
- - Load from local file `tb datasource append [datasource_name] /path/to/local/file`
150
+ More info: https://www.tinybird.co/docs/forward/get-data-in
127
151
  """
128
-
152
+ env: str = ctx.ensure_object(dict)["env"]
129
153
  client: TinyB = ctx.obj["client"]
130
- await push_data(
131
- client,
132
- datasource_name,
133
- url,
134
- mode="append",
135
- concurrency=concurrency,
136
- )
154
+
155
+ # If data is passed as argument, we detect if it's a JSON object, a URL or a file
156
+ if data:
157
+ try:
158
+ json.loads(data)
159
+ events = data
160
+ except Exception:
161
+ pass
162
+ if urlparse(data).scheme in ("http", "https"):
163
+ url = data
164
+ if not events and not url:
165
+ file = data
166
+
167
+ # If data is not passed as argument, we use the data from the options
168
+ if not data:
169
+ data = file or url or events
170
+
171
+ if env == "local":
172
+ tip = "Did you build your project? Run `tb build` first."
173
+ else:
174
+ tip = "Did you deploy your project? Run `tb --cloud deploy` first."
175
+
176
+ datasources = await client.datasources()
177
+ if not datasources:
178
+ raise CLIDatasourceException(FeedbackManager.error(message=f"No data sources found. {tip}"))
179
+
180
+ if datasource_name and datasource_name not in [ds["name"] for ds in datasources]:
181
+ raise CLIDatasourceException(FeedbackManager.error(message=f"Datasource {datasource_name} not found. {tip}"))
182
+
183
+ if not datasource_name:
184
+ datasource_index = -1
185
+
186
+ click.echo(FeedbackManager.info(message="\n? Which data source do you want to ingest data into?"))
187
+ while datasource_index == -1:
188
+ for index, datasource in enumerate(datasources):
189
+ click.echo(f" [{index + 1}] {datasource['name']}")
190
+ click.echo(
191
+ FeedbackManager.gray(message="Tip: Run tb datasource append [datasource_name] to skip this step.")
192
+ )
193
+
194
+ datasource_index = click.prompt("\nSelect option", default=1)
195
+
196
+ if datasource_index == 0:
197
+ click.echo(FeedbackManager.warning(message="Datasource type selection cancelled by user"))
198
+ return None
199
+
200
+ try:
201
+ datasource_name = datasources[int(datasource_index) - 1]["name"]
202
+ except Exception:
203
+ datasource_index = -1
204
+
205
+ if not datasource_name:
206
+ raise CLIDatasourceException(FeedbackManager.error_datasource_name())
207
+
208
+ if not data:
209
+ data_index = -1
210
+ options = (
211
+ "Events API",
212
+ "Local File",
213
+ "Remote URL",
214
+ )
215
+ click.echo(FeedbackManager.info(message="\n? How do you want to ingest data?"))
216
+ while data_index == -1:
217
+ for index, option in enumerate(options):
218
+ click.echo(f" [{index + 1}] {option}")
219
+ click.echo(
220
+ FeedbackManager.gray(
221
+ message="Tip: Run tb datasource append [datasource_name] --events | --file | --url to skip this step"
222
+ )
223
+ )
224
+
225
+ data_index = click.prompt("\nSelect option", default=1)
226
+
227
+ if data_index == 0:
228
+ click.echo(FeedbackManager.warning(message="Data selection cancelled by user"))
229
+ return None
230
+
231
+ try:
232
+ data_index = int(data_index)
233
+ except Exception:
234
+ data_index = -1
235
+
236
+ if data_index == 1:
237
+ events = click.prompt("Events data")
238
+ elif data_index == 2:
239
+ data = click.prompt("Path to local file")
240
+ elif data_index == 3:
241
+ data = click.prompt("URL to remote file")
242
+ else:
243
+ raise CLIDatasourceException(FeedbackManager.error(message="Invalid ingestion option"))
244
+
245
+ if events:
246
+ click.echo(FeedbackManager.highlight(message=f"\n» Sending events to {datasource_name}"))
247
+ try:
248
+ json_data = json.loads(events)
249
+ except Exception:
250
+ raise CLIDatasourceException(FeedbackManager.error(message="Invalid events data"))
251
+
252
+ response = await sync_to_async(requests.post)(
253
+ f"{client.host}/v0/events?name={datasource_name}",
254
+ headers={"Authorization": f"Bearer {client.token}"},
255
+ json=json_data,
256
+ )
257
+
258
+ try:
259
+ res = response.json()
260
+ except Exception:
261
+ raise CLIDatasourceException(FeedbackManager.error(message=response.text))
262
+
263
+ successful_rows = res["successful_rows"]
264
+ quarantined_rows = res["quarantined_rows"]
265
+ if successful_rows > 0:
266
+ click.echo(
267
+ FeedbackManager.success(
268
+ message=f"✓ {successful_rows} row{'' if successful_rows == 1 else 's'} appended!"
269
+ )
270
+ )
271
+ if quarantined_rows > 0:
272
+ raise CLIDatasourceException(
273
+ FeedbackManager.error(
274
+ message=f"{quarantined_rows} row{'' if quarantined_rows == 1 else 's'} went to quarantine"
275
+ )
276
+ )
277
+ else:
278
+ click.echo(FeedbackManager.highlight(message=f"\n» Appending data to {datasource_name}"))
279
+ await push_data(
280
+ client,
281
+ datasource_name,
282
+ data,
283
+ mode="append",
284
+ concurrency=concurrency,
285
+ silent=True,
286
+ )
287
+ click.echo(FeedbackManager.success(message="✓ Rows appended!"))
137
288
 
138
289
 
139
290
  @datasource.command(name="replace")
@@ -473,3 +624,281 @@ async def datasource_sync(ctx: Context, datasource_name: str, yes: bool):
473
624
  raise
474
625
  except Exception as e:
475
626
  raise CLIDatasourceException(FeedbackManager.error_syncing_datasource(datasource=datasource_name, error=str(e)))
627
+
628
+
629
+ @datasource.command(name="create")
630
+ @click.option("--name", type=str, help="Name of the data source")
631
+ @click.option("--blank", is_flag=True, default=False, help="Create a blank data source")
632
+ @click.option("--file", type=str, help="Create a data source from a local file")
633
+ @click.option("--url", type=str, help="Create a data source from a remote URL")
634
+ @click.option("--connection", type=str, help="Create a data source from a connection")
635
+ @click.option("--prompt", type=str, help="Create a data source from a prompt")
636
+ @click.option("--s3", is_flag=True, default=False, help="Create a data source from a S3 connection")
637
+ @click.option("--gcs", is_flag=True, default=False, help="Create a data source from a GCS connection")
638
+ @click.option("--kafka", is_flag=True, default=False, help="Create a data source from a Kafka connection")
639
+ @click.pass_context
640
+ @coro
641
+ async def datasource_create(
642
+ ctx: Context,
643
+ name: str,
644
+ blank: bool,
645
+ file: str,
646
+ url: str,
647
+ connection: str,
648
+ prompt: str,
649
+ s3: bool,
650
+ gcs: bool,
651
+ kafka: bool,
652
+ ):
653
+ project: Project = ctx.ensure_object(dict)["project"]
654
+ client: TinyB = ctx.ensure_object(dict)["client"]
655
+ config = ctx.ensure_object(dict)["config"]
656
+ env: str = ctx.ensure_object(dict)["env"]
657
+
658
+ if env == "cloud":
659
+ raise CLIDatasourceException(
660
+ FeedbackManager.error(message="`tb datasource create` is not available against Tinybird Cloud.")
661
+ )
662
+
663
+ datasource_types = (
664
+ "Blank",
665
+ "Local file",
666
+ "Remote URL",
667
+ "Kafka",
668
+ "S3",
669
+ "GCS",
670
+ )
671
+ datasource_type: Optional[str] = None
672
+ connection_file: Optional[str] = None
673
+ ds_content = ""
674
+
675
+ if file:
676
+ datasource_type = "Local file"
677
+ elif url:
678
+ datasource_type = "Remote URL"
679
+ elif blank:
680
+ datasource_type = "Blank"
681
+ elif connection:
682
+ connection_files = project.get_connection_files()
683
+ connection_file = next((f for f in connection_files if f.endswith(f"{connection}.connection")), None)
684
+ if connection_file:
685
+ connection_content = Path(connection_file).read_text()
686
+ if project.is_kafka_connection(connection_content):
687
+ datasource_type = "Kafka"
688
+ elif project.is_s3_connection(connection_content):
689
+ datasource_type = "S3"
690
+ elif project.is_gcs_connection(connection_content):
691
+ datasource_type = "GCS"
692
+ elif s3:
693
+ datasource_type = "S3"
694
+ elif gcs:
695
+ datasource_type = "GCS"
696
+ elif kafka:
697
+ datasource_type = "Kafka"
698
+ elif prompt:
699
+ click.echo(FeedbackManager.gray(message="\n» Creating .datasource file..."))
700
+ user_token = config.get("user_token")
701
+ if not user_token:
702
+ raise Exception("This action requires authentication. Run 'tb login' first.")
703
+ project_config = CLIConfig.get_project_config()
704
+ tb_client: TinyB = project_config.get_client(token=config.get("token"), host=config.get("host"))
705
+ await create_resources_from_prompt(tb_client, user_token, prompt, project)
706
+ click.echo(FeedbackManager.success(message="✓ .datasource created!"))
707
+ return
708
+
709
+ if datasource_type is None:
710
+ click.echo(FeedbackManager.highlight(message="? Where do you want to create your .datasource from?"))
711
+ datasource_type_index = -1
712
+
713
+ while datasource_type_index == -1:
714
+ for index, datasource_type in enumerate(datasource_types):
715
+ click.echo(f" [{index + 1}] {datasource_type}")
716
+ click.echo(
717
+ FeedbackManager.gray(
718
+ message="Tip: Run `tb datasource create --file | --url | --connection` to skip this step."
719
+ )
720
+ )
721
+ datasource_type_index = click.prompt("\nSelect option", default=1)
722
+
723
+ if datasource_type_index == 0:
724
+ click.echo(FeedbackManager.warning(message="Datasource type selection cancelled by user"))
725
+ return None
726
+
727
+ try:
728
+ datasource_type = datasource_types[int(datasource_type_index) - 1]
729
+ except Exception:
730
+ datasource_type_index = -1
731
+
732
+ if not datasource_type:
733
+ click.echo(
734
+ FeedbackManager.error(
735
+ message=f"Invalid option: {datasource_type_index}. Please select a valid option from the list above."
736
+ )
737
+ )
738
+ return
739
+
740
+ connection_required = datasource_type in ("Kafka", "S3", "GCS")
741
+
742
+ if connection_required:
743
+ click.echo(FeedbackManager.gray(message="\n» Creating .datasource file..."))
744
+ connection_type = datasource_type.lower()
745
+
746
+ def get_connection_files():
747
+ connection_files = []
748
+ if connection_type == "kafka":
749
+ connection_files = project.get_kafka_connection_files()
750
+ elif connection_type == "s3":
751
+ connection_files = project.get_s3_connection_files()
752
+ elif connection_type == "gcs":
753
+ connection_files = project.get_gcs_connection_files()
754
+ return connection_files
755
+
756
+ connection_files = get_connection_files()
757
+ if len(connection_files) == 0:
758
+ click.echo(FeedbackManager.error(message=f"x No {datasource_type} connections found."))
759
+ if click.confirm(
760
+ FeedbackManager.highlight(message=f"\n? Do you want to create a {datasource_type} connection? [Y/n]"),
761
+ show_default=False,
762
+ default=True,
763
+ ):
764
+ click.echo(FeedbackManager.gray(message="\n» Creating .connection file..."))
765
+ default_connection_name = f"{datasource_type.lower()}_{generate_short_id()}"
766
+ connection_name = click.prompt(
767
+ FeedbackManager.highlight(message=f"? Connection name [{default_connection_name}]"),
768
+ show_default=False,
769
+ default=default_connection_name,
770
+ )
771
+ if datasource_type == "Kafka":
772
+ await generate_kafka_connection_with_secrets(connection_name, folder=project.folder)
773
+ elif datasource_type == "S3":
774
+ await generate_aws_iamrole_connection_file_with_secret(
775
+ connection_name,
776
+ service="s3",
777
+ role_arn_secret_name="S3_ARN",
778
+ region="eu-west-1",
779
+ folder=project.folder,
780
+ with_default_secret=True,
781
+ )
782
+ elif datasource_type == "GCS":
783
+ await generate_gcs_connection_file_with_secrets(
784
+ connection_name,
785
+ service="gcs",
786
+ svc_account_creds="GCS_SERVICE_ACCOUNT_CREDENTIALS_JSON",
787
+ folder=project.folder,
788
+ )
789
+ click.echo(FeedbackManager.info(message=f"/connections/{connection_name}.connection"))
790
+ click.echo(FeedbackManager.success(message="✓ .connection created!"))
791
+ connection_files = get_connection_files()
792
+ else:
793
+ click.echo(
794
+ FeedbackManager.info(message=f"→ Run `tb connection create {datasource_type.lower()}` to add one.")
795
+ )
796
+ return
797
+
798
+ if not connection_file:
799
+ connection_file = connection_files[0]
800
+ connection_path = Path(connection_file)
801
+ connection = connection_path.stem
802
+
803
+ ds_content = """SCHEMA >
804
+ `timestamp` DateTime `json:$.timestamp`,
805
+ `session_id` String `json:$.session_id`
806
+ """
807
+
808
+ if datasource_type == "Local file":
809
+ click.echo(FeedbackManager.gray(message="\n» Creating .datasource file..."))
810
+ if not file:
811
+ file = click.prompt(FeedbackManager.highlight(message="? Path"))
812
+ if file.startswith("~"):
813
+ file = os.path.expanduser(file)
814
+
815
+ folder_path = project.path
816
+ path = folder_path / file
817
+ if not path.exists():
818
+ path = Path(file)
819
+
820
+ data_format = path.suffix.lstrip(".")
821
+ ds_content = await analyze_file(str(path), client, format=data_format)
822
+ default_name = normalize_datasource_name(path.stem)
823
+ name = name or click.prompt(
824
+ FeedbackManager.highlight(message=f"? Data source name [{default_name}]"),
825
+ default=default_name,
826
+ show_default=False,
827
+ )
828
+
829
+ if datasource_type == "Remote URL":
830
+ click.echo(FeedbackManager.gray(message="\n» Creating .datasource file..."))
831
+ if not url:
832
+ url = click.prompt(FeedbackManager.highlight(message="? URL"))
833
+ format = url.split(".")[-1]
834
+ ds_content = await analyze_file(url, client, format)
835
+ default_name = normalize_datasource_name(Path(url).stem)
836
+ name = name or click.prompt(
837
+ FeedbackManager.highlight(message=f"? Data source name [{default_name}]"),
838
+ default=default_name,
839
+ show_default=False,
840
+ )
841
+
842
+ if datasource_type == "Blank":
843
+ click.echo(FeedbackManager.gray(message="\n» Creating .datasource file..."))
844
+
845
+ if datasource_type not in ("Remote URL", "Local file"):
846
+ default_name = f"ds_{generate_short_id()}"
847
+ name = name or click.prompt(
848
+ FeedbackManager.highlight(message=f"? Data source name [{default_name}]"),
849
+ default=default_name,
850
+ show_default=False,
851
+ )
852
+
853
+ if datasource_type == "Kafka":
854
+ connections = await client.connections("kafka")
855
+ connection_id = next((c["id"] for c in connections if c["name"] == connection), connection)
856
+ try:
857
+ topics = await client.kafka_list_topics(connection_id) if connection_id else []
858
+ except Exception:
859
+ topics = []
860
+ topic = topics[0] if len(topics) > 0 else "topic_0"
861
+ group_id = generate_kafka_group_id(topic)
862
+ ds_content += f"""
863
+ KAFKA_CONNECTION_NAME {connection}
864
+ KAFKA_TOPIC {{{{ tb_secret("KAFKA_TOPIC", "{topic}") }}}}
865
+ KAFKA_GROUP_ID {{{{ tb_secret("KAFKA_GROUP_ID", "{group_id}") }}}}
866
+ """
867
+
868
+ if datasource_type == "S3":
869
+ if not connection:
870
+ connections = await client.connections("s3")
871
+ connection = next((c["name"] for c in connections if c["name"] == connection), connection)
872
+ ds_content += f"""
873
+ IMPORT_CONNECTION_NAME "{connection}"
874
+ IMPORT_BUCKET_URI "s3://my-bucket/*.csv"
875
+ IMPORT_SCHEDULE "@auto"
876
+ """
877
+
878
+ if datasource_type == "GCS":
879
+ if not connection:
880
+ connections = await client.connections("gcs")
881
+ connection = next((c["name"] for c in connections if c["name"] == connection), connection)
882
+ ds_content += f"""
883
+ IMPORT_CONNECTION_NAME "{connection}"
884
+ IMPORT_BUCKET_URI "gs://my-bucket/*.csv"
885
+ IMPORT_SCHEDULE "@auto"
886
+ """
887
+
888
+ click.echo(FeedbackManager.info(message=f"/datasources/{name}.datasource"))
889
+ datasources_path = project.path / "datasources"
890
+ if not datasources_path.exists():
891
+ datasources_path.mkdir()
892
+ ds_file = datasources_path / f"{name}.datasource"
893
+ if not ds_file.exists():
894
+ ds_file.touch()
895
+ ds_file.write_text(ds_content)
896
+ click.echo(FeedbackManager.success(message="✓ .datasource created!"))
897
+
898
+
899
+ def generate_short_id():
900
+ return str(uuid.uuid4())[:4]
901
+
902
+
903
+ def generate_kafka_group_id(topic: str):
904
+ return f"{topic}_{int(datetime.timestamp(datetime.now()))}"
@@ -521,6 +521,14 @@ def create_deployment(
521
521
  TINYBIRD_API_URL = f"{client.host}/v1/deploy"
522
522
  TINYBIRD_API_KEY = client.token
523
523
 
524
+ if project.has_deeper_level():
525
+ click.echo(
526
+ FeedbackManager.warning(
527
+ message="\nDefault max_depth is 2, but project has deeper level. "
528
+ "You can change max_depth by running `tb --max-depth <depth> <cmd>`"
529
+ )
530
+ )
531
+
524
532
  files = [
525
533
  ("context://", ("cli-version", "1.0.0", "text/plain")),
526
534
  ]
@@ -30,6 +30,17 @@ class Project:
30
30
  project_files.extend(glob.glob(f"{self.path}{'/*' * level}/*.{extension}", recursive=True))
31
31
  return project_files
32
32
 
33
+ def has_deeper_level(self) -> bool:
34
+ """Check if there are folders with depth greater than max_depth in project path.
35
+
36
+ Returns:
37
+ bool: True if there are folders deeper than max_depth, False otherwise
38
+ """
39
+ for obj in glob.glob(f"{self.path}{'/*' * (self.max_depth - 1)}/*", recursive=False):
40
+ if Path(obj).is_dir():
41
+ return True
42
+ return False
43
+
33
44
  def get_project_files(self) -> List[str]:
34
45
  project_files: List[str] = []
35
46
  for extension in self.extensions:
@@ -66,6 +77,15 @@ class Project:
66
77
  def get_connection_files(self) -> List[str]:
67
78
  return self.get_files("connection")
68
79
 
80
+ def get_kafka_connection_files(self) -> List[str]:
81
+ return [f for f in self.get_connection_files() if self.is_kafka_connection(Path(f).read_text())]
82
+
83
+ def get_s3_connection_files(self) -> List[str]:
84
+ return [f for f in self.get_connection_files() if self.is_s3_connection(Path(f).read_text())]
85
+
86
+ def get_gcs_connection_files(self) -> List[str]:
87
+ return [f for f in self.get_connection_files() if self.is_gcs_connection(Path(f).read_text())]
88
+
69
89
  def get_pipe_datafile(self, filename: str) -> Optional[Datafile]:
70
90
  try:
71
91
  return parse_pipe(filename).datafile
@@ -96,3 +116,27 @@ class Project:
96
116
  @staticmethod
97
117
  def is_endpoint(content: str) -> bool:
98
118
  return re.search(r"TYPE endpoint", content, re.IGNORECASE) is not None
119
+
120
+ @staticmethod
121
+ def is_kafka_connection(content: str) -> bool:
122
+ return re.search(r"TYPE kafka", content, re.IGNORECASE) is not None
123
+
124
+ @staticmethod
125
+ def is_s3_connection(content: str) -> bool:
126
+ return re.search(r"TYPE s3", content, re.IGNORECASE) is not None
127
+
128
+ @staticmethod
129
+ def is_gcs_connection(content: str) -> bool:
130
+ return re.search(r"TYPE gcs", content, re.IGNORECASE) is not None
131
+
132
+ @staticmethod
133
+ def is_kafka_datasource(content: str) -> bool:
134
+ return re.search(r"KAFKA_CONNECTION_NAME", content, re.IGNORECASE) is not None
135
+
136
+ @staticmethod
137
+ def is_s3_datasource(content: str) -> bool:
138
+ return re.search(r"IMPORT_CONNECTION_NAME", content, re.IGNORECASE) is not None
139
+
140
+ @staticmethod
141
+ def is_gcs_datasource(content: str) -> bool:
142
+ return re.search(r"IMPORT_CONNECTION_NAME", content, re.IGNORECASE) is not None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: tinybird
3
- Version: 0.0.1.dev188
3
+ Version: 0.0.1.dev190
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/forward/commands
6
6
  Author: Tinybird
@@ -12,21 +12,21 @@ tinybird/syncasync.py,sha256=IPnOx6lMbf9SNddN1eBtssg8vCLHMt76SuZ6YNYm-Yk,27761
12
12
  tinybird/tornado_template.py,sha256=jjNVDMnkYFWXflmT8KU_Ssbo5vR8KQq3EJMk5vYgXRw,41959
13
13
  tinybird/ch_utils/constants.py,sha256=aYvg2C_WxYWsnqPdZB1ZFoIr8ZY-XjUXYyHKE9Ansj0,3890
14
14
  tinybird/ch_utils/engine.py,sha256=X4tE9OrfaUy6kO9cqVEzyI9cDcmOF3IAssRRzsTsfEQ,40781
15
- tinybird/tb/__cli__.py,sha256=0dMMoeV7WGBd5xykwKr1XNZzsV203grOWkx1PgeDVgo,247
15
+ tinybird/tb/__cli__.py,sha256=Uk4h64toVqAfnRJ4rIo4K3vfl_FunHW228-sIHLBDt0,247
16
16
  tinybird/tb/check_pypi.py,sha256=rW4QmDRbtgKdUUwJCnBkVjmTjZSZGN-XgZhx7vMkC0w,1009
17
17
  tinybird/tb/cli.py,sha256=u3eGOhX0MHkuT6tiwaZ0_3twqLmqKXDAOxF7yV_Nn9Q,1075
18
- tinybird/tb/client.py,sha256=59GH0IoYSV_KUG0eEbDDYHSWH4OlkVnSRFXE3mYAM0s,56571
18
+ tinybird/tb/client.py,sha256=CO-dQw8h28X6T6IO-Z79yPBKaJQT1Rwya5b6gexvw58,56491
19
19
  tinybird/tb/config.py,sha256=jT9xndpeCY_g0HdB5qE2EquC0TFRRnkPnQFWZWd04jo,3998
20
- tinybird/tb/modules/build.py,sha256=rRL5XKBdadMc9uVDEUt0GXm0h09Y6XXw199rdmRI1qo,19127
20
+ tinybird/tb/modules/build.py,sha256=KvF0s8hGgY_rZs7jSqYiauCk3MAlCmW_gQtnsJDJWBk,19411
21
21
  tinybird/tb/modules/cicd.py,sha256=Njb6eZOHHbUkoJJx6KoixO9PsfA_T-3Ybkya9-50Ca8,7328
22
22
  tinybird/tb/modules/cli.py,sha256=dXZs-MuqYPvxStVj7aLg36LwXtEB8NzTobDmHV9nzZI,15508
23
- tinybird/tb/modules/common.py,sha256=DYCjpj0iBaCDZ8BJ0MNG_6m6NyFMCrpQShIajHKLIfM,83373
23
+ tinybird/tb/modules/common.py,sha256=6SYrDH-GhWm8N8S2GmHdIbvf1mUYIzWYlz0WcjDIhGw,83947
24
24
  tinybird/tb/modules/config.py,sha256=ziqW_t_mRVvWOd85VoB4vKyvgMkEfpXDf9H4v38p2xc,11422
25
- tinybird/tb/modules/connection.py,sha256=7oOR7x4PhBcm1ETFFCH2YJ_3oeGXjAbmx1cnZX9_L70,9014
25
+ tinybird/tb/modules/connection.py,sha256=z1xWP2gtjKEbjc4ZF1aD7QUgl8V--wf2IRNy-4sRFm8,9779
26
26
  tinybird/tb/modules/copy.py,sha256=2Mm4FWKehOG7CoOhiF1m9UZJgJn0W1_cMolqju8ONYg,5805
27
- tinybird/tb/modules/create.py,sha256=sfIOcN3tujt7O1r9RNWqhhI-gQTDnO6zEgMwZHH2D8s,20201
28
- tinybird/tb/modules/datasource.py,sha256=0_6Cn07p5GoNBBGdu88pSeLvTWojln1-k23FsS8jTDs,17801
29
- tinybird/tb/modules/deployment.py,sha256=t6DDLJ1YdY3SJiTPbEG7CRblSLkbuqwzauQ9y65FWtY,27147
27
+ tinybird/tb/modules/create.py,sha256=2uW-4t7c7e4xkZ-GpK_8XaA-nuXwklq7rTks4k6qrtI,20917
28
+ tinybird/tb/modules/datasource.py,sha256=gycHinGTDxZMDqwbIaRJkR2SUxdhbr1prCFQHkssgVs,35124
29
+ tinybird/tb/modules/deployment.py,sha256=OGJdriqywhCtsG7rKLFtVSLjoEbbva1Nb29-jQBk3wM,27432
30
30
  tinybird/tb/modules/deprecations.py,sha256=rrszC1f_JJeJ8mUxGoCxckQTJFBCR8wREf4XXXN-PRc,4507
31
31
  tinybird/tb/modules/dev_server.py,sha256=57FCKuWpErwYUYgHspYDkLWEm9F4pbvVOtMrFXX1fVU,10129
32
32
  tinybird/tb/modules/endpoint.py,sha256=XySDt3pk66vxOZ0egUfz4bY8bEk3BjOXkv-L0OIJ3sc,12083
@@ -45,7 +45,7 @@ tinybird/tb/modules/materialization.py,sha256=QJX5kCPhhm6IXBO1JsalVfbQdypCe_eOUD
45
45
  tinybird/tb/modules/mock.py,sha256=IyHweMUM6bUH8IhyiX2tTMpdVpTFUeAJ41lZ5P42-HQ,5303
46
46
  tinybird/tb/modules/open.py,sha256=OuctINN77oexpSjth9uoIZPCelKO4Li-yyVxeSnk1io,1371
47
47
  tinybird/tb/modules/pipe.py,sha256=AQKEDagO6e3psPVjJkS_MDbn8aK-apAiLp26k7jgAV0,2432
48
- tinybird/tb/modules/project.py,sha256=_UWO79zhh2H0UA9F0wpKXYaylFPdjhGNmNAjIOSBvT8,3425
48
+ tinybird/tb/modules/project.py,sha256=iIEaBQsdLXyzJ_45Paf1jwbSrtTwWv131VCuPsTQttA,5215
49
49
  tinybird/tb/modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
50
50
  tinybird/tb/modules/secret.py,sha256=WsqzxxLh9W_jkuHL2JofMXdIJy0lT5WEI-7bQSIDgAc,2921
51
51
  tinybird/tb/modules/shell.py,sha256=Zd_4Ak_5tKVX-cw6B4ag36xZeEGHeh-jZpAsIXkoMoE,14116
@@ -80,8 +80,8 @@ tinybird/tb_cli_modules/config.py,sha256=IsgdtFRnUrkY8-Zo32lmk6O7u3bHie1QCxLwgp4
80
80
  tinybird/tb_cli_modules/exceptions.py,sha256=pmucP4kTF4irIt7dXiG-FcnI-o3mvDusPmch1L8RCWk,3367
81
81
  tinybird/tb_cli_modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
82
82
  tinybird/tb_cli_modules/telemetry.py,sha256=Hh2Io8ZPROSunbOLuMvuIFU4TqwWPmQTqal4WS09K1A,10449
83
- tinybird-0.0.1.dev188.dist-info/METADATA,sha256=psVJzC4sJRKjG8Re5Jjo0GNwSrW5JdCAxGrcrzcWgoY,1608
84
- tinybird-0.0.1.dev188.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
85
- tinybird-0.0.1.dev188.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
86
- tinybird-0.0.1.dev188.dist-info/top_level.txt,sha256=VqqqEmkAy7UNaD8-V51FCoMMWXjLUlR0IstvK7tJYVY,54
87
- tinybird-0.0.1.dev188.dist-info/RECORD,,
83
+ tinybird-0.0.1.dev190.dist-info/METADATA,sha256=MDRLrgiaBGisoaGthlSBKhjMSvdw8XSWPF5VEUsjrvM,1608
84
+ tinybird-0.0.1.dev190.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
85
+ tinybird-0.0.1.dev190.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
86
+ tinybird-0.0.1.dev190.dist-info/top_level.txt,sha256=VqqqEmkAy7UNaD8-V51FCoMMWXjLUlR0IstvK7tJYVY,54
87
+ tinybird-0.0.1.dev190.dist-info/RECORD,,