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.
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/PKG-INFO +1 -1
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/prompts.py +29 -1
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/sql_template.py +84 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/__cli__.py +2 -2
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/cli.py +1 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/build.py +11 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/cicd.py +7 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/cli.py +43 -4
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/common.py +2 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/create.py +25 -5
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/common.py +29 -5
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/parse_datasource.py +2 -1
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/parse_pipe.py +2 -1
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/deployment.py +23 -3
- tinybird-0.0.1.dev96/tinybird/tb/modules/infra.py +507 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/mock.py +4 -4
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/project.py +4 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/watch.py +1 -1
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird.egg-info/PKG-INFO +1 -1
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird.egg-info/SOURCES.txt +1 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/setup.cfg +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/__cli__.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/ch_utils/constants.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/ch_utils/engine.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/check_pypi.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/client.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/config.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/connectors.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/context.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/datafile.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/datatypes.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/feedback_manager.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/git_settings.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/sql.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/sql_template_fmt.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/sql_toolset.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/syncasync.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/auth.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/config.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/connection.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/copy.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/build.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/build_common.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/build_datasource.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/build_pipe.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/diff.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/exceptions.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/fixture.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/format_common.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/format_datasource.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/format_pipe.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/pipe_checker.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/playground.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/pull.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datasource.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/endpoint.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/exceptions.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/feedback_manager.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/fmt.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/job.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/llm.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/llm_utils.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/local.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/local_common.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/login.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/logout.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/materialization.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/open.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/pipe.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/playground.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/regions.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/secret.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/shell.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/table.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/tag.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/telemetry.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/test.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/tinyunit/tinyunit.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/tinyunit/tinyunit_lib.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/token.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/workspace.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/workspace_members.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/auth.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/branch.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/cicd.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/cli.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/common.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/config.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/connection.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/datasource.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/exceptions.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/fmt.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/job.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/pipe.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/regions.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/tag.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/telemetry.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/test.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/workspace.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb_cli_modules/workspace_members.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tornado_template.py +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird.egg-info/dependency_links.txt +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird.egg-info/entry_points.txt +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird.egg-info/requires.txt +0 -0
- {tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird.egg-info/top_level.txt +0 -0
|
@@ -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.
|
|
8
|
-
__revision__ = '
|
|
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
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
1288
|
+
lineno = kwargs["lineno"]
|
|
1278
1289
|
if len(args) < 2:
|
|
1279
1290
|
raise DatafileSyntaxError(
|
|
1280
|
-
message='TOKEN takes two params: token name and
|
|
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
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
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")
|
{tinybird-0.0.1.dev94 → tinybird-0.0.1.dev96}/tinybird/tb/modules/datafile/parse_datasource.py
RENAMED
|
@@ -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
|
-
|
|
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=
|
|
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"))
|