tinybird 0.0.1.dev261__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/sql_template.py +8 -1
- 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/deployment.py +32 -5
- tinybird/tb/modules/deployment_common.py +5 -1
- tinybird/tb/modules/test_common.py +11 -2
- {tinybird-0.0.1.dev261.dist-info → tinybird-0.0.1.dev263.dist-info}/METADATA +1 -1
- {tinybird-0.0.1.dev261.dist-info → tinybird-0.0.1.dev263.dist-info}/RECORD +23 -20
- {tinybird-0.0.1.dev261.dist-info → tinybird-0.0.1.dev263.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev261.dist-info → tinybird-0.0.1.dev263.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev261.dist-info → tinybird-0.0.1.dev263.dist-info}/top_level.txt +0 -0
|
@@ -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")
|
|
@@ -188,14 +188,27 @@ def deployment_group() -> None:
|
|
|
188
188
|
default=None,
|
|
189
189
|
help="URL of the template to use for the deployment. Example: https://github.com/tinybirdco/web-analytics-starter-kit/tree/main/tinybird",
|
|
190
190
|
)
|
|
191
|
+
@click.option(
|
|
192
|
+
"-v",
|
|
193
|
+
"--verbose",
|
|
194
|
+
is_flag=True,
|
|
195
|
+
default=False,
|
|
196
|
+
help="Show verbose output. Disabled by default.",
|
|
197
|
+
)
|
|
191
198
|
@click.pass_context
|
|
192
199
|
def deployment_create(
|
|
193
|
-
ctx: click.Context,
|
|
200
|
+
ctx: click.Context,
|
|
201
|
+
wait: bool,
|
|
202
|
+
auto: bool,
|
|
203
|
+
check: bool,
|
|
204
|
+
allow_destructive_operations: bool,
|
|
205
|
+
template: Optional[str],
|
|
206
|
+
verbose: bool,
|
|
194
207
|
) -> None:
|
|
195
208
|
"""
|
|
196
209
|
Validate and deploy the project server side.
|
|
197
210
|
"""
|
|
198
|
-
create_deployment_cmd(ctx, wait, auto, check, allow_destructive_operations, template)
|
|
211
|
+
create_deployment_cmd(ctx, wait, auto, check, allow_destructive_operations, template, verbose)
|
|
199
212
|
|
|
200
213
|
|
|
201
214
|
@deployment_group.command(name="ls")
|
|
@@ -337,14 +350,27 @@ def deployment_discard(ctx: click.Context, wait: bool) -> None:
|
|
|
337
350
|
default=None,
|
|
338
351
|
help="URL of the template to use for the deployment. Example: https://github.com/tinybirdco/web-analytics-starter-kit/tree/main/tinybird",
|
|
339
352
|
)
|
|
353
|
+
@click.option(
|
|
354
|
+
"-v",
|
|
355
|
+
"--verbose",
|
|
356
|
+
is_flag=True,
|
|
357
|
+
default=False,
|
|
358
|
+
help="Show verbose output. Disabled by default.",
|
|
359
|
+
)
|
|
340
360
|
@click.pass_context
|
|
341
361
|
def deploy(
|
|
342
|
-
ctx: click.Context,
|
|
362
|
+
ctx: click.Context,
|
|
363
|
+
wait: bool,
|
|
364
|
+
auto: bool,
|
|
365
|
+
check: bool,
|
|
366
|
+
allow_destructive_operations: bool,
|
|
367
|
+
template: Optional[str],
|
|
368
|
+
verbose: bool,
|
|
343
369
|
) -> None:
|
|
344
370
|
"""
|
|
345
371
|
Deploy the project.
|
|
346
372
|
"""
|
|
347
|
-
create_deployment_cmd(ctx, wait, auto, check, allow_destructive_operations, template)
|
|
373
|
+
create_deployment_cmd(ctx, wait, auto, check, allow_destructive_operations, template, verbose)
|
|
348
374
|
|
|
349
375
|
|
|
350
376
|
def create_deployment_cmd(
|
|
@@ -354,6 +380,7 @@ def create_deployment_cmd(
|
|
|
354
380
|
check: Optional[bool] = None,
|
|
355
381
|
allow_destructive_operations: Optional[bool] = None,
|
|
356
382
|
template: Optional[str] = None,
|
|
383
|
+
verbose: bool = False,
|
|
357
384
|
) -> None:
|
|
358
385
|
project: Project = ctx.ensure_object(dict)["project"]
|
|
359
386
|
if template:
|
|
@@ -378,4 +405,4 @@ def create_deployment_cmd(
|
|
|
378
405
|
click.echo(FeedbackManager.success(message="Template downloaded successfully"))
|
|
379
406
|
client = ctx.ensure_object(dict)["client"]
|
|
380
407
|
config: Dict[str, Any] = ctx.ensure_object(dict)["config"]
|
|
381
|
-
create_deployment(project, client, config, wait, auto, check, allow_destructive_operations)
|
|
408
|
+
create_deployment(project, client, config, wait, auto, verbose, check, allow_destructive_operations)
|
|
@@ -194,6 +194,7 @@ def create_deployment(
|
|
|
194
194
|
config: Dict[str, Any],
|
|
195
195
|
wait: bool,
|
|
196
196
|
auto: bool,
|
|
197
|
+
verbose: bool = False,
|
|
197
198
|
check: Optional[bool] = None,
|
|
198
199
|
allow_destructive_operations: Optional[bool] = None,
|
|
199
200
|
) -> None:
|
|
@@ -247,9 +248,12 @@ def create_deployment(
|
|
|
247
248
|
if f.get("level", "").upper() == "ERROR":
|
|
248
249
|
feedback_func = FeedbackManager.error
|
|
249
250
|
feedback_icon = ""
|
|
250
|
-
|
|
251
|
+
elif f.get("level", "").upper() == "WARNING":
|
|
251
252
|
feedback_func = FeedbackManager.warning
|
|
252
253
|
feedback_icon = "△ "
|
|
254
|
+
elif verbose and f.get("level", "").upper() == "INFO":
|
|
255
|
+
feedback_func = FeedbackManager.info
|
|
256
|
+
feedback_icon = ""
|
|
253
257
|
resource = f.get("resource")
|
|
254
258
|
resource_bit = f"{resource}: " if resource else ""
|
|
255
259
|
click.echo(feedback_func(message=f"{feedback_icon}{f.get('level')}: {resource_bit}{f.get('message')}"))
|
|
@@ -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:
|