tinybird 0.0.1.dev262__py3-none-any.whl → 0.0.1.dev263__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tinybird might be problematic. Click here for more details.
- tinybird/datafile/common.py +151 -0
- tinybird/tb/__cli__.py +2 -2
- tinybird/tb/modules/agent/agent.py +130 -17
- tinybird/tb/modules/agent/banner.py +107 -32
- tinybird/tb/modules/agent/command_agent.py +59 -0
- tinybird/tb/modules/agent/prompts.py +91 -20
- tinybird/tb/modules/agent/testing_agent.py +62 -0
- tinybird/tb/modules/agent/tools/create_datafile.py +1 -1
- tinybird/tb/modules/agent/tools/execute_query.py +18 -0
- tinybird/tb/modules/agent/tools/run_command.py +38 -0
- tinybird/tb/modules/agent/tools/test.py +28 -0
- tinybird/tb/modules/agent/utils.py +7 -2
- tinybird/tb/modules/cli.py +8 -6
- tinybird/tb/modules/datasource.py +3 -1
- tinybird/tb/modules/test_common.py +11 -2
- {tinybird-0.0.1.dev262.dist-info → tinybird-0.0.1.dev263.dist-info}/METADATA +1 -1
- {tinybird-0.0.1.dev262.dist-info → tinybird-0.0.1.dev263.dist-info}/RECORD +20 -17
- {tinybird-0.0.1.dev262.dist-info → tinybird-0.0.1.dev263.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev262.dist-info → tinybird-0.0.1.dev263.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev262.dist-info → tinybird-0.0.1.dev263.dist-info}/top_level.txt +0 -0
tinybird/datafile/common.py
CHANGED
|
@@ -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
|
tinybird/tb/__cli__.py
CHANGED
|
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
|
|
|
4
4
|
__url__ = 'https://www.tinybird.co/docs/forward/commands'
|
|
5
5
|
__author__ = 'Tinybird'
|
|
6
6
|
__author_email__ = 'support@tinybird.co'
|
|
7
|
-
__version__ = '0.0.1.
|
|
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}""")
|
|
@@ -1,56 +1,131 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
1
4
|
import click
|
|
2
5
|
|
|
3
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
|
+
|
|
4
38
|
def display_banner():
|
|
5
39
|
reset = "\033[0m"
|
|
40
|
+
capabilities = detect_terminal_capabilities()
|
|
6
41
|
|
|
7
42
|
click.echo("\n")
|
|
8
|
-
|
|
9
|
-
banner
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
+
]
|
|
17
65
|
|
|
18
66
|
def interpolate_color(start_rgb, end_rgb, factor):
|
|
19
67
|
"""Interpolate between two RGB colors"""
|
|
20
68
|
return [int(start_rgb[i] + (end_rgb[i] - start_rgb[i]) * factor) for i in range(3)]
|
|
21
69
|
|
|
22
|
-
def rgb_to_ansi(r, g, b):
|
|
70
|
+
def rgb_to_ansi(r: int, g: int, b: int, use_truecolor: bool):
|
|
23
71
|
"""Convert RGB values to ANSI escape code"""
|
|
24
|
-
|
|
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"
|
|
25
88
|
|
|
26
89
|
# Define start and end colors for smooth gradient
|
|
27
90
|
start_color = [0, 128, 128] # Deep teal
|
|
28
|
-
end_color = [100,
|
|
91
|
+
end_color = [100, 190, 190] # Light turquoise (balanced green and blue)
|
|
29
92
|
|
|
30
|
-
# Print each line with
|
|
93
|
+
# Print each line with gradient for modern terminals, solid color for limited terminals
|
|
31
94
|
for line in banner:
|
|
32
95
|
colored_line = ""
|
|
33
|
-
# Count non-space characters for gradient calculation
|
|
34
|
-
non_space_chars = sum(1 for char in line if char != " ")
|
|
35
|
-
char_count = 0
|
|
36
|
-
|
|
37
|
-
for char in line:
|
|
38
|
-
if char == " ":
|
|
39
|
-
colored_line += char
|
|
40
|
-
continue
|
|
41
|
-
|
|
42
|
-
# Calculate smooth gradient position (0.0 to 1.0)
|
|
43
|
-
if non_space_chars > 1:
|
|
44
|
-
gradient_position = char_count / (non_space_chars - 1)
|
|
45
|
-
else:
|
|
46
|
-
gradient_position = 0
|
|
47
96
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
51
123
|
|
|
52
|
-
|
|
53
|
-
|
|
124
|
+
for char in line:
|
|
125
|
+
if char == " ":
|
|
126
|
+
colored_line += char
|
|
127
|
+
else:
|
|
128
|
+
colored_line += f"{color_code}{char}"
|
|
54
129
|
|
|
55
130
|
click.echo(colored_line + reset)
|
|
56
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
|
|
@@ -16,7 +16,6 @@ from tinybird.prompts import (
|
|
|
16
16
|
pipe_instructions,
|
|
17
17
|
s3_connection_example,
|
|
18
18
|
sink_pipe_instructions,
|
|
19
|
-
test_instructions,
|
|
20
19
|
)
|
|
21
20
|
from tinybird.tb.modules.project import Project
|
|
22
21
|
|
|
@@ -89,10 +88,7 @@ sql_instructions = """
|
|
|
89
88
|
- Use node names as table names only when nodes are present in the same file.
|
|
90
89
|
- Do not reference the current node name in the SQL.
|
|
91
90
|
- SQL queries only accept SELECT statements with conditions, aggregations, joins, etc.
|
|
92
|
-
-
|
|
93
|
-
- Use ONLY SELECT statements in the SQL section.
|
|
94
|
-
- INSERT INTO is not supported in SQL section.
|
|
95
|
-
- Do NOT query system.<table_name> tables.
|
|
91
|
+
- ONLY SELECT statements are allowed in any sql query.
|
|
96
92
|
- When using functions try always ClickHouse functions first, then SQL functions.
|
|
97
93
|
- Parameters are never quoted in any case.
|
|
98
94
|
- Use the following syntax in the SQL section for the iceberg table function: iceberg('s3://bucket/path/to/table', {{tb_secret('aws_access_key_id')}}, {{tb_secret('aws_secret_access_key')}})
|
|
@@ -115,7 +111,6 @@ datafile_instructions = """
|
|
|
115
111
|
def resources_prompt(project: Project) -> str:
|
|
116
112
|
files = project.get_project_files()
|
|
117
113
|
fixture_files = project.get_fixture_files()
|
|
118
|
-
test_files = project.get_test_files()
|
|
119
114
|
|
|
120
115
|
resources_content = "# Existing resources in the project:\n"
|
|
121
116
|
if files:
|
|
@@ -148,6 +143,29 @@ def resources_prompt(project: Project) -> str:
|
|
|
148
143
|
else:
|
|
149
144
|
fixture_content += "No fixture files found"
|
|
150
145
|
|
|
146
|
+
return resources_content + "\n" + fixture_content
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def tests_files_prompt(project: Project) -> str:
|
|
150
|
+
files = project.get_project_files()
|
|
151
|
+
test_files = project.get_test_files()
|
|
152
|
+
|
|
153
|
+
resources_content = "# Existing resources in the project:\n"
|
|
154
|
+
if files:
|
|
155
|
+
resources: list[dict[str, Any]] = []
|
|
156
|
+
for filename in files:
|
|
157
|
+
file_path = Path(filename)
|
|
158
|
+
resource = {
|
|
159
|
+
"path": str(file_path.relative_to(project.folder)),
|
|
160
|
+
"type": get_resource_type(file_path),
|
|
161
|
+
"name": file_path.stem,
|
|
162
|
+
"content": file_path.read_text(),
|
|
163
|
+
}
|
|
164
|
+
resources.append(resource)
|
|
165
|
+
resources_content = format_as_xml(resources, root_tag="resources", item_tag="resource")
|
|
166
|
+
else:
|
|
167
|
+
resources_content += "No resources found"
|
|
168
|
+
|
|
151
169
|
test_content = "# Test files in the project:\n"
|
|
152
170
|
if test_files:
|
|
153
171
|
tests: list[dict[str, Any]] = []
|
|
@@ -163,7 +181,7 @@ def resources_prompt(project: Project) -> str:
|
|
|
163
181
|
else:
|
|
164
182
|
test_content += "No test files found"
|
|
165
183
|
|
|
166
|
-
return resources_content + "\n" +
|
|
184
|
+
return resources_content + "\n" + test_content
|
|
167
185
|
|
|
168
186
|
|
|
169
187
|
def get_resource_type(path: Path) -> str:
|
|
@@ -522,14 +540,62 @@ FROM small_table
|
|
|
522
540
|
JOIN (SELECT id, col FROM huge_table WHERE id IN (SELECT id FROM small_table)) filtered
|
|
523
541
|
ON small_table.id = filtered.id
|
|
524
542
|
```
|
|
525
|
-
"""
|
|
526
543
|
|
|
544
|
+
<dev_notes>
|
|
545
|
+
IMPORTANT: DO NOT USE THE FOLLOWING WHEN QUERYING:
|
|
546
|
+
- CREATE TABLE, INSERT INTO, CREATE DATABASE, SHOW TABLES, TRUNCATE TABLE, DELETE FROM, SHOW DATASOURCES, etc. are not allowed.
|
|
547
|
+
- ONLY use SELECT statements.
|
|
548
|
+
- currentDatabase is not allowed.
|
|
549
|
+
- system tables are not allowed: system.tables, system.datasources, information_schema.tables...
|
|
550
|
+
</dev_notes>
|
|
551
|
+
"""
|
|
527
552
|
|
|
528
|
-
agent_system_prompt = f"""
|
|
529
|
-
You are a Tinybird Code, an agentic CLI that can help users to work with Tinybird.
|
|
530
553
|
|
|
531
|
-
|
|
554
|
+
test_instructions = """
|
|
555
|
+
# Working with test files:
|
|
556
|
+
- The test file name must match the name of the pipe it is testing.
|
|
557
|
+
- Every scenario name must be unique inside the test file.
|
|
558
|
+
- When looking for the parameters available, you will find them in the pipe file in the following format: {{{{String(my_param_name, default_value)}}}}.
|
|
559
|
+
- If the resource has no parameters, generate a single test with empty parameters.
|
|
560
|
+
- The format of the parameters is the following: param1=value1¶m2=value2¶m3=value3
|
|
561
|
+
- If some parameters are provided by the user and you need to use them, preserve in the same format as they were provided, like case sensitive
|
|
562
|
+
- Test as many scenarios as possible.
|
|
563
|
+
- Create tests only when the user explicitly asks for it with prompts like "Create tests for this endpoint" or "Create tests for this pipe".
|
|
564
|
+
- If the user asks for "testing an endpoint" or "call an endpoint", just request to the endpoint.
|
|
565
|
+
- The data that the tests are using is the data provided in the fixtures folder, so do not use `execute_query` or `request_endpoint` tools to analyze the data.
|
|
566
|
+
- MANDATORY: Before creating the test, analyze the fixture files that the tables of the endpoint are using so you can create relevant tests.
|
|
567
|
+
- IMPORTANT: expected_result field should always be an empty string, because it will be filled by the `create_test` tool.
|
|
568
|
+
- If the endpoint does not have parameters, you can omit parameters and generate a single test.
|
|
569
|
+
- The format of the test file is the following:
|
|
570
|
+
<test_file_format>
|
|
571
|
+
- name: kpis_single_day
|
|
572
|
+
description: Test hourly granularity for a single day
|
|
573
|
+
parameters: date_from=2024-01-01&date_to=2024-01-01
|
|
574
|
+
expected_result: ''
|
|
575
|
+
|
|
576
|
+
- name: kpis_date_range
|
|
577
|
+
description: Test daily granularity for a date range
|
|
578
|
+
parameters: date_from=2024-01-01&date_to=2024-01-31
|
|
579
|
+
expected_result: ''
|
|
580
|
+
|
|
581
|
+
- name: kpis_default_range
|
|
582
|
+
description: Test default behavior without date parameters (last 7 days)
|
|
583
|
+
parameters: ''
|
|
584
|
+
expected_result: ''
|
|
585
|
+
|
|
586
|
+
- name: kpis_fixed_time
|
|
587
|
+
description: Test with fixed timestamp for consistent testing
|
|
588
|
+
parameters: fixed_time=2024-01-15T12:00:00
|
|
589
|
+
expected_result: ''
|
|
590
|
+
|
|
591
|
+
- name: kpis_single_day
|
|
592
|
+
description: Test single day with hourly granularity
|
|
593
|
+
parameters: date_from=2024-01-01&date_to=2024-01-01
|
|
594
|
+
expected_result: ''
|
|
595
|
+
</test_file_format>
|
|
596
|
+
"""
|
|
532
597
|
|
|
598
|
+
tone_and_style_instructions = """
|
|
533
599
|
# Tone and style
|
|
534
600
|
You should be concise, direct, and to the point. Maintain a professional tone. Do not use emojis.
|
|
535
601
|
Remember that your output will be displayed on a command line interface. Your responses can use Github-flavored markdown for formatting.
|
|
@@ -548,6 +614,15 @@ Do not add additional code explanation summary unless requested by the user. Aft
|
|
|
548
614
|
|
|
549
615
|
# Code style
|
|
550
616
|
IMPORTANT: DO NOT ADD ANY COMMENTS unless asked by the user.
|
|
617
|
+
"""
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
agent_system_prompt = f"""
|
|
621
|
+
You are a Tinybird Code, an agentic CLI that can help users to work with Tinybird.
|
|
622
|
+
|
|
623
|
+
You are an interactive CLI tool that helps users with data engineering tasks. Use the instructions below and the tools available to you to assist the user.
|
|
624
|
+
|
|
625
|
+
{tone_and_style_instructions}
|
|
551
626
|
|
|
552
627
|
# Tools
|
|
553
628
|
You have access to the following tools:
|
|
@@ -662,15 +737,11 @@ GCS: {gcs_connection_example}
|
|
|
662
737
|
- `DateTime` parameters accept values in format `YYYY-MM-DD HH:MM:SS`
|
|
663
738
|
- `Date` parameters accept values in format `YYYYMMDD`
|
|
664
739
|
|
|
665
|
-
# Working with
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
- If
|
|
670
|
-
- The data that the tests are using is the data provided in the fixtures folder.
|
|
671
|
-
- Querying data or requesting endpoints won't return the data that the tests are using.
|
|
672
|
-
- MANDATORY: Before creating the test, analyze the fixture files that the tables of the endpoint are using so you can create relevant tests.
|
|
673
|
-
</dev_notes>
|
|
740
|
+
# Working with test files:
|
|
741
|
+
- Use `manage_tests` tool to create, update or run tests.
|
|
742
|
+
|
|
743
|
+
# Working with commands:
|
|
744
|
+
- If you dont have a tool that can solve the task, use `run_command` tool to check if the task can be solved with a normal tinybird cli command.
|
|
674
745
|
|
|
675
746
|
# Info
|
|
676
747
|
Today is {datetime.now().strftime("%Y-%m-%d")}
|
|
@@ -0,0 +1,62 @@
|
|
|
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 test_instructions, tests_files_prompt, tone_and_style_instructions
|
|
8
|
+
from tinybird.tb.modules.agent.tools.test import create_tests as create_tests_tool
|
|
9
|
+
from tinybird.tb.modules.agent.tools.test import run_tests as run_tests_tool
|
|
10
|
+
from tinybird.tb.modules.agent.utils import TinybirdAgentContext
|
|
11
|
+
from tinybird.tb.modules.project import Project
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestingAgent:
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
token: str,
|
|
18
|
+
user_token: str,
|
|
19
|
+
host: str,
|
|
20
|
+
workspace_id: str,
|
|
21
|
+
project: Project,
|
|
22
|
+
dangerously_skip_permissions: bool,
|
|
23
|
+
prompt_mode: bool,
|
|
24
|
+
thinking_animation: ThinkingAnimation,
|
|
25
|
+
):
|
|
26
|
+
self.token = token
|
|
27
|
+
self.user_token = user_token
|
|
28
|
+
self.host = host
|
|
29
|
+
self.dangerously_skip_permissions = dangerously_skip_permissions or prompt_mode
|
|
30
|
+
self.project = project
|
|
31
|
+
self.thinking_animation = thinking_animation
|
|
32
|
+
self.messages: list[ModelMessage] = []
|
|
33
|
+
self.agent = Agent(
|
|
34
|
+
model=create_model(user_token, host, workspace_id),
|
|
35
|
+
deps_type=TinybirdAgentContext,
|
|
36
|
+
instructions=[
|
|
37
|
+
"""
|
|
38
|
+
You are part of Tinybird Code, an agentic CLI that can help users to work with Tinybird.
|
|
39
|
+
You are a sub-agent of the main Tinybird Code agent. You are responsible for managing test files.
|
|
40
|
+
You can do the following:
|
|
41
|
+
- Create new test files.
|
|
42
|
+
- Update existing test files.
|
|
43
|
+
- Run tests.
|
|
44
|
+
""",
|
|
45
|
+
tone_and_style_instructions,
|
|
46
|
+
test_instructions,
|
|
47
|
+
],
|
|
48
|
+
tools=[
|
|
49
|
+
Tool(create_tests_tool, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
50
|
+
Tool(run_tests_tool, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
51
|
+
],
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
@self.agent.instructions
|
|
55
|
+
def get_tests_files(ctx: RunContext[TinybirdAgentContext]) -> str:
|
|
56
|
+
return tests_files_prompt(self.project)
|
|
57
|
+
|
|
58
|
+
def run(self, task: str, deps: TinybirdAgentContext, usage: Usage):
|
|
59
|
+
result = self.agent.run_sync(task, deps=deps, usage=usage, message_history=self.messages)
|
|
60
|
+
new_messages = result.new_messages()
|
|
61
|
+
self.messages.extend(new_messages)
|
|
62
|
+
return result
|
|
@@ -56,7 +56,7 @@ def create_datafile(ctx: RunContext[TinybirdAgentContext], resource: Datafile) -
|
|
|
56
56
|
action_text = "created" if not exists else "updated"
|
|
57
57
|
click.echo(FeedbackManager.success(message=f"✓ {resource.pathname} {action_text}"))
|
|
58
58
|
ctx.deps.thinking_animation.start()
|
|
59
|
-
return f"{action_text} {resource.pathname}"
|
|
59
|
+
return f"{action_text} {resource.pathname}. Project built successfully."
|
|
60
60
|
except AgentRunCancelled as e:
|
|
61
61
|
raise e
|
|
62
62
|
except CLIBuildException as e:
|
|
@@ -6,6 +6,20 @@ from tinybird.tb.modules.agent.utils import TinybirdAgentContext
|
|
|
6
6
|
from tinybird.tb.modules.common import echo_safe_humanfriendly_tables_format_pretty_table
|
|
7
7
|
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
8
8
|
|
|
9
|
+
forbidden_commands = [
|
|
10
|
+
"currentDatabase()",
|
|
11
|
+
"create table",
|
|
12
|
+
"insert into",
|
|
13
|
+
"create database",
|
|
14
|
+
"show tables",
|
|
15
|
+
"show datasources",
|
|
16
|
+
"truncate table",
|
|
17
|
+
"delete from",
|
|
18
|
+
"system.tables",
|
|
19
|
+
"system.datasources",
|
|
20
|
+
"information_schema.tables",
|
|
21
|
+
]
|
|
22
|
+
|
|
9
23
|
|
|
10
24
|
def execute_query(ctx: RunContext[TinybirdAgentContext], query: str, task: str, cloud: bool = False):
|
|
11
25
|
"""Execute a query:
|
|
@@ -19,6 +33,10 @@ def execute_query(ctx: RunContext[TinybirdAgentContext], query: str, task: str,
|
|
|
19
33
|
str: The result of the query.
|
|
20
34
|
"""
|
|
21
35
|
try:
|
|
36
|
+
for forbidden_command in forbidden_commands:
|
|
37
|
+
if forbidden_command in query.lower():
|
|
38
|
+
return f"Error executing query: {forbidden_command} is not allowed."
|
|
39
|
+
|
|
22
40
|
cloud_or_local = "cloud" if cloud else "local"
|
|
23
41
|
ctx.deps.thinking_animation.stop()
|
|
24
42
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from pydantic_ai import RunContext
|
|
5
|
+
|
|
6
|
+
from tinybird.tb.modules.agent.utils import AgentRunCancelled, TinybirdAgentContext, show_confirmation, show_input
|
|
7
|
+
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run_command(ctx: RunContext[TinybirdAgentContext], command: str):
|
|
11
|
+
"""Run a tinybird CLI command with the given arguments
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
command (str): The command to run. Required. Examples: `tb --local sql "select 1"`, `tb --cloud datasource ls`, `tb --help`
|
|
15
|
+
"""
|
|
16
|
+
try:
|
|
17
|
+
ctx.deps.thinking_animation.stop()
|
|
18
|
+
confirmation = show_confirmation(
|
|
19
|
+
title=f"Run command: {command}?", skip_confirmation=ctx.deps.dangerously_skip_permissions
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if confirmation == "review":
|
|
23
|
+
feedback = show_input(ctx.deps.workspace_name)
|
|
24
|
+
ctx.deps.thinking_animation.start()
|
|
25
|
+
return f"User did not confirm the command and gave the following feedback: {feedback}"
|
|
26
|
+
|
|
27
|
+
click.echo(FeedbackManager.highlight(message=f"» Running command: {command}"))
|
|
28
|
+
command = command.replace("tb", "tb --no-version-warning")
|
|
29
|
+
result = subprocess.run(command, shell=True, capture_output=True, text=True)
|
|
30
|
+
click.echo(result.stdout)
|
|
31
|
+
ctx.deps.thinking_animation.start()
|
|
32
|
+
return result.stdout
|
|
33
|
+
except AgentRunCancelled as e:
|
|
34
|
+
raise e
|
|
35
|
+
except Exception as e:
|
|
36
|
+
click.echo(FeedbackManager.error(message=f"Error running command: {e}"))
|
|
37
|
+
ctx.deps.thinking_animation.start()
|
|
38
|
+
return f"Error running command: {e}"
|
|
@@ -11,7 +11,9 @@ from tinybird.tb.modules.agent.utils import (
|
|
|
11
11
|
show_confirmation,
|
|
12
12
|
show_input,
|
|
13
13
|
)
|
|
14
|
+
from tinybird.tb.modules.exceptions import CLIBuildException
|
|
14
15
|
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
16
|
+
from tinybird.tb.modules.test_common import dump_tests, parse_tests
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
def create_tests(ctx: RunContext[TinybirdAgentContext], pipe_name: str, test_content: str) -> str:
|
|
@@ -27,11 +29,32 @@ def create_tests(ctx: RunContext[TinybirdAgentContext], pipe_name: str, test_con
|
|
|
27
29
|
running_tests = False
|
|
28
30
|
try:
|
|
29
31
|
ctx.deps.thinking_animation.stop()
|
|
32
|
+
ctx.deps.build_project_test(silent=True)
|
|
30
33
|
path = Path(ctx.deps.folder) / "tests" / f"{pipe_name}.yaml"
|
|
31
34
|
current_test_content: Optional[str] = None
|
|
32
35
|
if path.exists():
|
|
33
36
|
current_test_content = path.read_text()
|
|
34
37
|
|
|
38
|
+
pipe_tests_content = parse_tests(test_content)
|
|
39
|
+
for test in pipe_tests_content:
|
|
40
|
+
test_params = test["parameters"].split("?")[1] if "?" in test["parameters"] else test["parameters"]
|
|
41
|
+
response = None
|
|
42
|
+
try:
|
|
43
|
+
response = ctx.deps.get_pipe_data_test(pipe_name=pipe_name, test_params=test_params)
|
|
44
|
+
except Exception:
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
if response.status_code >= 400:
|
|
48
|
+
test["expected_http_status"] = response.status_code
|
|
49
|
+
test["expected_result"] = response.json()["error"]
|
|
50
|
+
else:
|
|
51
|
+
if "expected_http_status" in test:
|
|
52
|
+
del test["expected_http_status"]
|
|
53
|
+
|
|
54
|
+
test["expected_result"] = response.text or ""
|
|
55
|
+
|
|
56
|
+
test_content = dump_tests(pipe_tests_content)
|
|
57
|
+
|
|
35
58
|
if current_test_content:
|
|
36
59
|
content = create_terminal_box(current_test_content, new_content=test_content, title=path.name)
|
|
37
60
|
else:
|
|
@@ -107,6 +130,11 @@ def run_tests(ctx: RunContext[TinybirdAgentContext], pipe_name: Optional[str] =
|
|
|
107
130
|
return f"All tests in the project ran successfully\n{test_output}"
|
|
108
131
|
except AgentRunCancelled as e:
|
|
109
132
|
raise e
|
|
133
|
+
except CLIBuildException as e:
|
|
134
|
+
ctx.deps.thinking_animation.stop()
|
|
135
|
+
click.echo(FeedbackManager.error(message=e))
|
|
136
|
+
ctx.deps.thinking_animation.start()
|
|
137
|
+
return f"Error building project: {e}"
|
|
110
138
|
except Exception as e:
|
|
111
139
|
error_message = str(e)
|
|
112
140
|
test_exit_code = "test_error__error__"
|
|
@@ -20,6 +20,7 @@ from prompt_toolkit.shortcuts import PromptSession
|
|
|
20
20
|
from prompt_toolkit.styles import Style as PromptStyle
|
|
21
21
|
from pydantic import BaseModel, Field
|
|
22
22
|
from pydantic_ai import RunContext
|
|
23
|
+
from requests import Response
|
|
23
24
|
|
|
24
25
|
from tinybird.tb.modules.agent.memory import load_history
|
|
25
26
|
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
@@ -40,6 +41,7 @@ class TinybirdAgentContext(BaseModel):
|
|
|
40
41
|
get_project_files: Callable[[], List[str]]
|
|
41
42
|
explore_data: Callable[[str], str]
|
|
42
43
|
build_project: Callable[..., None]
|
|
44
|
+
build_project_test: Callable[..., None]
|
|
43
45
|
deploy_project: Callable[[], None]
|
|
44
46
|
deploy_check_project: Callable[[], None]
|
|
45
47
|
mock_data: Callable[..., list[dict[str, Any]]]
|
|
@@ -50,6 +52,7 @@ class TinybirdAgentContext(BaseModel):
|
|
|
50
52
|
execute_query_local: Callable[..., dict[str, Any]]
|
|
51
53
|
request_endpoint_cloud: Callable[..., dict[str, Any]]
|
|
52
54
|
request_endpoint_local: Callable[..., dict[str, Any]]
|
|
55
|
+
get_pipe_data_test: Callable[..., Response]
|
|
53
56
|
get_datasource_datafile_cloud: Callable[..., str]
|
|
54
57
|
get_datasource_datafile_local: Callable[..., str]
|
|
55
58
|
get_pipe_datafile_cloud: Callable[..., str]
|
|
@@ -714,13 +717,15 @@ class AgentRunCancelled(Exception):
|
|
|
714
717
|
pass
|
|
715
718
|
|
|
716
719
|
|
|
717
|
-
def show_confirmation(title: str, skip_confirmation: bool = False) -> ConfirmationResult:
|
|
720
|
+
def show_confirmation(title: str, skip_confirmation: bool = False, show_review: bool = True) -> ConfirmationResult:
|
|
718
721
|
if skip_confirmation:
|
|
719
722
|
return "yes"
|
|
720
723
|
|
|
721
724
|
while True:
|
|
722
725
|
result = show_options(
|
|
723
|
-
options=["Yes, continue", "No, tell Tinybird Code what to do", "Cancel"]
|
|
726
|
+
options=["Yes, continue", "No, tell Tinybird Code what to do", "Cancel"]
|
|
727
|
+
if show_review
|
|
728
|
+
else ["Yes, continue", "Cancel"],
|
|
724
729
|
title=title,
|
|
725
730
|
)
|
|
726
731
|
|
tinybird/tb/modules/cli.py
CHANGED
|
@@ -399,21 +399,23 @@ def create_ctx_client(ctx: Context, config: Dict[str, Any], cloud: bool, staging
|
|
|
399
399
|
command_always_test = ["test"]
|
|
400
400
|
|
|
401
401
|
if (
|
|
402
|
-
|
|
403
|
-
and (cloud or command in commands_always_cloud)
|
|
402
|
+
(cloud or command in commands_always_cloud)
|
|
404
403
|
and command not in commands_always_local
|
|
405
404
|
and command not in command_always_test
|
|
406
405
|
):
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
406
|
+
if show_warnings:
|
|
407
|
+
click.echo(
|
|
408
|
+
FeedbackManager.gray(
|
|
409
|
+
message=f"Running against Tinybird Cloud: Workspace {config.get('name', 'default')}"
|
|
410
|
+
)
|
|
411
|
+
)
|
|
410
412
|
|
|
411
413
|
method = None
|
|
412
414
|
if ctx.params.get("token"):
|
|
413
415
|
method = "token via --token option"
|
|
414
416
|
elif os.environ.get("TB_TOKEN"):
|
|
415
417
|
method = "token from TB_TOKEN environment variable"
|
|
416
|
-
if method:
|
|
418
|
+
if method and show_warnings:
|
|
417
419
|
click.echo(FeedbackManager.gray(message=f"Authentication method: {method}"))
|
|
418
420
|
|
|
419
421
|
return _get_tb_client(config.get("token", ""), config["host"], staging=staging)
|
|
@@ -95,7 +95,7 @@ def datasource_ls(ctx: Context, match: Optional[str], format_: str):
|
|
|
95
95
|
shared_from,
|
|
96
96
|
name,
|
|
97
97
|
humanfriendly.format_number(stats.get("row_count")) if stats.get("row_count", None) else "-",
|
|
98
|
-
humanfriendly.format_size(int(stats.get("bytes"))) if stats.get("bytes", None) else "-",
|
|
98
|
+
humanfriendly.format_size(int(stats.get("bytes", 0))) if stats.get("bytes", None) else "-",
|
|
99
99
|
t["created_at"][:-7],
|
|
100
100
|
t["updated_at"][:-7],
|
|
101
101
|
t.get("service", ""),
|
|
@@ -455,6 +455,8 @@ def datasource_truncate(ctx, datasource_name, yes, cascade):
|
|
|
455
455
|
except Exception as e:
|
|
456
456
|
raise CLIDatasourceException(FeedbackManager.error_exception(error=e))
|
|
457
457
|
click.echo(FeedbackManager.success_truncate_datasource(datasource=cascade_ds))
|
|
458
|
+
else:
|
|
459
|
+
click.echo(FeedbackManager.info(message="Operation cancelled by user"))
|
|
458
460
|
|
|
459
461
|
|
|
460
462
|
@datasource.command(name="delete")
|
|
@@ -133,6 +133,15 @@ def create_test(
|
|
|
133
133
|
return tests
|
|
134
134
|
|
|
135
135
|
|
|
136
|
+
def parse_tests(tests_content: str) -> List[Dict[str, Any]]:
|
|
137
|
+
return yaml.safe_load(tests_content)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def dump_tests(tests: List[Dict[str, Any]]) -> str:
|
|
141
|
+
yaml_str = yaml.safe_dump(tests, sort_keys=False)
|
|
142
|
+
return yaml_str.replace("- name:", "\n- name:")
|
|
143
|
+
|
|
144
|
+
|
|
136
145
|
def update_test(pipe: str, project: Project, client: TinyB) -> None:
|
|
137
146
|
try:
|
|
138
147
|
folder = project.folder
|
|
@@ -149,7 +158,7 @@ def update_test(pipe: str, project: Project, client: TinyB) -> None:
|
|
|
149
158
|
|
|
150
159
|
click.echo(FeedbackManager.highlight(message=f"\n» Updating tests expectations for {pipe_name} endpoint..."))
|
|
151
160
|
pipe_tests_path = Path(project.folder) / pipe_tests_path
|
|
152
|
-
pipe_tests_content =
|
|
161
|
+
pipe_tests_content = parse_tests(pipe_tests_path.read_text())
|
|
153
162
|
for test in pipe_tests_content:
|
|
154
163
|
test_params = test["parameters"].split("?")[1] if "?" in test["parameters"] else test["parameters"]
|
|
155
164
|
response = None
|
|
@@ -197,7 +206,7 @@ def run_tests(name: Tuple[str, ...], project: Project, client: TinyB) -> None:
|
|
|
197
206
|
def run_test(test_file) -> Optional[str]:
|
|
198
207
|
test_file_path = Path(test_file)
|
|
199
208
|
click.echo(FeedbackManager.info(message=f"* {test_file_path.stem}{test_file_path.suffix}"))
|
|
200
|
-
test_file_content =
|
|
209
|
+
test_file_content = parse_tests(test_file_path.read_text())
|
|
201
210
|
test_file_errors = ""
|
|
202
211
|
for test in test_file_content:
|
|
203
212
|
try:
|
|
@@ -12,12 +12,12 @@ tinybird/syncasync.py,sha256=IPnOx6lMbf9SNddN1eBtssg8vCLHMt76SuZ6YNYm-Yk,27761
|
|
|
12
12
|
tinybird/tornado_template.py,sha256=jjNVDMnkYFWXflmT8KU_Ssbo5vR8KQq3EJMk5vYgXRw,41959
|
|
13
13
|
tinybird/ch_utils/constants.py,sha256=fPgZtwbr1ymxaW7uqVWHKmAbt7uGj3SxCCS3xsEMJqA,4151
|
|
14
14
|
tinybird/ch_utils/engine.py,sha256=4X1B-iuhdW_mxKnX_m3iCsxgP9RPVgR75g7yH1vsJ6A,40851
|
|
15
|
-
tinybird/datafile/common.py,sha256=
|
|
15
|
+
tinybird/datafile/common.py,sha256=lN-fNCaxtFWQAd0WN5Q-mgljM7F8sYOBQcU1JJogw1g,105324
|
|
16
16
|
tinybird/datafile/exceptions.py,sha256=8rw2umdZjtby85QbuRKFO5ETz_eRHwUY5l7eHsy1wnI,556
|
|
17
17
|
tinybird/datafile/parse_connection.py,sha256=tRyn2Rpr1TeWet5BXmMoQgaotbGdYep1qiTak_OqC5E,1825
|
|
18
18
|
tinybird/datafile/parse_datasource.py,sha256=ssW8QeFSgglVFi3sDZj_HgkJiTJ2069v2JgqnH3CkDE,1825
|
|
19
19
|
tinybird/datafile/parse_pipe.py,sha256=xf4m0Tw44QWJzHzAm7Z7FwUoUUtr7noMYjU1NiWnX0k,3880
|
|
20
|
-
tinybird/tb/__cli__.py,sha256=
|
|
20
|
+
tinybird/tb/__cli__.py,sha256=TzjK12wvsWtuLOWcJjdir-py6u6IeFKVGg35GyNA3z4,247
|
|
21
21
|
tinybird/tb/check_pypi.py,sha256=Gp0HkHHDFMSDL6nxKlOY51z7z1Uv-2LRexNTZSHHGmM,552
|
|
22
22
|
tinybird/tb/cli.py,sha256=FdDFEIayjmsZEVsVSSvRiVYn_FHOVg_zWQzchnzfWho,1008
|
|
23
23
|
tinybird/tb/client.py,sha256=pJbdkWMXGAqKseNAvdsRRnl_c7I-DCMB0dWCQnG82nU,54146
|
|
@@ -25,13 +25,13 @@ tinybird/tb/config.py,sha256=mhMTGnMB5KcxGoh3dewIr2Jjsa6pHE183gCPAQWyp6o,3973
|
|
|
25
25
|
tinybird/tb/modules/build.py,sha256=efD-vamK1NPaDo9R86Hn8be2DYoW0Hh5bZiH7knK5dk,7790
|
|
26
26
|
tinybird/tb/modules/build_common.py,sha256=dthlaDn_CuwZnedQcUi7iIdDoHWfSbbbGQwiDgNcmC0,13062
|
|
27
27
|
tinybird/tb/modules/cicd.py,sha256=0KLKccha9IP749QvlXBmzdWv1On3mFwMY4DUcJlBxiE,7326
|
|
28
|
-
tinybird/tb/modules/cli.py,sha256=
|
|
28
|
+
tinybird/tb/modules/cli.py,sha256=qZIp8Ez4sunQ3_lIj1QGc3yOp8-SYfNHASHpE_7Ji6Y,16978
|
|
29
29
|
tinybird/tb/modules/common.py,sha256=tj6DR2yOqMMQ0PILwFGXmMogxdrbQCgj36HdSM611rs,82657
|
|
30
30
|
tinybird/tb/modules/config.py,sha256=gK7rgaWTDd4ZKCrNEg_Uemr26EQjqWt6TjyQKujxOws,11462
|
|
31
31
|
tinybird/tb/modules/connection.py,sha256=-MY56NUAai6EMC4-wpi7bT0_nz_SA8QzTmHkV7HB1IQ,17810
|
|
32
32
|
tinybird/tb/modules/copy.py,sha256=dPZkcIDvxjJrlQUIvToO0vsEEEs4EYumbNV77-BzNoU,4404
|
|
33
33
|
tinybird/tb/modules/create.py,sha256=pJxHXG69c9Z_21s-7VuJ3RZOF_nJU51LEwiAkvI3dZY,23251
|
|
34
|
-
tinybird/tb/modules/datasource.py,sha256=
|
|
34
|
+
tinybird/tb/modules/datasource.py,sha256=pae-ENeHYIF1HHYRSOziFC-2FPLUFa0KS60YpdlKCS8,41725
|
|
35
35
|
tinybird/tb/modules/deployment.py,sha256=EDEVOqFk5fp0fvvs2jV0mT5POFd-i_8uZIUREwzAbEk,13199
|
|
36
36
|
tinybird/tb/modules/deployment_common.py,sha256=CswUMfp5dW0NgiuP_ZGORqvqooHFYU2ZJaxOcpRFDT0,17866
|
|
37
37
|
tinybird/tb/modules/deprecations.py,sha256=rrszC1f_JJeJ8mUxGoCxckQTJFBCR8wREf4XXXN-PRc,4507
|
|
@@ -63,28 +63,30 @@ tinybird/tb/modules/sink.py,sha256=dK2s__my0ePIUYrqBzhPSgdWN9rbpvP1G4dT7DJzz80,3
|
|
|
63
63
|
tinybird/tb/modules/table.py,sha256=4XrtjM-N0zfNtxVkbvLDQQazno1EPXnxTyo7llivfXk,11035
|
|
64
64
|
tinybird/tb/modules/telemetry.py,sha256=T9gtsQffWqG_4hRBaUJPzOfMkPwz7mH-R6Bn1XRYViA,11482
|
|
65
65
|
tinybird/tb/modules/test.py,sha256=O2-mS4uMU6nPi7yWPpWzshAgOlYKiGS-tkM12pXQGMI,1906
|
|
66
|
-
tinybird/tb/modules/test_common.py,sha256=
|
|
66
|
+
tinybird/tb/modules/test_common.py,sha256=YZwAdSfYVXdvArfTc9tH-2QBOhb_XbnJ3eKvyXTJuEM,12717
|
|
67
67
|
tinybird/tb/modules/token.py,sha256=DkXW9FNCLGBisXewfk195jTJ6B1Iz7zq3cEEac48aAs,12731
|
|
68
68
|
tinybird/tb/modules/watch.py,sha256=No0bK1M1_3CYuMaIgylxf7vYFJ72lTJe3brz6xQ-mJo,8819
|
|
69
69
|
tinybird/tb/modules/workspace.py,sha256=Q_8HcxMsNg8QG9aBlwcWS2umrDP5IkTIHqqz3sfmGuc,11341
|
|
70
70
|
tinybird/tb/modules/workspace_members.py,sha256=5JdkJgfuEwbq-t6vxkBhYwgsiTDxF790wsa6Xfif9nk,8608
|
|
71
71
|
tinybird/tb/modules/agent/__init__.py,sha256=i3oe3vDIWWPaicdCM0zs7D7BJ1W0k7th93ooskHAV00,54
|
|
72
|
-
tinybird/tb/modules/agent/agent.py,sha256=
|
|
72
|
+
tinybird/tb/modules/agent/agent.py,sha256=cHpPZiUeW525i9PxspGoilTc-1HPAHCCzxiYdcdJQ7c,28214
|
|
73
73
|
tinybird/tb/modules/agent/animations.py,sha256=4WOC5_2BracttmMCrV0H91tXfWcUzQHBUaIJc5FA7tE,3490
|
|
74
|
-
tinybird/tb/modules/agent/banner.py,sha256=
|
|
74
|
+
tinybird/tb/modules/agent/banner.py,sha256=2UpuuIqWxS0Ltab6i_FE4dkNxlJCdgKGCMtbwQGiLA8,7185
|
|
75
|
+
tinybird/tb/modules/agent/command_agent.py,sha256=Q4M5qV9j3aETrXswoWZ02ZWQZhJm42HGHO1ymJrfDnw,2665
|
|
75
76
|
tinybird/tb/modules/agent/memory.py,sha256=O6Kumn9AyKxcTkhI45yjAUZ3ZIAibLOcNWoiEuLYeqY,3245
|
|
76
77
|
tinybird/tb/modules/agent/models.py,sha256=LW1D27gjcd_jwFmghEzteCgToDfodX2B6B5S8BYbysw,735
|
|
77
|
-
tinybird/tb/modules/agent/prompts.py,sha256=
|
|
78
|
-
tinybird/tb/modules/agent/
|
|
78
|
+
tinybird/tb/modules/agent/prompts.py,sha256=NEIk3OK0xXm3QS1Iox4T1MMo19hLhfaICMEwzqVGW7E,31069
|
|
79
|
+
tinybird/tb/modules/agent/testing_agent.py,sha256=mH7ccuWeTnkpENCmwym9ubDFncMXQoSo0jq2pRxJVDY,2553
|
|
80
|
+
tinybird/tb/modules/agent/utils.py,sha256=VYuoodb05Ucq0b3qgIAf2KhOTTRD85_Pfab0xKwbg40,29610
|
|
79
81
|
tinybird/tb/modules/agent/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
80
82
|
tinybird/tb/modules/agent/tools/analyze.py,sha256=CR5LXg4fou-zYEksqnjpJ0icvxJVoKnTctoI1NRvqCM,3873
|
|
81
83
|
tinybird/tb/modules/agent/tools/append.py,sha256=XA8ZeqxZcRL_0ZCd5FyggpWeH53mwTMby4lHV8wQa7c,6039
|
|
82
84
|
tinybird/tb/modules/agent/tools/build.py,sha256=Hm-xDAP9ckMiKquT-DmDg5H0yxZefLOaWKANyoVSaEQ,846
|
|
83
|
-
tinybird/tb/modules/agent/tools/create_datafile.py,sha256
|
|
85
|
+
tinybird/tb/modules/agent/tools/create_datafile.py,sha256=M2DKTSDnAMs22oSVfBagjXQOTJejf5Kp-RHd15LDKqQ,6931
|
|
84
86
|
tinybird/tb/modules/agent/tools/deploy.py,sha256=2hKj6LMYiDea6ventsBjE6ArECGIryTdo3X-LYo5oZI,1248
|
|
85
87
|
tinybird/tb/modules/agent/tools/deploy_check.py,sha256=2Wr9hQfKPlhqhumOv5TNl_xFctvdq_DHZ2dI2h_LggY,1048
|
|
86
88
|
tinybird/tb/modules/agent/tools/diff_resource.py,sha256=_9xHcDzCTKk_E1wKQbuktVqV6U9sA0kqYaBxWvtliX0,2613
|
|
87
|
-
tinybird/tb/modules/agent/tools/execute_query.py,sha256=
|
|
89
|
+
tinybird/tb/modules/agent/tools/execute_query.py,sha256=egUhKqRDF9C9458-rtu-yr-OeyvS0UrEUQwUgGd0Gts,3170
|
|
88
90
|
tinybird/tb/modules/agent/tools/explore.py,sha256=ihALc_kBcsjrKT3hZyicqyIowB0g_K3AtNNi-5uz9-8,412
|
|
89
91
|
tinybird/tb/modules/agent/tools/get_endpoint_stats.py,sha256=LiEK6ToyPDW2aI8ijclzuwdYAcFmwH-TyjqdFEzQWAc,1689
|
|
90
92
|
tinybird/tb/modules/agent/tools/get_openapi_definition.py,sha256=mjIVVXtgvTs5LzOR8Bp4jB1XhLVMysHrHXawkErFdt8,2282
|
|
@@ -92,7 +94,8 @@ tinybird/tb/modules/agent/tools/mock.py,sha256=4JJ_45uWkbDhTGeKpwDv8L07ewsbF8u7q
|
|
|
92
94
|
tinybird/tb/modules/agent/tools/plan.py,sha256=2KHLNkr2f1RfkbAR4mCVsv94LGosXd8-ky7v6BB1OtQ,985
|
|
93
95
|
tinybird/tb/modules/agent/tools/preview_datafile.py,sha256=Gbao_FxhXstnUnngVQxztpizjugyfx1rOXTkw7Yabls,858
|
|
94
96
|
tinybird/tb/modules/agent/tools/request_endpoint.py,sha256=xNYIEFI9sfq3SjevYqJaHH1EP6uEX5HHrbKS6qo3Imo,2852
|
|
95
|
-
tinybird/tb/modules/agent/tools/
|
|
97
|
+
tinybird/tb/modules/agent/tools/run_command.py,sha256=321w5dYOnapMf-TFIkgizErllwB5Poc0RYOjBAQeMPc,1576
|
|
98
|
+
tinybird/tb/modules/agent/tools/test.py,sha256=CbGak_coopCTtqHoPWy-BwgLMIyEyeO34NTNkv18au4,6041
|
|
96
99
|
tinybird/tb/modules/datafile/build.py,sha256=NFKBrusFLU0WJNCXePAFWiEDuTaXpwc0lHlOQWEJ43s,51117
|
|
97
100
|
tinybird/tb/modules/datafile/build_common.py,sha256=2yNdxe49IMA9wNvl25NemY2Iaz8L66snjOdT64dm1is,4511
|
|
98
101
|
tinybird/tb/modules/datafile/build_datasource.py,sha256=Ra8pVQBDafbFRUKlhpgohhTsRyp_ADKZJVG8Gd69idY,17227
|
|
@@ -113,8 +116,8 @@ tinybird/tb_cli_modules/config.py,sha256=IsgdtFRnUrkY8-Zo32lmk6O7u3bHie1QCxLwgp4
|
|
|
113
116
|
tinybird/tb_cli_modules/exceptions.py,sha256=pmucP4kTF4irIt7dXiG-FcnI-o3mvDusPmch1L8RCWk,3367
|
|
114
117
|
tinybird/tb_cli_modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
|
|
115
118
|
tinybird/tb_cli_modules/telemetry.py,sha256=Hh2Io8ZPROSunbOLuMvuIFU4TqwWPmQTqal4WS09K1A,10449
|
|
116
|
-
tinybird-0.0.1.
|
|
117
|
-
tinybird-0.0.1.
|
|
118
|
-
tinybird-0.0.1.
|
|
119
|
-
tinybird-0.0.1.
|
|
120
|
-
tinybird-0.0.1.
|
|
119
|
+
tinybird-0.0.1.dev263.dist-info/METADATA,sha256=CPGH7Rh1ytBwX3FCTvtbnd4WFYcslMn-MVrbdOqAEF8,1733
|
|
120
|
+
tinybird-0.0.1.dev263.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
121
|
+
tinybird-0.0.1.dev263.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
|
|
122
|
+
tinybird-0.0.1.dev263.dist-info/top_level.txt,sha256=VqqqEmkAy7UNaD8-V51FCoMMWXjLUlR0IstvK7tJYVY,54
|
|
123
|
+
tinybird-0.0.1.dev263.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|