tinybird 0.0.1.dev261__tar.gz → 0.0.1.dev263__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.dev261 → tinybird-0.0.1.dev263}/PKG-INFO +1 -1
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/datafile/common.py +151 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/sql_template.py +8 -1
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/__cli__.py +2 -2
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/agent.py +130 -17
- tinybird-0.0.1.dev263/tinybird/tb/modules/agent/banner.py +131 -0
- tinybird-0.0.1.dev263/tinybird/tb/modules/agent/command_agent.py +59 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/prompts.py +91 -20
- tinybird-0.0.1.dev263/tinybird/tb/modules/agent/testing_agent.py +62 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/create_datafile.py +1 -1
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/execute_query.py +18 -0
- tinybird-0.0.1.dev263/tinybird/tb/modules/agent/tools/run_command.py +38 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/test.py +28 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/utils.py +7 -2
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/cli.py +8 -6
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/datasource.py +3 -1
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/deployment.py +32 -5
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/deployment_common.py +5 -1
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/test_common.py +11 -2
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird.egg-info/PKG-INFO +1 -1
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird.egg-info/SOURCES.txt +3 -0
- tinybird-0.0.1.dev261/tinybird/tb/modules/agent/banner.py +0 -56
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/setup.cfg +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/__cli__.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/ch_utils/constants.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/ch_utils/engine.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/check_pypi.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/client.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/config.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/connectors.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/context.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/datafile/exceptions.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/datafile/parse_connection.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/datafile/parse_datasource.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/datafile/parse_pipe.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/datatypes.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/feedback_manager.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/git_settings.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/prompts.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/sql.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/sql_template_fmt.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/sql_toolset.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/syncasync.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/check_pypi.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/cli.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/client.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/config.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/__init__.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/animations.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/memory.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/models.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/__init__.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/analyze.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/append.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/build.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/deploy.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/deploy_check.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/diff_resource.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/explore.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/get_endpoint_stats.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/get_openapi_definition.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/mock.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/plan.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/preview_datafile.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/agent/tools/request_endpoint.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/build.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/build_common.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/cicd.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/common.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/config.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/connection.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/copy.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/create.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/datafile/build.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/datafile/build_common.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/datafile/build_datasource.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/datafile/build_pipe.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/datafile/diff.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/datafile/fixture.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/datafile/format_common.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/datafile/format_datasource.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/datafile/format_pipe.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/datafile/pipe_checker.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/datafile/playground.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/datafile/pull.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/deprecations.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/dev_server.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/endpoint.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/exceptions.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/feedback_manager.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/info.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/infra.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/job.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/llm.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/llm_utils.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/local.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/local_common.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/login.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/login_common.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/logout.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/materialization.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/mock.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/mock_common.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/open.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/pipe.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/project.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/regions.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/secret.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/secret_common.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/shell.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/sink.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/table.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/telemetry.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/test.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/tinyunit/tinyunit.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/tinyunit/tinyunit_lib.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/token.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/watch.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/workspace.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb/modules/workspace_members.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb_cli.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/auth.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/branch.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/cicd.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/cli.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/common.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/config.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/connection.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/datasource.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/exceptions.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/fmt.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/job.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/pipe.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/regions.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/tag.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/telemetry.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/test.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/workspace.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tb_cli_modules/workspace_members.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird/tornado_template.py +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird.egg-info/dependency_links.txt +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird.egg-info/entry_points.txt +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird.egg-info/requires.txt +0 -0
- {tinybird-0.0.1.dev261 → tinybird-0.0.1.dev263}/tinybird.egg-info/top_level.txt +0 -0
|
@@ -461,6 +461,15 @@ class Datafile:
|
|
|
461
461
|
raise DatafileValidationError(
|
|
462
462
|
f"Invalid permission {token['permission']} for token {token['token_name']}. Only READ and APPEND are allowed for datasources"
|
|
463
463
|
)
|
|
464
|
+
|
|
465
|
+
# Validate sorting key if present
|
|
466
|
+
if "engine" in node and isinstance(node["engine"], dict) and "args" in node["engine"]:
|
|
467
|
+
for arg_name, arg_value in node["engine"]["args"]:
|
|
468
|
+
if arg_name.lower() == "sorting_key":
|
|
469
|
+
# Check for sorting key constraints
|
|
470
|
+
self._validate_sorting_key(arg_value, node)
|
|
471
|
+
break
|
|
472
|
+
|
|
464
473
|
# Validate Kafka params
|
|
465
474
|
if any(param in node for param in KAFKA_PARAMS) and (
|
|
466
475
|
missing := [param for param in REQUIRED_KAFKA_PARAMS if param not in node]
|
|
@@ -483,6 +492,148 @@ class Datafile:
|
|
|
483
492
|
# We cannot validate a datafile whose kind is unknown
|
|
484
493
|
pass
|
|
485
494
|
|
|
495
|
+
def _validate_sorting_key(self, sorting_key: str, node: Dict[str, Any]) -> None:
|
|
496
|
+
"""
|
|
497
|
+
Validates that a sorting key doesn't reference:
|
|
498
|
+
- Nullable columns
|
|
499
|
+
- AggregateFunction types
|
|
500
|
+
- Engine version columns for ReplacingMergeTree
|
|
501
|
+
"""
|
|
502
|
+
if sorting_key == "tuple()" or not sorting_key:
|
|
503
|
+
return # Empty sorting key is valid
|
|
504
|
+
|
|
505
|
+
engine_ver_column = self._extract_engine_ver_column(node)
|
|
506
|
+
schema_columns = {col["name"]: col for col in node["columns"]}
|
|
507
|
+
sorting_key_columns = self._parse_sorting_key_columns(sorting_key, engine_ver_column)
|
|
508
|
+
|
|
509
|
+
self._validate_columns_against_schema(sorting_key_columns, schema_columns)
|
|
510
|
+
|
|
511
|
+
def _extract_engine_ver_column(self, node: Dict[str, Any]) -> Optional[str]:
|
|
512
|
+
engine_info = node.get("engine", {})
|
|
513
|
+
|
|
514
|
+
if not isinstance(engine_info, dict):
|
|
515
|
+
return None
|
|
516
|
+
|
|
517
|
+
engine_type = engine_info.get("type", "")
|
|
518
|
+
if engine_type != "ReplacingMergeTree":
|
|
519
|
+
return None
|
|
520
|
+
|
|
521
|
+
engine_args = engine_info.get("args", [])
|
|
522
|
+
for arg_name, arg_value in engine_args:
|
|
523
|
+
if arg_name == "ver":
|
|
524
|
+
return arg_value
|
|
525
|
+
|
|
526
|
+
return None
|
|
527
|
+
|
|
528
|
+
def _parse_sorting_key_columns(self, sorting_key: str, engine_ver_column: Optional[str]) -> List[str]:
|
|
529
|
+
"""Parse sorting key to extract column names and validate constraints."""
|
|
530
|
+
# Validate ENGINE_VER column constraint early
|
|
531
|
+
if engine_ver_column and engine_ver_column in sorting_key:
|
|
532
|
+
raise DatafileValidationError(
|
|
533
|
+
f"ENGINE_VER column '{engine_ver_column}' cannot be included in the sorting key for ReplacingMergeTree. "
|
|
534
|
+
f"Including the version column in the sorting key prevents deduplication because rows with different "
|
|
535
|
+
f"versions will have different sorting keys and won't be considered duplicates. The sorting key should "
|
|
536
|
+
f"define the record identity (what makes it unique), while ENGINE_VER tracks which version to keep."
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
# Remove tuple() wrapper if present
|
|
540
|
+
column_str = sorting_key
|
|
541
|
+
if column_str.startswith("tuple(") and column_str.endswith(")"):
|
|
542
|
+
column_str = column_str[6:-1]
|
|
543
|
+
|
|
544
|
+
sorting_key_columns = []
|
|
545
|
+
|
|
546
|
+
for part in column_str.split(","):
|
|
547
|
+
part = part.strip()
|
|
548
|
+
|
|
549
|
+
if self._is_aggregate_function_expression(part):
|
|
550
|
+
raise DatafileValidationError(
|
|
551
|
+
f"Sorting key contains aggregate function expression '{part}'. Aggregate function expressions cannot be used in sorting keys."
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
# Extract column names from the part
|
|
555
|
+
extracted_columns = self._extract_column_names_from_part(part)
|
|
556
|
+
sorting_key_columns.extend(extracted_columns)
|
|
557
|
+
|
|
558
|
+
return sorting_key_columns
|
|
559
|
+
|
|
560
|
+
def _is_aggregate_function_expression(self, part: str) -> bool:
|
|
561
|
+
"""Check if a sorting key part is an aggregate function expression."""
|
|
562
|
+
if not ("(" in part and part.endswith(")")):
|
|
563
|
+
return False
|
|
564
|
+
|
|
565
|
+
func_start = part.find("(")
|
|
566
|
+
func_name = part[:func_start].strip().lower()
|
|
567
|
+
|
|
568
|
+
aggregate_function_names = {
|
|
569
|
+
"sum",
|
|
570
|
+
"count",
|
|
571
|
+
"avg",
|
|
572
|
+
"min",
|
|
573
|
+
"max",
|
|
574
|
+
"any",
|
|
575
|
+
"grouparray",
|
|
576
|
+
"groupuniqarray",
|
|
577
|
+
"uniq",
|
|
578
|
+
"summerge",
|
|
579
|
+
"countmerge",
|
|
580
|
+
"avgmerge",
|
|
581
|
+
"minmerge",
|
|
582
|
+
"maxmerge",
|
|
583
|
+
"anymerge",
|
|
584
|
+
"grouparraymerge",
|
|
585
|
+
"groupuniqarraymerge",
|
|
586
|
+
"uniqmerge",
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return func_name in aggregate_function_names
|
|
590
|
+
|
|
591
|
+
def _extract_column_names_from_part(self, part: str) -> List[str]:
|
|
592
|
+
"""Extract column names from a sorting key part."""
|
|
593
|
+
columns = []
|
|
594
|
+
|
|
595
|
+
if "(" in part and part.endswith(")"):
|
|
596
|
+
# Function expression - extract column names from inside parentheses
|
|
597
|
+
func_start = part.find("(")
|
|
598
|
+
inner_content = part[func_start + 1 : -1].strip()
|
|
599
|
+
for inner_part in inner_content.split(","):
|
|
600
|
+
inner_part = inner_part.strip().strip("`")
|
|
601
|
+
if inner_part and inner_part.isidentifier():
|
|
602
|
+
columns.append(inner_part)
|
|
603
|
+
elif part:
|
|
604
|
+
# Simple column name
|
|
605
|
+
column_name = part.strip("`")
|
|
606
|
+
if column_name:
|
|
607
|
+
columns.append(column_name)
|
|
608
|
+
|
|
609
|
+
return columns
|
|
610
|
+
|
|
611
|
+
def _validate_columns_against_schema(
|
|
612
|
+
self, sorting_key_columns: List[str], schema_columns: Dict[str, Dict[str, Any]]
|
|
613
|
+
) -> None:
|
|
614
|
+
"""Validate each column in the sorting key against the schema."""
|
|
615
|
+
if not schema_columns:
|
|
616
|
+
return # No schema information available, can't validate
|
|
617
|
+
|
|
618
|
+
for col_name in sorting_key_columns:
|
|
619
|
+
if col_name not in schema_columns:
|
|
620
|
+
continue
|
|
621
|
+
|
|
622
|
+
self._validate_single_column(col_name, schema_columns[col_name])
|
|
623
|
+
|
|
624
|
+
def _validate_single_column(self, col_name: str, column_info: Dict[str, Any]) -> None:
|
|
625
|
+
"""Validate a single column for use in sorting keys."""
|
|
626
|
+
col_type = column_info.get("type", "").lower()
|
|
627
|
+
is_nullable = column_info.get("nullable", False)
|
|
628
|
+
if is_nullable:
|
|
629
|
+
raise DatafileValidationError(
|
|
630
|
+
f"Sorting key contains nullable column '{col_name}'. Nullable columns cannot be used in sorting keys."
|
|
631
|
+
)
|
|
632
|
+
if "aggregatefunction" in col_type:
|
|
633
|
+
raise DatafileValidationError(
|
|
634
|
+
f"Sorting key contains column '{col_name}' with AggregateFunction type. AggregateFunction columns cannot be used in sorting keys."
|
|
635
|
+
)
|
|
636
|
+
|
|
486
637
|
|
|
487
638
|
def format_filename(filename: str, hide_folders: bool = False):
|
|
488
639
|
return os.path.basename(filename) if hide_folders else filename
|
|
@@ -74,7 +74,7 @@ class SQLTemplateException(ValueError):
|
|
|
74
74
|
# replace_vars_smart(t)
|
|
75
75
|
# print(generate(t, **{x: '' for x in names}))
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
JOB_TIMESTAMP_PARAM = "job_timestamp"
|
|
78
78
|
DEFAULT_PARAM_NAMES = ["format", "q"]
|
|
79
79
|
RESERVED_PARAM_NAMES = [
|
|
80
80
|
"__tb__semver",
|
|
@@ -93,6 +93,7 @@ RESERVED_PARAM_NAMES = [
|
|
|
93
93
|
"tag",
|
|
94
94
|
"template_parameters",
|
|
95
95
|
"token",
|
|
96
|
+
JOB_TIMESTAMP_PARAM,
|
|
96
97
|
]
|
|
97
98
|
|
|
98
99
|
parameter_types = [
|
|
@@ -2002,6 +2003,7 @@ def format_SQLTemplateException_message(e: SQLTemplateException, vars_and_types:
|
|
|
2002
2003
|
item.get("default") is None
|
|
2003
2004
|
and item.get("used_in", None) is None
|
|
2004
2005
|
and item.get("name") not in vars_with_default_none
|
|
2006
|
+
and item.get("name") is not JOB_TIMESTAMP_PARAM
|
|
2005
2007
|
):
|
|
2006
2008
|
vars_with_default_none.append(item["name"])
|
|
2007
2009
|
|
|
@@ -2294,6 +2296,11 @@ def render_sql_template(
|
|
|
2294
2296
|
processed_variables = preprocess_variables(variables, template_variables_with_types)
|
|
2295
2297
|
variables.update(processed_variables)
|
|
2296
2298
|
|
|
2299
|
+
# Handle job_timestamp special case providing the default value if not provided
|
|
2300
|
+
if any(var["name"] == JOB_TIMESTAMP_PARAM for var in template_variables_with_types):
|
|
2301
|
+
variables = variables or {}
|
|
2302
|
+
variables.setdefault(JOB_TIMESTAMP_PARAM, datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
|
2303
|
+
|
|
2297
2304
|
if test_mode:
|
|
2298
2305
|
|
|
2299
2306
|
def dummy(*args, **kwargs):
|
|
@@ -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.
|
|
8
|
-
__revision__ = '
|
|
7
|
+
__version__ = '0.0.1.dev263'
|
|
8
|
+
__revision__ = '1d13423'
|
|
@@ -2,6 +2,7 @@ import asyncio
|
|
|
2
2
|
import shlex
|
|
3
3
|
import subprocess
|
|
4
4
|
import sys
|
|
5
|
+
import urllib.parse
|
|
5
6
|
from functools import partial
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import Any, Optional
|
|
@@ -11,13 +12,16 @@ import humanfriendly
|
|
|
11
12
|
from pydantic_ai import Agent, RunContext, Tool
|
|
12
13
|
from pydantic_ai.agent import AgentRunResult
|
|
13
14
|
from pydantic_ai.messages import ModelMessage, ModelRequest, UserPromptPart
|
|
15
|
+
from requests import Response
|
|
14
16
|
|
|
15
17
|
from tinybird.tb.client import TinyB
|
|
16
18
|
from tinybird.tb.modules.agent.animations import ThinkingAnimation
|
|
17
19
|
from tinybird.tb.modules.agent.banner import display_banner
|
|
20
|
+
from tinybird.tb.modules.agent.command_agent import CommandAgent
|
|
18
21
|
from tinybird.tb.modules.agent.memory import clear_history, clear_messages, load_messages, save_messages
|
|
19
22
|
from tinybird.tb.modules.agent.models import create_model, model_costs
|
|
20
23
|
from tinybird.tb.modules.agent.prompts import agent_system_prompt, load_custom_project_rules, resources_prompt
|
|
24
|
+
from tinybird.tb.modules.agent.testing_agent import TestingAgent
|
|
21
25
|
from tinybird.tb.modules.agent.tools.analyze import analyze_file, analyze_url
|
|
22
26
|
from tinybird.tb.modules.agent.tools.append import append_file, append_url
|
|
23
27
|
from tinybird.tb.modules.agent.tools.build import build
|
|
@@ -32,9 +36,7 @@ from tinybird.tb.modules.agent.tools.mock import mock
|
|
|
32
36
|
from tinybird.tb.modules.agent.tools.plan import plan
|
|
33
37
|
from tinybird.tb.modules.agent.tools.preview_datafile import preview_datafile
|
|
34
38
|
from tinybird.tb.modules.agent.tools.request_endpoint import request_endpoint
|
|
35
|
-
from tinybird.tb.modules.agent.
|
|
36
|
-
from tinybird.tb.modules.agent.tools.test import run_tests as run_tests_tool
|
|
37
|
-
from tinybird.tb.modules.agent.utils import AgentRunCancelled, TinybirdAgentContext, show_input
|
|
39
|
+
from tinybird.tb.modules.agent.utils import AgentRunCancelled, TinybirdAgentContext, show_confirmation, show_input
|
|
38
40
|
from tinybird.tb.modules.build_common import process as build_process
|
|
39
41
|
from tinybird.tb.modules.common import _analyze, _get_tb_client, echo_safe_humanfriendly_tables_format_pretty_table
|
|
40
42
|
from tinybird.tb.modules.config import CLIConfig
|
|
@@ -64,6 +66,7 @@ class TinybirdAgent:
|
|
|
64
66
|
self.host = host
|
|
65
67
|
self.dangerously_skip_permissions = dangerously_skip_permissions or prompt_mode
|
|
66
68
|
self.project = project
|
|
69
|
+
self.thinking_animation = ThinkingAnimation()
|
|
67
70
|
if prompt_mode:
|
|
68
71
|
self.messages: list[ModelMessage] = load_messages()[-5:]
|
|
69
72
|
else:
|
|
@@ -102,12 +105,61 @@ class TinybirdAgent:
|
|
|
102
105
|
Tool(execute_query, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
103
106
|
Tool(request_endpoint, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
104
107
|
Tool(diff_resource, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
105
|
-
Tool(create_tests_tool, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
106
|
-
Tool(run_tests_tool, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
107
108
|
],
|
|
108
109
|
history_processors=[self._context_aware_processor],
|
|
109
110
|
)
|
|
110
111
|
|
|
112
|
+
self.testing_agent = TestingAgent(
|
|
113
|
+
dangerously_skip_permissions=self.dangerously_skip_permissions,
|
|
114
|
+
prompt_mode=prompt_mode,
|
|
115
|
+
thinking_animation=self.thinking_animation,
|
|
116
|
+
token=self.token,
|
|
117
|
+
user_token=self.user_token,
|
|
118
|
+
host=self.host,
|
|
119
|
+
workspace_id=workspace_id,
|
|
120
|
+
project=self.project,
|
|
121
|
+
)
|
|
122
|
+
self.command_agent = CommandAgent(
|
|
123
|
+
dangerously_skip_permissions=self.dangerously_skip_permissions,
|
|
124
|
+
prompt_mode=prompt_mode,
|
|
125
|
+
thinking_animation=self.thinking_animation,
|
|
126
|
+
token=self.token,
|
|
127
|
+
user_token=self.user_token,
|
|
128
|
+
host=self.host,
|
|
129
|
+
workspace_id=workspace_id,
|
|
130
|
+
project=self.project,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
@self.agent.tool
|
|
134
|
+
def manage_tests(ctx: RunContext[TinybirdAgentContext], task: str) -> str:
|
|
135
|
+
"""Delegate test management to the test agent:
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
task (str): The detailed task to perform. Required.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
str: The result of the query.
|
|
142
|
+
"""
|
|
143
|
+
result = self.testing_agent.run(task, deps=ctx.deps, usage=ctx.usage)
|
|
144
|
+
|
|
145
|
+
if not result:
|
|
146
|
+
return "Could not solve the task using the test agent"
|
|
147
|
+
|
|
148
|
+
return result.output
|
|
149
|
+
|
|
150
|
+
@self.agent.tool
|
|
151
|
+
def run_command(ctx: RunContext[TinybirdAgentContext], task: str) -> str:
|
|
152
|
+
"""Solve a task using directly Tinybird CLI commands.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
task (str): The task to solve. Required.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
str: The result of the command.
|
|
159
|
+
"""
|
|
160
|
+
result = self.command_agent.run(task, deps=ctx.deps, usage=ctx.usage)
|
|
161
|
+
return result.output
|
|
162
|
+
|
|
111
163
|
@self.agent.instructions
|
|
112
164
|
def get_local_host(ctx: RunContext[TinybirdAgentContext]) -> str:
|
|
113
165
|
return f"Tinybird Local host: {ctx.deps.local_host}"
|
|
@@ -128,8 +180,6 @@ class TinybirdAgent:
|
|
|
128
180
|
def get_project_files(ctx: RunContext[TinybirdAgentContext]) -> str:
|
|
129
181
|
return resources_prompt(self.project)
|
|
130
182
|
|
|
131
|
-
self.thinking_animation = ThinkingAnimation()
|
|
132
|
-
|
|
133
183
|
def add_message(self, message: ModelMessage) -> None:
|
|
134
184
|
self.messages.append(message)
|
|
135
185
|
|
|
@@ -155,6 +205,7 @@ class TinybirdAgent:
|
|
|
155
205
|
project = self.project
|
|
156
206
|
folder = self.project.folder
|
|
157
207
|
local_client = get_tinybird_local_client(config, test=False, silent=False)
|
|
208
|
+
test_client = get_tinybird_local_client(config, test=True, silent=True)
|
|
158
209
|
return TinybirdAgentContext(
|
|
159
210
|
# context does not support the whole client, so we need to pass only the functions we need
|
|
160
211
|
explore_data=client.explore_data,
|
|
@@ -169,6 +220,8 @@ class TinybirdAgent:
|
|
|
169
220
|
execute_query_local=partial(execute_query_local, config=config),
|
|
170
221
|
request_endpoint_cloud=partial(request_endpoint_cloud, config=config),
|
|
171
222
|
request_endpoint_local=partial(request_endpoint_local, config=config),
|
|
223
|
+
build_project_test=partial(build_project_test, project=project, client=test_client),
|
|
224
|
+
get_pipe_data_test=partial(get_pipe_data_test, client=test_client),
|
|
172
225
|
get_datasource_datafile_cloud=partial(get_datasource_datafile_cloud, config=config),
|
|
173
226
|
get_datasource_datafile_local=partial(get_datasource_datafile_local, config=config),
|
|
174
227
|
get_pipe_datafile_cloud=partial(get_pipe_datafile_cloud, config=config),
|
|
@@ -176,7 +229,7 @@ class TinybirdAgent:
|
|
|
176
229
|
get_connection_datafile_cloud=partial(get_connection_datafile_cloud, config=config),
|
|
177
230
|
get_connection_datafile_local=partial(get_connection_datafile_local, config=config),
|
|
178
231
|
get_project_files=project.get_project_files,
|
|
179
|
-
run_tests=partial(run_tests, project=project,
|
|
232
|
+
run_tests=partial(run_tests, project=project, client=test_client),
|
|
180
233
|
folder=folder,
|
|
181
234
|
thinking_animation=self.thinking_animation,
|
|
182
235
|
workspace_name=self.project.workspace_name,
|
|
@@ -277,15 +330,40 @@ def run_agent(
|
|
|
277
330
|
FeedbackManager.error(message="Tinybird Code requires authentication. Run 'tb login' first.")
|
|
278
331
|
)
|
|
279
332
|
return
|
|
333
|
+
build_user_input: Optional[str] = None
|
|
334
|
+
try:
|
|
335
|
+
build_project(config, project, test=False, silent=True)
|
|
336
|
+
except CLIBuildException as e:
|
|
337
|
+
if prompt:
|
|
338
|
+
raise e
|
|
339
|
+
click.echo(FeedbackManager.error(message=e))
|
|
340
|
+
try:
|
|
341
|
+
show_confirmation(
|
|
342
|
+
title="Fix project errors?", skip_confirmation=dangerously_skip_permissions, show_review=False
|
|
343
|
+
)
|
|
344
|
+
except AgentRunCancelled:
|
|
345
|
+
click.echo(FeedbackManager.info(message="User cancelled the operation"))
|
|
346
|
+
return
|
|
280
347
|
|
|
281
|
-
|
|
348
|
+
build_user_input = f"Error building project. Fix the errors before continuing. {e}"
|
|
282
349
|
|
|
283
350
|
# In prompt mode, always skip permissions to avoid interactive prompts
|
|
284
351
|
prompt_mode = prompt is not None
|
|
285
|
-
|
|
352
|
+
|
|
353
|
+
agent = TinybirdAgent(
|
|
354
|
+
token,
|
|
355
|
+
user_token,
|
|
356
|
+
host,
|
|
357
|
+
workspace_id,
|
|
358
|
+
project,
|
|
359
|
+
dangerously_skip_permissions,
|
|
360
|
+
prompt_mode,
|
|
361
|
+
)
|
|
286
362
|
|
|
287
363
|
# Print mode: run once with the provided prompt and exit
|
|
288
364
|
if prompt:
|
|
365
|
+
if build_user_input:
|
|
366
|
+
prompt = f"User input: {prompt}\n\n{build_user_input}"
|
|
289
367
|
agent.run(prompt, config)
|
|
290
368
|
return
|
|
291
369
|
|
|
@@ -302,7 +380,8 @@ def run_agent(
|
|
|
302
380
|
try:
|
|
303
381
|
while True:
|
|
304
382
|
try:
|
|
305
|
-
user_input = show_input(workspace_name)
|
|
383
|
+
user_input = build_user_input or show_input(workspace_name)
|
|
384
|
+
build_user_input = None
|
|
306
385
|
if user_input.startswith("tb "):
|
|
307
386
|
cmd_parts = shlex.split(user_input)
|
|
308
387
|
subprocess.run(cmd_parts)
|
|
@@ -373,12 +452,12 @@ def run_agent(
|
|
|
373
452
|
|
|
374
453
|
|
|
375
454
|
def build_project(
|
|
376
|
-
config: dict[str, Any], project: Project, silent: bool =
|
|
455
|
+
config: dict[str, Any], project: Project, silent: bool = False, test: bool = True, load_fixtures: bool = False
|
|
377
456
|
) -> None:
|
|
378
|
-
|
|
457
|
+
client = get_tinybird_local_client(config, test=test, silent=silent)
|
|
379
458
|
build_error = build_process(
|
|
380
459
|
project=project,
|
|
381
|
-
tb_client=
|
|
460
|
+
tb_client=client,
|
|
382
461
|
watch=False,
|
|
383
462
|
silent=silent,
|
|
384
463
|
exit_on_error=False,
|
|
@@ -388,6 +467,23 @@ def build_project(
|
|
|
388
467
|
raise CLIBuildException(build_error)
|
|
389
468
|
|
|
390
469
|
|
|
470
|
+
def build_project_test(
|
|
471
|
+
client: TinyB,
|
|
472
|
+
project: Project,
|
|
473
|
+
silent: bool = False,
|
|
474
|
+
) -> None:
|
|
475
|
+
build_error = build_process(
|
|
476
|
+
project=project,
|
|
477
|
+
tb_client=client,
|
|
478
|
+
watch=False,
|
|
479
|
+
silent=silent,
|
|
480
|
+
exit_on_error=False,
|
|
481
|
+
load_fixtures=True,
|
|
482
|
+
)
|
|
483
|
+
if build_error:
|
|
484
|
+
raise CLIBuildException(build_error)
|
|
485
|
+
|
|
486
|
+
|
|
391
487
|
def deploy_project(config: dict[str, Any], project: Project) -> None:
|
|
392
488
|
client = _get_tb_client(config["token"], config["host"])
|
|
393
489
|
try:
|
|
@@ -408,6 +504,8 @@ def deploy_check_project(config: dict[str, Any], project: Project) -> None:
|
|
|
408
504
|
try:
|
|
409
505
|
create_deployment(project=project, client=client, config=config, check=True, wait=True, auto=True)
|
|
410
506
|
except SystemExit as e:
|
|
507
|
+
if hasattr(e, "code") and e.code == 0:
|
|
508
|
+
return
|
|
411
509
|
raise CLIDeploymentException(e.args[0])
|
|
412
510
|
|
|
413
511
|
|
|
@@ -529,9 +627,24 @@ def get_connection_datafile_local(config: dict[str, Any], connection_name: str)
|
|
|
529
627
|
return "Connection not found"
|
|
530
628
|
|
|
531
629
|
|
|
532
|
-
def run_tests(
|
|
533
|
-
local_client = get_tinybird_local_client(config, test=True, silent=True)
|
|
630
|
+
def run_tests(client: TinyB, project: Project, pipe_name: Optional[str] = None) -> None:
|
|
534
631
|
try:
|
|
535
|
-
run_tests_common(name=(pipe_name,) if pipe_name else (), project=project, client=
|
|
632
|
+
run_tests_common(name=(pipe_name,) if pipe_name else (), project=project, client=client)
|
|
536
633
|
except SystemExit as e:
|
|
537
634
|
raise Exception(e.args[0])
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def get_pipe_data_test(client: TinyB, pipe_name: str, test_params: Optional[dict[str, str]] = None) -> Response:
|
|
638
|
+
pipe = client._req(f"/v0/pipes/{pipe_name}")
|
|
639
|
+
output_node = next(
|
|
640
|
+
(node for node in pipe["nodes"] if node["node_type"] != "default" and node["node_type"] != "standard"),
|
|
641
|
+
{"name": "not_found"},
|
|
642
|
+
)
|
|
643
|
+
if output_node["node_type"] == "endpoint":
|
|
644
|
+
return client._req_raw(f"/v0/pipes/{pipe_name}.ndjson?{test_params}")
|
|
645
|
+
|
|
646
|
+
params = {
|
|
647
|
+
"q": output_node["sql"],
|
|
648
|
+
"pipeline": pipe_name,
|
|
649
|
+
}
|
|
650
|
+
return client._req_raw(f"""/v0/sql?{urllib.parse.urlencode(params)}&{test_params}""")
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def detect_terminal_capabilities():
|
|
8
|
+
"""Detect terminal color and Unicode capabilities"""
|
|
9
|
+
# Check for true color support
|
|
10
|
+
colorterm = os.environ.get("COLORTERM", "").lower()
|
|
11
|
+
term = os.environ.get("TERM", "").lower()
|
|
12
|
+
term_program = os.environ.get("TERM_PROGRAM", "").lower()
|
|
13
|
+
|
|
14
|
+
# Known terminals with good true color support
|
|
15
|
+
modern_terminals = ["warp", "ghostty", "iterm2", "alacritty", "kitty", "hyper"]
|
|
16
|
+
|
|
17
|
+
# Check for true color support
|
|
18
|
+
has_truecolor = (
|
|
19
|
+
colorterm in ["truecolor", "24bit"]
|
|
20
|
+
or term_program in modern_terminals
|
|
21
|
+
or "truecolor" in term
|
|
22
|
+
or "24bit" in term
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Check if it's standard macOS Terminal
|
|
26
|
+
is_macos_terminal = term_program == "apple_terminal"
|
|
27
|
+
|
|
28
|
+
# Check for Unicode support (most modern terminals support this)
|
|
29
|
+
has_unicode = sys.stdout.encoding and "utf" in sys.stdout.encoding.lower()
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
"truecolor": has_truecolor and not is_macos_terminal,
|
|
33
|
+
"unicode": has_unicode,
|
|
34
|
+
"is_macos_terminal": is_macos_terminal,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def display_banner():
|
|
39
|
+
reset = "\033[0m"
|
|
40
|
+
capabilities = detect_terminal_capabilities()
|
|
41
|
+
|
|
42
|
+
click.echo("\n")
|
|
43
|
+
|
|
44
|
+
# Choose banner based on Unicode support
|
|
45
|
+
if capabilities["unicode"]:
|
|
46
|
+
# Unicode box-drawing characters banner
|
|
47
|
+
banner = [
|
|
48
|
+
" ████████╗██╗███╗ ██╗██╗ ██╗██████╗ ██╗██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ███████╗",
|
|
49
|
+
" ╚══██╔══╝██║████╗ ██║╚██╗ ██╔╝██╔══██╗██║██╔══██╗██╔══██╗ ██╔════╝██╔═══██╗██╔══██╗██╔════╝",
|
|
50
|
+
" ██║ ██║██╔██╗ ██║ ╚████╔╝ ██████╔╝██║██████╔╝██║ ██║ ██║ ██║ ██║██║ ██║█████╗ ",
|
|
51
|
+
" ██║ ██║██║╚██╗██║ ╚██╔╝ ██╔══██╗██║██╔══██╗██║ ██║ ██║ ██║ ██║██║ ██║██╔══╝ ",
|
|
52
|
+
" ██║ ██║██║ ╚████║ ██║ ██████╔╝██║██║ ██║██████╔╝ ╚██████╗╚██████╔╝██████╔╝███████╗",
|
|
53
|
+
" ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝",
|
|
54
|
+
]
|
|
55
|
+
else:
|
|
56
|
+
# ASCII fallback banner
|
|
57
|
+
banner = [
|
|
58
|
+
" ████████T██I███N ██N██ ██Y██████B ██I██████B ██████B ██████C ██████O ██████D ███████E",
|
|
59
|
+
" ╚══██╔══╝██║████╗ ██║╚██╗ ██╔╝██╔══██╗██║██╔══██╗██╔══██╗ ██╔════╝██╔═══██╗██╔══██╗██╔════╝",
|
|
60
|
+
" ██║ ██║██╔██╗ ██║ ╚████╔╝ ██████╔╝██║██████╔╝██║ ██║ ██║ ██║ ██║██║ ██║█████╗ ",
|
|
61
|
+
" ██║ ██║██║╚██╗██║ ╚██╔╝ ██╔══██╗██║██╔══██╗██║ ██║ ██║ ██║ ██║██║ ██║██╔══╝ ",
|
|
62
|
+
" ██║ ██║██║ ╚████║ ██║ ██████╔╝██║██║ ██║██████╔╝ ╚██████╗╚██████╔╝██████╔╝███████╗",
|
|
63
|
+
" ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
def interpolate_color(start_rgb, end_rgb, factor):
|
|
67
|
+
"""Interpolate between two RGB colors"""
|
|
68
|
+
return [int(start_rgb[i] + (end_rgb[i] - start_rgb[i]) * factor) for i in range(3)]
|
|
69
|
+
|
|
70
|
+
def rgb_to_ansi(r: int, g: int, b: int, use_truecolor: bool):
|
|
71
|
+
"""Convert RGB values to ANSI escape code"""
|
|
72
|
+
if use_truecolor:
|
|
73
|
+
return f"\033[38;2;{r};{g};{b}m"
|
|
74
|
+
else:
|
|
75
|
+
# Convert to 8-bit color (256 color palette)
|
|
76
|
+
# Simple approximation: map RGB to 216-color cube + grayscale
|
|
77
|
+
if r == g == b:
|
|
78
|
+
# Grayscale
|
|
79
|
+
gray = int(r / 255 * 23) + 232
|
|
80
|
+
return f"\033[38;5;{gray}m"
|
|
81
|
+
else:
|
|
82
|
+
# Color cube (6x6x6)
|
|
83
|
+
r_idx = int(r / 255 * 5)
|
|
84
|
+
g_idx = int(g / 255 * 5)
|
|
85
|
+
b_idx = int(b / 255 * 5)
|
|
86
|
+
color_idx = 16 + (36 * r_idx) + (6 * g_idx) + b_idx
|
|
87
|
+
return f"\033[38;5;{color_idx}m"
|
|
88
|
+
|
|
89
|
+
# Define start and end colors for smooth gradient
|
|
90
|
+
start_color = [0, 128, 128] # Deep teal
|
|
91
|
+
end_color = [100, 190, 190] # Light turquoise (balanced green and blue)
|
|
92
|
+
|
|
93
|
+
# Print each line with gradient for modern terminals, solid color for limited terminals
|
|
94
|
+
for line in banner:
|
|
95
|
+
colored_line = ""
|
|
96
|
+
|
|
97
|
+
if capabilities["truecolor"]:
|
|
98
|
+
# Use gradient for modern terminals
|
|
99
|
+
non_space_chars = sum(1 for char in line if char != " ")
|
|
100
|
+
char_count = 0
|
|
101
|
+
|
|
102
|
+
for char in line:
|
|
103
|
+
if char == " ":
|
|
104
|
+
colored_line += char
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
# Calculate smooth gradient position (0.0 to 1.0)
|
|
108
|
+
if non_space_chars > 1:
|
|
109
|
+
gradient_position = char_count / (non_space_chars - 1)
|
|
110
|
+
else:
|
|
111
|
+
gradient_position = 0
|
|
112
|
+
|
|
113
|
+
# Interpolate color
|
|
114
|
+
current_rgb = interpolate_color(start_color, end_color, gradient_position)
|
|
115
|
+
color_code = rgb_to_ansi(*current_rgb, use_truecolor=True) # type: ignore
|
|
116
|
+
|
|
117
|
+
colored_line += f"{color_code}{char}"
|
|
118
|
+
char_count += 1
|
|
119
|
+
else:
|
|
120
|
+
# Use solid color for limited terminals (like macOS Terminal)
|
|
121
|
+
solid_color = start_color # Use the deep teal consistently
|
|
122
|
+
color_code = rgb_to_ansi(*solid_color, use_truecolor=False) # type: ignore
|
|
123
|
+
|
|
124
|
+
for char in line:
|
|
125
|
+
if char == " ":
|
|
126
|
+
colored_line += char
|
|
127
|
+
else:
|
|
128
|
+
colored_line += f"{color_code}{char}"
|
|
129
|
+
|
|
130
|
+
click.echo(colored_line + reset)
|
|
131
|
+
click.echo()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from pydantic_ai import Agent, RunContext, Tool
|
|
2
|
+
from pydantic_ai.messages import ModelMessage
|
|
3
|
+
from pydantic_ai.usage import Usage
|
|
4
|
+
|
|
5
|
+
from tinybird.tb.modules.agent.animations import ThinkingAnimation
|
|
6
|
+
from tinybird.tb.modules.agent.models import create_model
|
|
7
|
+
from tinybird.tb.modules.agent.prompts import tests_files_prompt
|
|
8
|
+
from tinybird.tb.modules.agent.tools.run_command import run_command
|
|
9
|
+
from tinybird.tb.modules.agent.utils import TinybirdAgentContext
|
|
10
|
+
from tinybird.tb.modules.project import Project
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CommandAgent:
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
token: str,
|
|
17
|
+
user_token: str,
|
|
18
|
+
host: str,
|
|
19
|
+
workspace_id: str,
|
|
20
|
+
project: Project,
|
|
21
|
+
dangerously_skip_permissions: bool,
|
|
22
|
+
prompt_mode: bool,
|
|
23
|
+
thinking_animation: ThinkingAnimation,
|
|
24
|
+
):
|
|
25
|
+
self.token = token
|
|
26
|
+
self.user_token = user_token
|
|
27
|
+
self.host = host
|
|
28
|
+
self.dangerously_skip_permissions = dangerously_skip_permissions or prompt_mode
|
|
29
|
+
self.project = project
|
|
30
|
+
self.thinking_animation = thinking_animation
|
|
31
|
+
self.messages: list[ModelMessage] = []
|
|
32
|
+
self.agent = Agent(
|
|
33
|
+
model=create_model(user_token, host, workspace_id),
|
|
34
|
+
deps_type=TinybirdAgentContext,
|
|
35
|
+
instructions=[
|
|
36
|
+
"""
|
|
37
|
+
You are part of Tinybird Code, an agentic CLI that can help users to work with Tinybird.
|
|
38
|
+
You are a sub-agent of the main Tinybird Code agent. You are responsible for running commands on the user's machine.
|
|
39
|
+
You will be given a task to perform and you will use `run_command` tool to complete it.
|
|
40
|
+
If you do not find a command that can solve the task, just say that there is no command that can solve the task.
|
|
41
|
+
You can run `-h` in every level of the command to get help. E.g. `tb -h`, `tb datasource -h`, `tb datasource ls -h`.
|
|
42
|
+
When you need to access Tinybird Cloud, add the `--cloud` flag. E.g. `tb --cloud datasource ls`.
|
|
43
|
+
Token and host are not required to add to the commands.
|
|
44
|
+
Always run first help commands to be sure that the commands you are running is not interactive.
|
|
45
|
+
""",
|
|
46
|
+
],
|
|
47
|
+
tools=[
|
|
48
|
+
Tool(run_command, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
49
|
+
],
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@self.agent.instructions
|
|
53
|
+
def get_tests_files(ctx: RunContext[TinybirdAgentContext]) -> str:
|
|
54
|
+
return tests_files_prompt(self.project)
|
|
55
|
+
|
|
56
|
+
def run(self, task: str, deps: TinybirdAgentContext, usage: Usage):
|
|
57
|
+
result = self.agent.run_sync(task, deps=deps, usage=usage, message_history=self.messages)
|
|
58
|
+
self.messages.extend(result.new_messages())
|
|
59
|
+
return result
|