tinybird 0.0.1.dev94__tar.gz → 0.0.1.dev96__tar.gz

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

Potentially problematic release.


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

Files changed (108) hide show
  1. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/PKG-INFO +1 -1
  2. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/prompts.py +29 -1
  3. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/sql_template.py +84 -0
  4. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/__cli__.py +2 -2
  5. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/cli.py +1 -0
  6. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/build.py +11 -0
  7. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/cicd.py +7 -0
  8. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/cli.py +43 -4
  9. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/common.py +2 -0
  10. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/create.py +25 -5
  11. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/common.py +29 -5
  12. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/parse_datasource.py +2 -1
  13. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/parse_pipe.py +2 -1
  14. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/deployment.py +23 -3
  15. tinybird-0.0.1.dev96/tinybird/tb/modules/infra.py +507 -0
  16. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/mock.py +4 -4
  17. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/project.py +4 -0
  18. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/watch.py +1 -1
  19. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird.egg-info/PKG-INFO +1 -1
  20. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird.egg-info/SOURCES.txt +1 -0
  21. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/setup.cfg +0 -0
  22. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/__cli__.py +0 -0
  23. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/ch_utils/constants.py +0 -0
  24. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/ch_utils/engine.py +0 -0
  25. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/check_pypi.py +0 -0
  26. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/client.py +0 -0
  27. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/config.py +0 -0
  28. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/connectors.py +0 -0
  29. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/context.py +0 -0
  30. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/datafile.py +0 -0
  31. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/datatypes.py +0 -0
  32. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/feedback_manager.py +0 -0
  33. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/git_settings.py +0 -0
  34. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/sql.py +0 -0
  35. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/sql_template_fmt.py +0 -0
  36. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/sql_toolset.py +0 -0
  37. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/syncasync.py +0 -0
  38. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/auth.py +0 -0
  39. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/config.py +0 -0
  40. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/connection.py +0 -0
  41. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/copy.py +0 -0
  42. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/build.py +0 -0
  43. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/build_common.py +0 -0
  44. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/build_datasource.py +0 -0
  45. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/build_pipe.py +0 -0
  46. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/diff.py +0 -0
  47. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/exceptions.py +0 -0
  48. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/fixture.py +0 -0
  49. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/format_common.py +0 -0
  50. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/format_datasource.py +0 -0
  51. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/format_pipe.py +0 -0
  52. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/pipe_checker.py +0 -0
  53. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/playground.py +0 -0
  54. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/pull.py +0 -0
  55. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datasource.py +0 -0
  56. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/endpoint.py +0 -0
  57. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/exceptions.py +0 -0
  58. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/feedback_manager.py +0 -0
  59. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/fmt.py +0 -0
  60. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/job.py +0 -0
  61. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/llm.py +0 -0
  62. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/llm_utils.py +0 -0
  63. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/local.py +0 -0
  64. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/local_common.py +0 -0
  65. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/login.py +0 -0
  66. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/logout.py +0 -0
  67. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/materialization.py +0 -0
  68. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/open.py +0 -0
  69. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/pipe.py +0 -0
  70. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/playground.py +0 -0
  71. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/regions.py +0 -0
  72. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/secret.py +0 -0
  73. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/shell.py +0 -0
  74. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/table.py +0 -0
  75. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/tag.py +0 -0
  76. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/telemetry.py +0 -0
  77. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/test.py +0 -0
  78. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/tinyunit/tinyunit.py +0 -0
  79. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/tinyunit/tinyunit_lib.py +0 -0
  80. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/token.py +0 -0
  81. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/workspace.py +0 -0
  82. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/workspace_members.py +0 -0
  83. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli.py +0 -0
  84. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/auth.py +0 -0
  85. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/branch.py +0 -0
  86. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/cicd.py +0 -0
  87. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/cli.py +0 -0
  88. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/common.py +0 -0
  89. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/config.py +0 -0
  90. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/connection.py +0 -0
  91. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/datasource.py +0 -0
  92. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/exceptions.py +0 -0
  93. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/fmt.py +0 -0
  94. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/job.py +0 -0
  95. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/pipe.py +0 -0
  96. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/regions.py +0 -0
  97. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/tag.py +0 -0
  98. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/telemetry.py +0 -0
  99. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/test.py +0 -0
  100. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  101. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
  102. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/workspace.py +0 -0
  103. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  104. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tornado_template.py +0 -0
  105. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird.egg-info/dependency_links.txt +0 -0
  106. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird.egg-info/entry_points.txt +0 -0
  107. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird.egg-info/requires.txt +0 -0
  108. {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: tinybird
3
- Version: 0.0.1.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
@@ -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
 
@@ -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)}")
@@ -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'
@@ -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"))