tinybird 0.0.1.dev253__py3-none-any.whl → 0.0.1.dev255__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/prompts.py CHANGED
@@ -689,7 +689,6 @@ materialized_pipe_instructions = """
689
689
  - TYPE MATERIALIZED is the type of the pipe and it is mandatory for materialized pipes.
690
690
  - The content of the .pipe file must follow the materialized_pipe_content format.
691
691
  - Use State modifier for the aggregated columns in the pipe.
692
- - Keep the SQL query simple and avoid using complex queries with joins, subqueries, etc.
693
692
  </materialized_pipe_instructions>
694
693
  <materialized_pipe_content>
695
694
  NODE daily_sales
@@ -812,11 +811,11 @@ TYPE endpoint
812
811
  """
813
812
 
814
813
  pipe_instructions = """
814
+ Follow these instructions when creating or updating any type of .pipe file:
815
815
  <pipe_file_instructions>
816
816
  - The pipe names must be unique.
817
817
  - Nodes do NOT use the same name as the Pipe they belong to. So if the pipe name is "my_pipe", the nodes must be named different like "my_pipe_node_1", "my_pipe_node_2", etc.
818
818
  - Node names MUST be different from the resource names in the project.
819
- - Avoid more than one node per pipe unless it is really necessary or requested by the user.
820
819
  - No indentation is allowed for property names: DESCRIPTION, NODE, SQL, TYPE, etc.
821
820
  - Allowed TYPE values are: endpoint, copy, materialized, sink.
822
821
  - Add always the output node in the TYPE section or in the last node of the pipe.
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.dev253'
8
- __revision__ = '3bc2143'
7
+ __version__ = '0.0.1.dev255'
8
+ __revision__ = '208306e'
@@ -30,22 +30,25 @@ from tinybird.tb.modules.agent.memory import clear_history
30
30
  from tinybird.tb.modules.agent.models import create_model, model_costs
31
31
  from tinybird.tb.modules.agent.prompts import (
32
32
  datafile_instructions,
33
+ endpoint_optimization_instructions,
33
34
  plan_instructions,
34
35
  resources_prompt,
36
+ sql_agent_instructions,
35
37
  sql_instructions,
36
38
  )
37
- from tinybird.tb.modules.agent.tools.append import append
39
+ from tinybird.tb.modules.agent.tools.analyze import analyze_file, analyze_url
40
+ from tinybird.tb.modules.agent.tools.append import append_file, append_url
38
41
  from tinybird.tb.modules.agent.tools.build import build
39
42
  from tinybird.tb.modules.agent.tools.create_datafile import create_datafile
40
43
  from tinybird.tb.modules.agent.tools.deploy import deploy
41
44
  from tinybird.tb.modules.agent.tools.deploy_check import deploy_check
45
+ from tinybird.tb.modules.agent.tools.diff_resource import diff_resource
42
46
  from tinybird.tb.modules.agent.tools.execute_query import execute_query
43
47
  from tinybird.tb.modules.agent.tools.get_endpoint_stats import get_endpoint_stats
44
48
  from tinybird.tb.modules.agent.tools.get_openapi_definition import get_openapi_definition
45
49
  from tinybird.tb.modules.agent.tools.mock import mock
46
50
  from tinybird.tb.modules.agent.tools.plan import plan
47
51
  from tinybird.tb.modules.agent.tools.preview_datafile import preview_datafile
48
- from tinybird.tb.modules.agent.tools.read_fixture_data import read_fixture_data
49
52
  from tinybird.tb.modules.agent.tools.request_endpoint import request_endpoint
50
53
  from tinybird.tb.modules.agent.utils import TinybirdAgentContext, show_input
51
54
  from tinybird.tb.modules.build_common import process as build_process
@@ -112,12 +115,15 @@ You have access to the following tools:
112
115
  5. `deploy` - Deploy the project to Tinybird Cloud.
113
116
  6. `deploy_check` - Check if the project can be deployed to Tinybird Cloud before deploying it.
114
117
  7. `mock` - Create mock data for a landing datasource.
115
- 8. `read_fixture_data` - Read a fixture data file present in the project folder.
116
- 9. `append` - Append existing fixture to a datasource.
117
- 10. `get_endpoint_stats` - Get metrics of the requests to an endpoint.
118
- 11. `get_openapi_definition` - Get the OpenAPI definition for all endpoints that are built/deployed to Tinybird Cloud or Local.
119
- 12. `execute_query` - Execute a query against Tinybird Cloud or Local.
118
+ 8. `analyze_file` - Analyze the content of a fixture file present in the project folder.
119
+ 9. `analyze_url` - Analyze the content of an external url.
120
+ 9. `append_file` - Append a file present in the project to a datasource.
121
+ 10. `append_url` - Append an external url to a datasource.
122
+ 11. `get_endpoint_stats` - Get metrics of the requests to an endpoint.
123
+ 12. `get_openapi_definition` - Get the OpenAPI definition for all endpoints that are built/deployed to Tinybird Cloud or Local.
124
+ 13. `execute_query` - Execute a query against Tinybird Cloud or Local.
120
125
  13. `request_endpoint` - Request an endpoint against Tinybird Cloud or Local.
126
+ 14. `diff_resource` - Diff the content of a resource in Tinybird Cloud vs Tinybird Local vs Project local file.
121
127
 
122
128
  # When creating or updating datafiles:
123
129
  1. Use `plan` tool to plan the creation or update of resources.
@@ -133,21 +139,11 @@ You have access to the following tools:
133
139
  - If the user does not specify anything about the desired schema, create a schema like this:
134
140
  SCHEMA >
135
141
  `data` String `json:$`
142
+
136
143
  - Use always json paths with .ndjson files.
137
144
 
138
145
  # When user wants to optimize an endpoint:
139
- First check if the query is optimized. E.g is filtering by a column present in the sorting key.
140
- Avoid when possible to update the landing datasource.
141
- Check endpoint stats to analyze how the endpoint is performing. Use `get_endpoint_stats` tool to get the stats.
142
- You can use `request_endpoint` tool to check if the endpoint is working as expected.
143
- You can use `execute_query` tool to execute a query against Tinybird Cloud or Local, to test different queries.
144
- When your data is in Tinybird, you can create intermediate data sources to preprocess data and make the endpoints faster. This can be done by using materialized views or copy pipes.
145
- - Copy pipes capture the result of a pipe at a specific point in time and write it to a target data source. They can run on a schedule or run on demand, making them ideal for event-sourced snapshots, data experimentation, and deduplication with snapshots.
146
- - Materialized views continuously re-evaluate a query as new events are inserted, maintaining an always up-to-date derived dataset. Unlike copy pipes which create point-in-time snapshots, materialized views provide real-time transformations of your data.
147
- Each approach has its own strengths and use cases:
148
- - Use copy pipes when you need scheduled or on-demand snapshots of your data.
149
- - Use materialized views when you need continuous, real-time transformations.
150
- Finally, update the existing endpoint itself, do not add a new one.
146
+ {endpoint_optimization_instructions}
151
147
 
152
148
  IMPORTANT: If the user cancels some of the steps or there is an error in file creation, DO NOT continue with the plan. Stop the process and wait for the user before using any other tool.
153
149
  IMPORTANT: Every time you finish a plan and start a new resource creation or update process, create a new plan before starting with the changes.
@@ -179,6 +175,7 @@ IMPORTANT: Every time you finish a plan and start a new resource creation or upd
179
175
  {copy_pipe_instructions}
180
176
 
181
177
  # Working with SQL queries:
178
+ {sql_agent_instructions}
182
179
  {sql_instructions}
183
180
 
184
181
  # Working with connections files:
@@ -189,6 +186,13 @@ Kafka: {kafka_connection_example}
189
186
  S3: {s3_connection_example}
190
187
  GCS: {gcs_connection_example}
191
188
 
189
+ # When executing a query or requesting an endpoint:
190
+ - You need to be sure that the selected resource is updated to the last version in the environment you are working on.
191
+ - Use `diff_resource` tool to compare the content of the resource to compare the differences between environments.
192
+ - Project local file is the source of truth.
193
+ - If the resource is not present or updated to the last version in Tinybird Local, it means you need to build the project.
194
+ - If the resource is not present or updated to the last version in Tinybird Cloud, it means you need to deploy the project.
195
+
192
196
  # Info
193
197
  Today is {datetime.now().strftime("%Y-%m-%d")}
194
198
  """,
@@ -200,8 +204,10 @@ Today is {datetime.now().strftime("%Y-%m-%d")}
200
204
  Tool(deploy, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
201
205
  Tool(deploy_check, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
202
206
  Tool(mock, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
203
- Tool(read_fixture_data, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
204
- Tool(append, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
207
+ Tool(analyze_file, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
208
+ Tool(analyze_url, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
209
+ Tool(append_file, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
210
+ Tool(append_url, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
205
211
  Tool(
206
212
  get_endpoint_stats, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True
207
213
  ),
@@ -213,6 +219,7 @@ Today is {datetime.now().strftime("%Y-%m-%d")}
213
219
  ),
214
220
  Tool(execute_query, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
215
221
  Tool(request_endpoint, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
222
+ Tool(diff_resource, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
216
223
  ],
217
224
  )
218
225
 
@@ -238,10 +245,16 @@ Today is {datetime.now().strftime("%Y-%m-%d")}
238
245
  mock_data=partial(mock_data, project=project, config=config),
239
246
  append_data=partial(append_data, config=config),
240
247
  analyze_fixture=partial(analyze_fixture, config=config),
241
- execute_cloud_query=partial(execute_cloud_query, config=config),
242
- execute_local_query=partial(execute_local_query, config=config),
248
+ execute_query_cloud=partial(execute_query_cloud, config=config),
249
+ execute_query_local=partial(execute_query_local, config=config),
243
250
  request_endpoint_cloud=partial(request_endpoint_cloud, config=config),
244
251
  request_endpoint_local=partial(request_endpoint_local, config=config),
252
+ get_datasource_datafile_cloud=partial(get_datasource_datafile_cloud, config=config),
253
+ get_datasource_datafile_local=partial(get_datasource_datafile_local, config=config),
254
+ get_pipe_datafile_cloud=partial(get_pipe_datafile_cloud, config=config),
255
+ get_pipe_datafile_local=partial(get_pipe_datafile_local, config=config),
256
+ get_connection_datafile_cloud=partial(get_connection_datafile_cloud, config=config),
257
+ get_connection_datafile_local=partial(get_connection_datafile_local, config=config),
245
258
  get_project_files=project.get_project_files,
246
259
  folder=folder,
247
260
  thinking_animation=thinking_animation,
@@ -281,8 +294,8 @@ def run_agent(
281
294
  token = config.get("token", None)
282
295
  host = config.get("host", None)
283
296
  user_token = config.get("user_token", None)
284
- workspace_id = config.get("id", None)
285
- workspace_name = config.get("name", None)
297
+ workspace_id = config.get("id", "")
298
+ workspace_name = config.get("name", "")
286
299
  try:
287
300
  if not token or not host or not workspace_id or not user_token:
288
301
  yes = click.confirm(
@@ -443,18 +456,18 @@ def mock_data(
443
456
  )
444
457
 
445
458
 
446
- def analyze_fixture(config: dict[str, Any], fixture_path: str) -> dict[str, Any]:
459
+ def analyze_fixture(config: dict[str, Any], fixture_path: str, format: str = "json") -> dict[str, Any]:
447
460
  local_client = get_tinybird_local_client(config, test=False, silent=True)
448
- meta, _data = _analyze(fixture_path, local_client, Path(fixture_path).suffix.lstrip("."))
461
+ meta, _data = _analyze(fixture_path, local_client, format)
449
462
  return meta
450
463
 
451
464
 
452
- def execute_cloud_query(config: dict[str, Any], query: str, pipe_name: Optional[str] = None) -> dict[str, Any]:
465
+ def execute_query_cloud(config: dict[str, Any], query: str, pipe_name: Optional[str] = None) -> dict[str, Any]:
453
466
  client = _get_tb_client(config["token"], config["host"])
454
467
  return client.query(sql=query, pipeline=pipe_name)
455
468
 
456
469
 
457
- def execute_local_query(config: dict[str, Any], query: str, pipe_name: Optional[str] = None) -> dict[str, Any]:
470
+ def execute_query_local(config: dict[str, Any], query: str, pipe_name: Optional[str] = None) -> dict[str, Any]:
458
471
  local_client = get_tinybird_local_client(config, test=False, silent=True)
459
472
  return local_client.query(sql=query, pipeline=pipe_name)
460
473
 
@@ -471,3 +484,51 @@ def request_endpoint_local(
471
484
  ) -> dict[str, Any]:
472
485
  local_client = get_tinybird_local_client(config, test=False, silent=True)
473
486
  return local_client.pipe_data(endpoint_name, format="json", params=params)
487
+
488
+
489
+ def get_datasource_datafile_cloud(config: dict[str, Any], datasource_name: str) -> str:
490
+ try:
491
+ client = _get_tb_client(config["token"], config["host"])
492
+ return client.datasource_file(datasource_name)
493
+ except Exception:
494
+ return "Datasource not found"
495
+
496
+
497
+ def get_datasource_datafile_local(config: dict[str, Any], datasource_name: str) -> str:
498
+ try:
499
+ local_client = get_tinybird_local_client(config, test=False, silent=True)
500
+ return local_client.datasource_file(datasource_name)
501
+ except Exception:
502
+ return "Datasource not found"
503
+
504
+
505
+ def get_pipe_datafile_cloud(config: dict[str, Any], pipe_name: str) -> str:
506
+ try:
507
+ client = _get_tb_client(config["token"], config["host"])
508
+ return client.pipe_file(pipe_name)
509
+ except Exception:
510
+ return "Pipe not found"
511
+
512
+
513
+ def get_pipe_datafile_local(config: dict[str, Any], pipe_name: str) -> str:
514
+ try:
515
+ local_client = get_tinybird_local_client(config, test=False, silent=True)
516
+ return local_client.pipe_file(pipe_name)
517
+ except Exception:
518
+ return "Pipe not found"
519
+
520
+
521
+ def get_connection_datafile_cloud(config: dict[str, Any], connection_name: str) -> str:
522
+ try:
523
+ client = _get_tb_client(config["token"], config["host"])
524
+ return client.connection_file(connection_name)
525
+ except Exception:
526
+ return "Connection not found"
527
+
528
+
529
+ def get_connection_datafile_local(config: dict[str, Any], connection_name: str) -> str:
530
+ try:
531
+ local_client = get_tinybird_local_client(config, test=False, silent=True)
532
+ return local_client.connection_file(connection_name)
533
+ except Exception:
534
+ return "Connection not found"
@@ -1,4 +1,7 @@
1
1
  from pathlib import Path
2
+ from typing import Any
3
+
4
+ from pydantic_ai import format_as_xml
2
5
 
3
6
  from tinybird.tb.modules.project import Project
4
7
 
@@ -73,9 +76,10 @@ sql_instructions = """
73
76
  - Use node names as table names only when nodes are present in the same file.
74
77
  - Do not reference the current node name in the SQL.
75
78
  - SQL queries only accept SELECT statements with conditions, aggregations, joins, etc.
76
- - Do NOT use CREATE TABLE, INSERT INTO, CREATE DATABASE, etc.
79
+ - Do NOT use CREATE TABLE, INSERT INTO, CREATE DATABASE, SHOW TABLES, etc.
77
80
  - Use ONLY SELECT statements in the SQL section.
78
81
  - INSERT INTO is not supported in SQL section.
82
+ - Do NOT query system.<table_name> tables.
79
83
  - When using functions try always ClickHouse functions first, then SQL functions.
80
84
  - Parameters are never quoted in any case.
81
85
  - 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')}})
@@ -101,38 +105,32 @@ def resources_prompt(project: Project) -> str:
101
105
 
102
106
  resources_content = "# Existing resources in the project:\n"
103
107
  if files:
104
- paths = [Path(file_path) for file_path in files]
105
-
106
- resources_content += "\n".join(
107
- [
108
- f"""
109
- <resource>
110
- <path>{file_path.relative_to(project.folder)}</path>
111
- <type>{get_resource_type(file_path)}</type>
112
- <name>{file_path.stem}</name>
113
- <content>{file_path.read_text()}</content>
114
- </resource>
115
- """
116
- for file_path in paths
117
- ]
118
- )
108
+ resources: list[dict[str, Any]] = []
109
+ for filename in files:
110
+ file_path = Path(filename)
111
+ resource = {
112
+ "path": str(file_path.relative_to(project.folder)),
113
+ "type": get_resource_type(file_path),
114
+ "name": file_path.stem,
115
+ "content": file_path.read_text(),
116
+ }
117
+ resources.append(resource)
118
+ resources_content = format_as_xml(resources, root_tag="resources", item_tag="resource")
119
119
  else:
120
120
  resources_content += "No resources found"
121
121
 
122
122
  fixture_content = "# Fixture files in the project:\n"
123
123
  if fixture_files:
124
- paths = [Path(file_path) for file_path in fixture_files]
125
- fixture_content += "\n".join(
126
- [
127
- f"""
128
- <fixture>
129
- <path>{file_path.relative_to(project.folder)}</path>
130
- <name>{file_path.stem}</name>
131
- </fixture>
132
- """
133
- for file_path in paths
134
- ]
135
- )
124
+ fixtures: list[dict[str, Any]] = []
125
+ for filename in fixture_files:
126
+ file_path = Path(filename)
127
+ fixture = {
128
+ "path": str(file_path.relative_to(project.folder)),
129
+ "name": file_path.stem,
130
+ }
131
+ fixtures.append(fixture)
132
+ fixture_content = format_as_xml(fixtures, root_tag="fixtures", item_tag="fixture")
133
+
136
134
  else:
137
135
  fixture_content += "No fixture files found"
138
136
 
@@ -147,3 +145,352 @@ def get_resource_type(path: Path) -> str:
147
145
  elif path.suffix.lower() == ".connection":
148
146
  return "connection"
149
147
  return "unknown"
148
+
149
+
150
+ endpoint_optimization_instructions = """
151
+ <endpoint_optimization_instructions>
152
+ ## Endpoint Optimization Instructions
153
+ ### Step 1: Identify Performance Issues
154
+ 1. Analyze the endpoint's query performance metrics
155
+ 2. Look for endpoints with high latency or excessive data scanning
156
+ 3. Check read_bytes/write_bytes ratios to detect inefficient operations
157
+
158
+ ### Step 2: Apply the 5-Question Diagnostic Framework
159
+
160
+ #### Question 1: Are you aggregating or transforming data at query time?
161
+ **Detection:**
162
+ - Look for `count()`, `sum()`, `avg()`, or data type casting in published API endpoints
163
+ - Check if the same calculations are performed on every request
164
+
165
+ **Fix:**
166
+ - Create Materialized Views to pre-aggregate data at ingestion time
167
+ - Move transformations from query time to ingestion time
168
+ - Example transformation:
169
+ ```sql
170
+ -- Before (in endpoint)
171
+ SELECT date, count(*) as daily_count
172
+ FROM events
173
+ GROUP BY date
174
+
175
+ -- After (in Materialized View)
176
+ ENGINE "AggregatingMergeTree"
177
+ ENGINE_PARTITION_KEY "toYYYYMM(date)"
178
+ ENGINE_SORTING_KEY "date"
179
+ AS SELECT
180
+ date,
181
+ count(*) as daily_count
182
+ FROM events
183
+ GROUP BY date
184
+ ```
185
+
186
+ #### Question 2: Are you filtering by fields in the sorting key?
187
+ **Detection:**
188
+ - Examine WHERE clauses in queries
189
+ - Check if filtered columns are part of the sorting key
190
+ - Look for filters on partition keys instead of sorting keys
191
+
192
+ **Fix:**
193
+ - Ensure sorting key includes frequently filtered columns
194
+ - Order sorting key columns by selectivity (most selective first)
195
+ - Guidelines:
196
+ - Use 3-5 columns in sorting key
197
+ - Place `customer_id` or tenant identifiers first for multi-tenant apps
198
+ - Avoid `timestamp` as the first sorting key element
199
+ - Never use partition key for filtering
200
+
201
+ **Example Fix:**
202
+ ```sql
203
+ -- Before
204
+ ENGINE_SORTING_KEY "timestamp, customer_id"
205
+
206
+ -- After (better for multi-tenant filtering)
207
+ ENGINE_SORTING_KEY "customer_id, timestamp"
208
+ ```
209
+
210
+ #### Question 3: Are you using the best data types?
211
+ **Detection:**
212
+ - Scan for overly large data types:
213
+ - String where UUID would work
214
+ - Int64 where UInt32 would suffice
215
+ - DateTime with unnecessary precision
216
+ - Nullable columns that could have defaults
217
+
218
+ **Fix:**
219
+ - Downsize data types:
220
+ ```sql
221
+ -- Before
222
+ id String,
223
+ count Int64,
224
+ created_at DateTime64(3),
225
+ status Nullable(String)
226
+
227
+ -- After
228
+ id UUID,
229
+ count UInt32,
230
+ created_at DateTime,
231
+ status LowCardinality(String) DEFAULT 'pending'
232
+ ```
233
+ - Use `LowCardinality()` for strings with <100k unique values
234
+ - Replace Nullable with default values using `coalesce()`
235
+
236
+ #### Question 4: Are you doing complex operations early in the pipeline?
237
+ **Detection:**
238
+ - Look for JOINs or aggregations before filters
239
+ - Check operation order in multi-node pipes
240
+
241
+ **Fix:**
242
+ - Reorder operations: Filter → Simple transforms → Complex operations
243
+ - Example:
244
+ ```sql
245
+ -- Before
246
+ SELECT * FROM (
247
+ SELECT a.*, b.name
248
+ FROM events a
249
+ JOIN users b ON a.user_id = b.id
250
+ ) WHERE date >= today() - 7
251
+
252
+ -- After
253
+ SELECT a.*, b.name
254
+ FROM (
255
+ SELECT * FROM events
256
+ WHERE date >= today() - 7
257
+ ) a
258
+ JOIN users b ON a.user_id = b.id
259
+ ```
260
+
261
+ #### Question 5: Are you joining two or more data sources?
262
+ **Detection:**
263
+ - Identify JOINs in queries
264
+ - Check read_bytes/write_bytes ratio in Materialized Views
265
+ - Look for full table scans on joined tables
266
+
267
+ **Fix Options:**
268
+ 1. Replace JOIN with subquery:
269
+ ```sql
270
+ -- Before
271
+ SELECT e.*, u.name
272
+ FROM events e
273
+ JOIN users u ON e.user_id = u.id
274
+
275
+ -- After
276
+ SELECT e.*,
277
+ (SELECT name FROM users WHERE id = e.user_id) as name
278
+ FROM events e
279
+ WHERE user_id IN (SELECT id FROM users)
280
+ ```
281
+
282
+ 2. Optimize Materialized View JOINs:
283
+ ```sql
284
+ -- Before (inefficient)
285
+ SELECT a.id, a.value, b.value
286
+ FROM a
287
+ LEFT JOIN b USING id
288
+
289
+ -- After (optimized)
290
+ SELECT a.id, a.value, b.value
291
+ FROM a
292
+ LEFT JOIN (
293
+ SELECT id, value
294
+ FROM b
295
+ WHERE b.id IN (SELECT id FROM a)
296
+ ) b USING id
297
+ ```
298
+
299
+ ### Step 3: Implementation Actions
300
+
301
+ #### For Schema Changes:
302
+ 1. Update the datasource schema
303
+ 2. Update the sorting keys and data types
304
+ 3. Update dependent pipes and endpoints
305
+
306
+ #### For Query Optimizations:
307
+ 1. Create Materialized Views for repeated aggregations
308
+ 2. Rewrite queries following best practices
309
+ 3. Test performance improvements
310
+
311
+ #### For JOIN Optimizations:
312
+ 1. Evaluate if JOIN is necessary
313
+ 2. Consider denormalization strategies
314
+ 3. Use Copy Pipes for historical data recalculation
315
+ 4. Implement filtered JOINs in Materialized Views
316
+
317
+ #### In general:
318
+ 1. If you need to iterate an existing resource, do not create a new iteration, just update it with the needed changes.
319
+
320
+ ## Monitoring and Validation
321
+
322
+ ### Monitoring:
323
+ 1. Set up alerts for endpoints exceeding latency thresholds
324
+ 2. Review of tinybird.pipe_stats_rt (realtime stats of last 24h) and tinybird.pipe_stats (historical stats aggregated by day)
325
+ 3. Track processed data patterns over time
326
+ 4. Monitor for query pattern changes
327
+
328
+ ### Success Metrics:
329
+ - Reduced query latency
330
+ - Lower data scanning (read_bytes)
331
+ - Improved read_bytes/write_bytes ratio
332
+ - Consistent sub-second API response times
333
+
334
+ ## Code Templates
335
+
336
+ ### Materialized View Template:
337
+ ```sql
338
+ NODE materialized_view_name
339
+ SQL >
340
+ SELECT
341
+ -- Pre-aggregated fields
342
+ toDate(timestamp) as date,
343
+ customer_id,
344
+ count(*) as event_count,
345
+ sum(amount) as total_amount
346
+ FROM source_table
347
+ GROUP BY date, customer_id
348
+
349
+ TYPE materialized
350
+ DATASOURCE mv_datasource_name
351
+ ENGINE "AggregatingMergeTree"
352
+ ENGINE_PARTITION_KEY "toYYYYMM(date)"
353
+ ENGINE_SORTING_KEY "customer_id, date"
354
+ ```
355
+
356
+ ### Optimized Query Template:
357
+ ```sql
358
+ NODE endpoint_query
359
+ SQL >
360
+ -- Step 1: Filter early
361
+ WITH filtered_data AS (
362
+ SELECT * FROM events
363
+ WHERE customer_id = {{ String(customer_id) }}
364
+ AND date >= {{ Date(start_date) }}
365
+ AND date <= {{ Date(end_date) }}
366
+ )
367
+ -- Step 2: Simple operations
368
+ SELECT
369
+ date,
370
+ sum(amount) as daily_total
371
+ FROM filtered_data
372
+ GROUP BY date
373
+ ORDER BY date DESC
374
+ ```
375
+
376
+ ## Best Practices Summary
377
+
378
+ 1. **Think ingestion-time, not query-time** - Move computations upstream
379
+ 2. **Index smartly** - Sorting keys should match filter patterns
380
+ 3. **Size appropriately** - Use the smallest viable data types
381
+ 4. **Filter first** - Reduce data before complex operations
382
+ 5. **JOIN carefully** - Consider alternatives and optimize when necessary
383
+ </endpoint_optimization_instructions>
384
+ """
385
+
386
+
387
+ sql_agent_instructions = """
388
+ # SQL Best Practices Rules
389
+
390
+ ## Core Principles
391
+ 1. **The best data is the data you don't write** - Don't save unnecessary data
392
+ 2. **The second best data is the one you don't read** - Filter as early as possible
393
+ 3. **Sequential reads are much faster** - Use proper indexes and sorting keys
394
+ 4. **The less data you process after read, the better** - Select only needed columns
395
+ 5. **Perform complex operations later in the processing pipeline** - Filter before joins/aggregations
396
+
397
+ ## SQL Query Rules
398
+
399
+ ### 1. Filter Placement Rules
400
+ - **ALWAYS** apply WHERE filters before ORDER BY clauses
401
+ - **ALWAYS** apply WHERE filters before GROUP BY operations
402
+ - **ALWAYS** filter data at the earliest possible point in the query
403
+ - **NEVER** sort data before filtering it
404
+
405
+ ### 2. Column Selection Rules
406
+ - **NEVER** use SELECT * in production queries
407
+ - **ALWAYS** specify only the columns you need
408
+ - **ALWAYS** minimize the number of columns retrieved to reduce memory usage
409
+
410
+ ### 3. Sorting and Index Rules
411
+ - **ALWAYS** filter by ENGINE_SORTING_KEY columns first (typically date/time columns)
412
+ - **ALWAYS** order filtering conditions from most to least selective
413
+ - **ALWAYS** use columns in ENGINE_SORTING_KEY for WHERE clauses when possible
414
+ - **NEVER** use functions on indexed columns in WHERE clauses (e.g., avoid DATE_FORMAT, EXTRACT)
415
+
416
+ ### 4. Join Optimization Rules
417
+ - **ALWAYS** pre-filter data before JOIN operations
418
+ - **NEVER** join tables with more than 1 million rows without filtering
419
+ - **ALWAYS** filter the right-side table in joins using subqueries
420
+ - **PREFERRED** pattern for large joins:
421
+ ```sql
422
+ -- Good: Pre-filter right table
423
+ FROM left_table AS left
424
+ INNER JOIN (
425
+ SELECT id, column FROM right_table
426
+ WHERE id IN (SELECT id FROM left_table)
427
+ ) AS right ON left.id = right.id
428
+ ```
429
+
430
+ ### 5. Aggregation Rules
431
+ - **NEVER** use nested aggregate functions (e.g., MAX(AVG(column)))
432
+ - **ALWAYS** use subqueries instead of nested aggregates
433
+ - **ALWAYS** filter data before GROUP BY operations
434
+ - **ALWAYS** perform aggregations as late as possible in the query
435
+
436
+ ### 6. Complex Operations Order
437
+ - **ALWAYS** follow this operation order:
438
+ 1. Filter (WHERE)
439
+ 2. Select only needed columns
440
+ 3. Join (if necessary)
441
+ 4. Group/Aggregate (if necessary)
442
+ 5. Sort (ORDER BY)
443
+ 6. Limit
444
+
445
+ ### 7. Aggregate Function Rules
446
+ - **ALWAYS** use -Merge combinators (countMerge, avgMerge, etc.) when querying AggregateFunction columns
447
+ - **ALWAYS** apply -Merge functions as late as possible in the pipeline
448
+ - **NEVER** select AggregateFunction columns without the appropriate -Merge combinator
449
+
450
+ ### 8. Performance Rules
451
+ - **AVOID** full table scans - always include WHERE clauses
452
+ - **AVOID** reading more than 1GB of data in a single query
453
+ - **AVOID** operations that load large datasets into memory
454
+ - **MINIMIZE** the number of rows processed at each step
455
+
456
+ ### 9. Memory Optimization Rules
457
+ - **REDUCE** column count when hitting memory limits
458
+ - **AVOID** cross JOINs that generate excessive rows
459
+ - **FILTER** before massive GROUP BY operations
460
+ - **CHUNK** large populate operations (they run in 1M row chunks)
461
+
462
+ ### 10. Query Pattern Examples
463
+
464
+ **BAD Pattern - Filtering after sorting:**
465
+ ```sql
466
+ SELECT * FROM table ORDER BY date WHERE condition = true
467
+ ```
468
+
469
+ **GOOD Pattern - Filtering before sorting:**
470
+ ```sql
471
+ SELECT column1, column2 FROM table WHERE condition = true ORDER BY date
472
+ ```
473
+
474
+ **BAD Pattern - Nested aggregates:**
475
+ ```sql
476
+ SELECT MAX(AVG(amount)) FROM table
477
+ ```
478
+
479
+ **GOOD Pattern - Using subquery:**
480
+ ```sql
481
+ SELECT MAX(avg_amount) FROM (SELECT AVG(amount) as avg_amount FROM table)
482
+ ```
483
+
484
+ **BAD Pattern - Unfiltered join:**
485
+ ```sql
486
+ SELECT * FROM small_table JOIN huge_table ON small_table.id = huge_table.id
487
+ ```
488
+
489
+ **GOOD Pattern - Pre-filtered join:**
490
+ ```sql
491
+ SELECT needed_columns
492
+ FROM small_table
493
+ JOIN (SELECT id, col FROM huge_table WHERE id IN (SELECT id FROM small_table)) filtered
494
+ ON small_table.id = filtered.id
495
+ ```
496
+ """
@@ -0,0 +1,79 @@
1
+ import json
2
+ from pathlib import Path
3
+ from urllib.parse import urlparse
4
+
5
+ import click
6
+ from pydantic_ai import RunContext
7
+
8
+ from tinybird.tb.modules.agent.utils import TinybirdAgentContext
9
+ from tinybird.tb.modules.feedback_manager import FeedbackManager
10
+
11
+
12
+ def analyze_file(ctx: RunContext[TinybirdAgentContext], fixture_pathname: str):
13
+ """Analyze a fixture data file present in the project folder
14
+
15
+ Args:
16
+ fixture_pathname (str): a path or an external url to a fixture file. Required.
17
+
18
+ Returns:
19
+ str: The content of the fixture data file.
20
+ """
21
+ try:
22
+ ctx.deps.thinking_animation.stop()
23
+ click.echo(FeedbackManager.highlight(message=f"» Analyzing {fixture_pathname}..."))
24
+ fixture_path = Path(ctx.deps.folder) / fixture_pathname.lstrip("/")
25
+
26
+ if not fixture_path.exists():
27
+ click.echo(FeedbackManager.error(message=f"No fixture data found for {fixture_pathname}."))
28
+ ctx.deps.thinking_animation.start()
29
+ return f"No fixture data found for {fixture_pathname}. Please check the path of the fixture and try again."
30
+
31
+ fixture_extension = fixture_path.suffix.lstrip(".")
32
+ response = ctx.deps.analyze_fixture(fixture_path=str(fixture_path), format=fixture_extension)
33
+ click.echo(FeedbackManager.success(message="✓ Done!\n"))
34
+ ctx.deps.thinking_animation.start()
35
+ # limit content to first 10 rows
36
+ data = response["preview"]["data"][:10]
37
+ columns = response["analysis"]["columns"]
38
+
39
+ return f"#Result of analysis of {fixture_pathname}:\n##Columns:\n{json.dumps(columns)}\n##Data sample:\n{json.dumps(data)}"
40
+ except Exception as e:
41
+ ctx.deps.thinking_animation.stop()
42
+ click.echo(FeedbackManager.error(message=f"Error analyzing {fixture_pathname}: {e}"))
43
+ ctx.deps.thinking_animation.start()
44
+ return f"Error analyzing {fixture_pathname}: {e}"
45
+
46
+
47
+ def analyze_url(ctx: RunContext[TinybirdAgentContext], fixture_url: str):
48
+ """Analyze a fixture file present in an external url
49
+
50
+ Args:
51
+ fixture_url (str): an external url to a fixture file. Required.
52
+
53
+ Returns:
54
+ str: The analysis with the columns and the first 10 rows of the fixture data file.
55
+ """
56
+ try:
57
+ ctx.deps.thinking_animation.stop()
58
+ is_url = urlparse(fixture_url).scheme in ("http", "https")
59
+ click.echo(FeedbackManager.highlight(message=f"» Analyzing {fixture_url}..."))
60
+ if not is_url:
61
+ click.echo(FeedbackManager.error(message=f"{fixture_url} is not a valid url."))
62
+ ctx.deps.thinking_animation.start()
63
+ return f"{fixture_url} is not a valid url. Please check the url and try again."
64
+
65
+ fixture_extension = fixture_url.split(".")[-1]
66
+
67
+ response = ctx.deps.analyze_fixture(fixture_path=fixture_url, format=fixture_extension)
68
+ click.echo(FeedbackManager.success(message="✓ Done!\n"))
69
+ ctx.deps.thinking_animation.start()
70
+ # limit content to first 10 rows
71
+ data = response["preview"]["data"][:10]
72
+ columns = response["analysis"]["columns"]
73
+
74
+ return f"#Result of analysis of URL {fixture_url}:\n##Columns:\n{json.dumps(columns)}\n##Data sample:\n{json.dumps(data)}"
75
+ except Exception as e:
76
+ ctx.deps.thinking_animation.stop()
77
+ click.echo(FeedbackManager.error(message=f"Error analyzing {fixture_url}: {e}"))
78
+ ctx.deps.thinking_animation.start()
79
+ return f"Error analyzing {fixture_url}: {e}"
@@ -5,7 +5,7 @@ from tinybird.tb.modules.agent.utils import TinybirdAgentContext, show_confirmat
5
5
  from tinybird.tb.modules.feedback_manager import FeedbackManager
6
6
 
7
7
 
8
- def append(ctx: RunContext[TinybirdAgentContext], datasource_name: str, fixture_pathname: str) -> str:
8
+ def append_file(ctx: RunContext[TinybirdAgentContext], datasource_name: str, fixture_pathname: str) -> str:
9
9
  """Append existing fixture to a datasource
10
10
 
11
11
  Args:
@@ -18,7 +18,7 @@ def append(ctx: RunContext[TinybirdAgentContext], datasource_name: str, fixture_
18
18
  try:
19
19
  ctx.deps.thinking_animation.stop()
20
20
  confirmation = show_confirmation(
21
- title=f"Append existing fixture for datasource {datasource_name}?",
21
+ title=f"Append fixture {fixture_pathname} to datasource {datasource_name}?",
22
22
  skip_confirmation=ctx.deps.dangerously_skip_permissions,
23
23
  )
24
24
 
@@ -41,7 +41,70 @@ def append(ctx: RunContext[TinybirdAgentContext], datasource_name: str, fixture_
41
41
  ctx.deps.thinking_animation.start()
42
42
  return f"Data appended to {datasource_name}"
43
43
  except Exception as e:
44
+ error_message = str(e)
44
45
  ctx.deps.thinking_animation.stop()
45
- click.echo(FeedbackManager.error(message=e))
46
+ click.echo(FeedbackManager.error(message=error_message))
47
+ error_message = handle_quarantine_error(ctx, error_message, datasource_name)
46
48
  ctx.deps.thinking_animation.start()
47
- return f"Error appending fixture {fixture_pathname} to {datasource_name}: {e}"
49
+ return f"Error appending fixture {fixture_pathname} to {datasource_name}: {error_message}"
50
+
51
+
52
+ def append_url(ctx: RunContext[TinybirdAgentContext], datasource_name: str, fixture_url: str) -> str:
53
+ """Append existing fixture to a datasource
54
+
55
+ Args:
56
+ datasource_name: Name of the datasource to append fixture to
57
+ fixture_url: external url to the fixture file to append
58
+
59
+ Returns:
60
+ str: Message indicating the success or failure of the appending
61
+ """
62
+ try:
63
+ ctx.deps.thinking_animation.stop()
64
+ confirmation = show_confirmation(
65
+ title=f"Append URL {fixture_url} to datasource {datasource_name}?",
66
+ skip_confirmation=ctx.deps.dangerously_skip_permissions,
67
+ )
68
+
69
+ if confirmation == "review":
70
+ click.echo()
71
+ feedback = show_input(ctx.deps.workspace_name)
72
+ ctx.deps.thinking_animation.start()
73
+ return f"User did not confirm appending URL {fixture_url} and gave the following feedback: {feedback}"
74
+
75
+ if confirmation == "cancel":
76
+ ctx.deps.thinking_animation.start()
77
+ return f"User rejected appending URL {fixture_url}. Skip this step"
78
+
79
+ ctx.deps.thinking_animation.stop()
80
+ click.echo(FeedbackManager.highlight(message=f"\n» Appending {fixture_url} to {datasource_name}..."))
81
+ ctx.deps.append_data(datasource_name=datasource_name, path=fixture_url)
82
+ click.echo(FeedbackManager.success(message=f"✓ Data appended to {datasource_name}"))
83
+ ctx.deps.thinking_animation.start()
84
+ return f"Data appended to {datasource_name}"
85
+ except Exception as e:
86
+ error_message = str(e)
87
+ ctx.deps.thinking_animation.stop()
88
+ click.echo(FeedbackManager.error(message=error_message))
89
+ error_message = handle_quarantine_error(ctx, error_message, datasource_name)
90
+ ctx.deps.thinking_animation.start()
91
+ return f"Error appending URL {fixture_url} to {datasource_name}: {error_message}"
92
+
93
+
94
+ def handle_quarantine_error(ctx: RunContext[TinybirdAgentContext], error_message: str, datasource_name: str) -> str:
95
+ try:
96
+ if "in quarantine" in error_message:
97
+ click.echo(FeedbackManager.highlight(message=f"\n» Looking for errors in {datasource_name}_quarantine..."))
98
+ query = (
99
+ f"select * from {datasource_name}_quarantine order by insertion_date desc limit 5 FORMAT CSVWithNames"
100
+ )
101
+ quarantine_data = ctx.deps.execute_query_local(query=query)
102
+ error_message = (
103
+ error_message
104
+ + f"\nThese are the first 5 rows of the quarantine table for datasource '{datasource_name}':\n{quarantine_data}. Use again `mock` tool but add this issue to the context."
105
+ )
106
+
107
+ except Exception as quarantine_error:
108
+ error_message = error_message + f"\nError accessing to {datasource_name}_quarantine: {quarantine_error}"
109
+
110
+ return error_message
@@ -0,0 +1,51 @@
1
+ from pathlib import Path
2
+
3
+ import click
4
+ from pydantic_ai import RunContext
5
+
6
+ from tinybird.tb.modules.agent.utils import Datafile, TinybirdAgentContext
7
+ from tinybird.tb.modules.feedback_manager import FeedbackManager
8
+
9
+
10
+ def diff_resource(ctx: RunContext[TinybirdAgentContext], resource: Datafile) -> str:
11
+ """Diff the content of a resource in Tinybird Cloud vs Tinybird Local vs Project local file
12
+
13
+ Args:
14
+ resource (Datafile): The resource to diff. Required.
15
+
16
+ Returns:
17
+ Datafile: The diff of the resource.
18
+ """
19
+ try:
20
+ ctx.deps.thinking_animation.stop()
21
+ click.echo(
22
+ FeedbackManager.highlight(message=f"\n» Comparing content of {resource.pathname} with Tinybird Cloud")
23
+ )
24
+ resource.pathname = resource.pathname.removeprefix("/")
25
+ project_file_path = Path(ctx.deps.folder) / resource.pathname
26
+ if not project_file_path.exists():
27
+ raise Exception(f"Resource {resource.pathname} not found in project")
28
+
29
+ project_file_content = project_file_path.read_text()
30
+ if resource.type == "datasource":
31
+ cloud_content = ctx.deps.get_datasource_datafile_cloud(datasource_name=resource.name)
32
+ local_content = ctx.deps.get_datasource_datafile_local(datasource_name=resource.name)
33
+ elif resource.type == "connection":
34
+ cloud_content = ctx.deps.get_connection_datafile_cloud(connection_name=resource.name)
35
+ local_content = ctx.deps.get_connection_datafile_local(connection_name=resource.name)
36
+ elif resource.type in ["endpoint", "materialized", "sink", "copy"]:
37
+ cloud_content = ctx.deps.get_pipe_datafile_cloud(pipe_name=resource.name)
38
+ local_content = ctx.deps.get_pipe_datafile_local(pipe_name=resource.name)
39
+ else:
40
+ raise Exception(f"{resource.type} is not a valid extension")
41
+
42
+ needs_to_build = project_file_content != local_content
43
+ needs_to_deploy = project_file_content != cloud_content
44
+ ctx.deps.thinking_animation.start()
45
+ diff = f"# Diff of resource {resource.name}:\n"
46
+ diff += f"## Tinybird Cloud: {'Deploy needed. Resource does not exist or needs to be updated. Run `deploy` tool to deploy the resource.' if needs_to_deploy else 'Nothing to deploy.'}\n"
47
+ diff += f"## Tinybird Local: {'Build needed. Resource does not exist or needs to be updated. Run `build` tool to build the resource.' if needs_to_build else 'Nothing to build.'}\n"
48
+ return diff
49
+ except Exception as e:
50
+ ctx.deps.thinking_animation.start()
51
+ return f"Could not diff resource {resource.pathname}: {e}"
@@ -3,7 +3,7 @@ import humanfriendly
3
3
  from pydantic_ai import RunContext
4
4
 
5
5
  from tinybird.tb.modules.agent.utils import TinybirdAgentContext
6
- from tinybird.tb.modules.common import echo_safe_humanfriendly_tables_format_smart_table
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
9
 
@@ -32,7 +32,7 @@ def execute_query(ctx: RunContext[TinybirdAgentContext], query: str, task: str,
32
32
  else:
33
33
  query = f"SELECT * FROM ({query}) {query_format}"
34
34
 
35
- execute_query = ctx.deps.execute_cloud_query if cloud else ctx.deps.execute_local_query
35
+ execute_query = ctx.deps.execute_query_cloud if cloud else ctx.deps.execute_query_local
36
36
  result = execute_query(query=query)
37
37
  stats = result["statistics"]
38
38
  seconds = stats["elapsed"]
@@ -44,9 +44,10 @@ def execute_query(ctx: RunContext[TinybirdAgentContext], query: str, task: str,
44
44
  if not result["data"]:
45
45
  click.echo(FeedbackManager.info_no_rows())
46
46
  else:
47
- echo_safe_humanfriendly_tables_format_smart_table(
48
- data=[d.values() for d in result["data"]], column_names=result["data"][0].keys()
47
+ echo_safe_humanfriendly_tables_format_pretty_table(
48
+ data=[d.values() for d in result["data"][:10]], column_names=result["data"][0].keys()
49
49
  )
50
+ click.echo("Showing first 10 results\n")
50
51
  ctx.deps.thinking_animation.start()
51
52
  result["data"] = result["data"][:10]
52
53
  return f"Result for task '{task}' in {cloud_or_local} environment: {result}. The user is being shown the full result in the console but this message only contains the first 10 rows."
@@ -42,7 +42,7 @@ def get_endpoint_stats(
42
42
  FORMAT JSON
43
43
  """
44
44
 
45
- execute_query = ctx.deps.execute_cloud_query if cloud else ctx.deps.execute_local_query
45
+ execute_query = ctx.deps.execute_query_cloud if cloud else ctx.deps.execute_query_local
46
46
 
47
47
  result = execute_query(query=query)
48
48
  click.echo(FeedbackManager.success(message="✓ Done!"))
@@ -60,7 +60,7 @@ def mock(
60
60
  FeedbackManager.highlight(message=f"\n» Looking for errors in {datasource_name}_quarantine...")
61
61
  )
62
62
  query = f"select * from {datasource_name}_quarantine order by insertion_date desc limit 5 FORMAT CSVWithNames"
63
- quarantine_data = ctx.deps.execute_local_query(query=query)
63
+ quarantine_data = ctx.deps.execute_query_local(query=query)
64
64
  error_message = (
65
65
  error_message
66
66
  + f"\nThese are the first 5 rows of the quarantine table for datasource '{datasource_name}':\n{quarantine_data}. Use again `mock` tool but add this issue to the context."
@@ -16,7 +16,7 @@ def preview_datafile(name: str, type: str, description: str, content: str, pathn
16
16
  """
17
17
 
18
18
  return Datafile(
19
- type=type,
19
+ type=type.lower(),
20
20
  name=name,
21
21
  content=content,
22
22
  description=description,
@@ -5,7 +5,7 @@ import humanfriendly
5
5
  from pydantic_ai import RunContext
6
6
 
7
7
  from tinybird.tb.modules.agent.utils import TinybirdAgentContext
8
- from tinybird.tb.modules.common import echo_safe_humanfriendly_tables_format_smart_table
8
+ from tinybird.tb.modules.common import echo_safe_humanfriendly_tables_format_pretty_table
9
9
  from tinybird.tb.modules.feedback_manager import FeedbackManager
10
10
 
11
11
 
@@ -48,10 +48,10 @@ def request_endpoint(
48
48
  if not result["data"]:
49
49
  click.echo(FeedbackManager.info_no_rows())
50
50
  else:
51
- echo_safe_humanfriendly_tables_format_smart_table(
52
- data=[d.values() for d in result["data"][:5]], column_names=result["data"][0].keys()
51
+ echo_safe_humanfriendly_tables_format_pretty_table(
52
+ data=[d.values() for d in result["data"][:10]], column_names=result["data"][0].keys()
53
53
  )
54
- click.echo("Showing first 5 results\n")
54
+ click.echo("Showing first 10 results\n")
55
55
  ctx.deps.thinking_animation.start()
56
56
  return f"Result for endpoint {endpoint_name} with params {params} in {cloud_or_local} environment: {result}. Do not show result is already shown in the console."
57
57
  except Exception as e:
@@ -41,10 +41,16 @@ class TinybirdAgentContext(BaseModel):
41
41
  mock_data: Callable[..., list[dict[str, Any]]]
42
42
  append_data: Callable[..., None]
43
43
  analyze_fixture: Callable[..., dict[str, Any]]
44
- execute_cloud_query: Callable[..., dict[str, Any]]
45
- execute_local_query: Callable[..., dict[str, Any]]
44
+ execute_query_cloud: Callable[..., dict[str, Any]]
45
+ execute_query_local: Callable[..., dict[str, Any]]
46
46
  request_endpoint_cloud: Callable[..., dict[str, Any]]
47
47
  request_endpoint_local: Callable[..., dict[str, Any]]
48
+ get_datasource_datafile_cloud: Callable[..., str]
49
+ get_datasource_datafile_local: Callable[..., str]
50
+ get_pipe_datafile_cloud: Callable[..., str]
51
+ get_pipe_datafile_local: Callable[..., str]
52
+ get_connection_datafile_cloud: Callable[..., str]
53
+ get_connection_datafile_local: Callable[..., str]
48
54
  dangerously_skip_permissions: bool
49
55
  token: str
50
56
  user_token: str
@@ -124,6 +124,23 @@ def echo_safe_humanfriendly_tables_format_smart_table(data: Iterable[Any], colum
124
124
  raise exc
125
125
 
126
126
 
127
+ def echo_safe_humanfriendly_tables_format_pretty_table(data: Iterable[Any], column_names: List[str]) -> None:
128
+ """
129
+ There is a bug in the humanfriendly library: it breaks to render the small table for small terminals
130
+ (`format_robust_table`) if we call format_smart_table with an empty dataset. This catches the error and prints
131
+ what we would call an empty "robust_table".
132
+ """
133
+ try:
134
+ click.echo(humanfriendly.tables.format_pretty_table(data, column_names=column_names))
135
+ except ValueError as exc:
136
+ if str(exc) == "max() arg is an empty sequence":
137
+ click.echo("------------")
138
+ click.echo("Empty")
139
+ click.echo("------------")
140
+ else:
141
+ raise exc
142
+
143
+
127
144
  def echo_safe_format_table(data: Iterable[Any], columns) -> None:
128
145
  """
129
146
  There is a bug in the humanfriendly library: it breaks to render the small table for small terminals
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: tinybird
3
- Version: 0.0.1.dev253
3
+ Version: 0.0.1.dev255
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/forward/commands
6
6
  Author: Tinybird
@@ -3,7 +3,7 @@ tinybird/context.py,sha256=FfqYfrGX_I7PKGTQo93utaKPDNVYWelg4Hsp3evX5wM,1291
3
3
  tinybird/datatypes.py,sha256=r4WCvspmrXTJHiPjjyOTiZyZl31FO3Ynkwq4LQsYm6E,11059
4
4
  tinybird/feedback_manager.py,sha256=1INQFfRfuMCb9lfB8KNf4r6qC2khW568hoHjtk-wshI,69305
5
5
  tinybird/git_settings.py,sha256=Sw_8rGmribEFJ4Z_6idrVytxpFYk7ez8ei0qHULzs3E,3934
6
- tinybird/prompts.py,sha256=4VmdaMX7oUFoqjseXe8QuF9wTtIabbDkdwVGmd34S7s,45502
6
+ tinybird/prompts.py,sha256=9zXOYI8TzFAyp_bolqtxhtaqg0c1u4AJolFM0o3aYiY,45393
7
7
  tinybird/sql.py,sha256=BufnOgclQokDyihtuXesOwHBsebN6wRXIxO5wKRkOwE,48299
8
8
  tinybird/sql_template.py,sha256=AezE1o6_BzbHFi0J9OIqTrXQ5WvoX5eNVq4QCbFjGcs,100338
9
9
  tinybird/sql_template_fmt.py,sha256=KUHdj5rYCYm_rKKdXYSJAE9vIyXUQLB0YSZnUXHeBlY,10196
@@ -17,7 +17,7 @@ tinybird/datafile/exceptions.py,sha256=8rw2umdZjtby85QbuRKFO5ETz_eRHwUY5l7eHsy1w
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=eK7q5QxuCP76bNpCpZzIiZeTSJL3JvT8Z0H4Zs8hUOY,247
20
+ tinybird/tb/__cli__.py,sha256=51Rg-OvKFWGEJgEjFVuc2XEUi5XT3bb5o8ZEUJ1bdhA,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
@@ -26,7 +26,7 @@ tinybird/tb/modules/build.py,sha256=efD-vamK1NPaDo9R86Hn8be2DYoW0Hh5bZiH7knK5dk,
26
26
  tinybird/tb/modules/build_common.py,sha256=rWhemU8bk0ZE2eiwZDaTmV9cPabDGGlyc2WnRxfhT0M,12859
27
27
  tinybird/tb/modules/cicd.py,sha256=0KLKccha9IP749QvlXBmzdWv1On3mFwMY4DUcJlBxiE,7326
28
28
  tinybird/tb/modules/cli.py,sha256=1kErLFhxgMWldbE7P4-bkPUcug8ymRyXDHRG9-vGb_4,16755
29
- tinybird/tb/modules/common.py,sha256=jTTaDDHrZREt--032XhP6GkbfFwC79YJ5aH1Sl7bmbo,81925
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
@@ -68,28 +68,29 @@ tinybird/tb/modules/watch.py,sha256=No0bK1M1_3CYuMaIgylxf7vYFJ72lTJe3brz6xQ-mJo,
68
68
  tinybird/tb/modules/workspace.py,sha256=Q_8HcxMsNg8QG9aBlwcWS2umrDP5IkTIHqqz3sfmGuc,11341
69
69
  tinybird/tb/modules/workspace_members.py,sha256=5JdkJgfuEwbq-t6vxkBhYwgsiTDxF790wsa6Xfif9nk,8608
70
70
  tinybird/tb/modules/agent/__init__.py,sha256=i3oe3vDIWWPaicdCM0zs7D7BJ1W0k7th93ooskHAV00,54
71
- tinybird/tb/modules/agent/agent.py,sha256=wZSHbWShmnq3VmS6Hf6TyU80_rOiZ4AwdltDu9Dnl3Q,23124
71
+ tinybird/tb/modules/agent/agent.py,sha256=HtXB75QJ71YH21BdbWhx2Pb1c2KUFu1_3TfdVulCoh4,25349
72
72
  tinybird/tb/modules/agent/animations.py,sha256=4WOC5_2BracttmMCrV0H91tXfWcUzQHBUaIJc5FA7tE,3490
73
73
  tinybird/tb/modules/agent/banner.py,sha256=KX_e467uiy1gWOZ4ofTZt0GCFGQqHQ_8Ob27XLQqda0,3053
74
74
  tinybird/tb/modules/agent/memory.py,sha256=H6SJK--2L5C87B7AJd_jMqsq3sCvFvZwZXmajuT0GBE,1171
75
75
  tinybird/tb/modules/agent/models.py,sha256=LW1D27gjcd_jwFmghEzteCgToDfodX2B6B5S8BYbysw,735
76
- tinybird/tb/modules/agent/prompts.py,sha256=wbe6vUnm-fskceWgP13R5VW1v_YF7_wLDe-wBN6rlWw,6998
77
- tinybird/tb/modules/agent/utils.py,sha256=me5-kflBxV4N4psBSxI3vAfmFvAtsSnboc7ND3M9omw,26249
76
+ tinybird/tb/modules/agent/prompts.py,sha256=qyAv3H1x9qctlYQSel0DHxLlRJM2_8HTg7M-foSoR0k,17567
77
+ tinybird/tb/modules/agent/utils.py,sha256=5mUnc4LZATHLzQZThotNErzZdHQDwK3eur6W4NZULWA,26561
78
78
  tinybird/tb/modules/agent/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
79
- tinybird/tb/modules/agent/tools/append.py,sha256=cBdKBhUW0kooIbywtZNd_bhxbbMyf5NUKHwanAl5BmA,2042
79
+ tinybird/tb/modules/agent/tools/analyze.py,sha256=7oxJ3waCS24Qja_k5GUB59_XiHTG9pCewZogOXhH0cA,3495
80
+ tinybird/tb/modules/agent/tools/append.py,sha256=ekG7OxOzPjjjAjYUONAfp_PWlzyLT1GDfQhECgO-DYY,5065
80
81
  tinybird/tb/modules/agent/tools/build.py,sha256=LhzJMx6tbxC7gogIrxhfKJc-SDgoSR-FC6IunfaCdn8,758
81
82
  tinybird/tb/modules/agent/tools/create_datafile.py,sha256=wcPcChACTIFKw0lKFTlhm0sWJKhQkMLPLnGNpKyeETA,2962
82
83
  tinybird/tb/modules/agent/tools/deploy.py,sha256=WrsSlaufKGOBx0S13uoMQQH2DnKue5LQ231Rx4RXh2I,1443
83
84
  tinybird/tb/modules/agent/tools/deploy_check.py,sha256=LBE8aENYvCEaxbVTVKMSI2NGGiHeh-y60_MPlfrcvFk,1331
84
- tinybird/tb/modules/agent/tools/execute_query.py,sha256=s6QCIe8iD44_XZWgN-zYvWtXPEgJb2kGx6zgXCJbhKc,2642
85
+ tinybird/tb/modules/agent/tools/diff_resource.py,sha256=euKo_mD9FZHC1X6yESv3D_CXMzBMB8EWHLeZjIRqjJg,2637
86
+ tinybird/tb/modules/agent/tools/execute_query.py,sha256=hMRahWZkyP9qa-nMGzY0Z7MjDa9sb5O9V1evmA_V_w8,2702
85
87
  tinybird/tb/modules/agent/tools/explore.py,sha256=ihALc_kBcsjrKT3hZyicqyIowB0g_K3AtNNi-5uz9-8,412
86
- tinybird/tb/modules/agent/tools/get_endpoint_stats.py,sha256=_3wAvDykJitIOb5BRnP7wCy6y06y1qlULHLWB-MvS2M,1705
88
+ tinybird/tb/modules/agent/tools/get_endpoint_stats.py,sha256=E9yPi9LwnpsTyjFd8EaiSNvDGVPkFSNqp_tZxg_pWqs,1705
87
89
  tinybird/tb/modules/agent/tools/get_openapi_definition.py,sha256=9cQ-SUeB1NVhPJN1s8aQh9KQxqI9-DEEW1Ot5r2JbOk,1575
88
- tinybird/tb/modules/agent/tools/mock.py,sha256=Omog_gdEdm8YuBXNrJdHwxHqjL_ji9UIr75mALF4ozI,3408
90
+ tinybird/tb/modules/agent/tools/mock.py,sha256=4gEAPZCdTPo1w-fbryWiEx3hPXOK2ZfW0MpXY7smpcI,3408
89
91
  tinybird/tb/modules/agent/tools/plan.py,sha256=pr6LnItz6vlOeCG8GE459ExsrBEG0KLx-g02SZGNjXU,1217
90
- tinybird/tb/modules/agent/tools/preview_datafile.py,sha256=e9q5fR0afApcrntzFrnuHmd10ex7MG_GM6T0Pwc9bRI,850
91
- tinybird/tb/modules/agent/tools/read_fixture_data.py,sha256=rvTdVlZsu3rQTSWqXzpFt4LEwnBcMLIT8hlI5C7MVN4,1430
92
- tinybird/tb/modules/agent/tools/request_endpoint.py,sha256=iwzjYLtX_4YS7b9KzRwPGtVkx2UNujMAXv8m7mm8Fac,2683
92
+ tinybird/tb/modules/agent/tools/preview_datafile.py,sha256=Gbao_FxhXstnUnngVQxztpizjugyfx1rOXTkw7Yabls,858
93
+ tinybird/tb/modules/agent/tools/request_endpoint.py,sha256=Jl64ln0Jspu_rmp3ycZabj-2IXkmWFSZxoCdcavRpQo,2687
93
94
  tinybird/tb/modules/datafile/build.py,sha256=NFKBrusFLU0WJNCXePAFWiEDuTaXpwc0lHlOQWEJ43s,51117
94
95
  tinybird/tb/modules/datafile/build_common.py,sha256=2yNdxe49IMA9wNvl25NemY2Iaz8L66snjOdT64dm1is,4511
95
96
  tinybird/tb/modules/datafile/build_datasource.py,sha256=Ra8pVQBDafbFRUKlhpgohhTsRyp_ADKZJVG8Gd69idY,17227
@@ -110,8 +111,8 @@ tinybird/tb_cli_modules/config.py,sha256=IsgdtFRnUrkY8-Zo32lmk6O7u3bHie1QCxLwgp4
110
111
  tinybird/tb_cli_modules/exceptions.py,sha256=pmucP4kTF4irIt7dXiG-FcnI-o3mvDusPmch1L8RCWk,3367
111
112
  tinybird/tb_cli_modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
112
113
  tinybird/tb_cli_modules/telemetry.py,sha256=Hh2Io8ZPROSunbOLuMvuIFU4TqwWPmQTqal4WS09K1A,10449
113
- tinybird-0.0.1.dev253.dist-info/METADATA,sha256=Ej8SV3sx05hZNJilrr1lV6-89EarblPY7rwcLtp486M,1733
114
- tinybird-0.0.1.dev253.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
115
- tinybird-0.0.1.dev253.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
116
- tinybird-0.0.1.dev253.dist-info/top_level.txt,sha256=VqqqEmkAy7UNaD8-V51FCoMMWXjLUlR0IstvK7tJYVY,54
117
- tinybird-0.0.1.dev253.dist-info/RECORD,,
114
+ tinybird-0.0.1.dev255.dist-info/METADATA,sha256=oudst3Nwc1lfUUD-RID7T4ncAgcIuWnv5_VNlSjK08c,1733
115
+ tinybird-0.0.1.dev255.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
116
+ tinybird-0.0.1.dev255.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
117
+ tinybird-0.0.1.dev255.dist-info/top_level.txt,sha256=VqqqEmkAy7UNaD8-V51FCoMMWXjLUlR0IstvK7tJYVY,54
118
+ tinybird-0.0.1.dev255.dist-info/RECORD,,
@@ -1,36 +0,0 @@
1
- import json
2
- from pathlib import Path
3
-
4
- import click
5
- from pydantic_ai import RunContext
6
-
7
- from tinybird.tb.modules.agent.utils import TinybirdAgentContext
8
- from tinybird.tb.modules.feedback_manager import FeedbackManager
9
-
10
-
11
- def read_fixture_data(ctx: RunContext[TinybirdAgentContext], fixture_pathname: str):
12
- """Read fixture data in the project folder
13
-
14
- Args:
15
- fixture_pathname (str): a path to a fixture file. Required.
16
-
17
- Returns:
18
- str: The content of the fixture data file.
19
- """
20
- ctx.deps.thinking_animation.stop()
21
- click.echo(FeedbackManager.highlight(message=f"» Analyzing {fixture_pathname}..."))
22
- fixture_path = Path(ctx.deps.folder) / fixture_pathname.lstrip("/")
23
-
24
- if not fixture_path.exists():
25
- click.echo(FeedbackManager.error(message=f"No fixture data found for {fixture_pathname}."))
26
- ctx.deps.thinking_animation.start()
27
- return f"No fixture data found for {fixture_pathname}. Please check the name of the fixture and try again."
28
-
29
- response = ctx.deps.analyze_fixture(fixture_path=str(fixture_path))
30
- click.echo(FeedbackManager.success(message="✓ Done!\n"))
31
- ctx.deps.thinking_animation.start()
32
- # limit content to first 10 rows
33
- data = response["preview"]["data"][:10]
34
- columns = response["analysis"]["columns"]
35
-
36
- return f"#Result of analysis of {fixture_pathname}:\n##Columns:\n{json.dumps(columns)}\n##Data sample:\n{json.dumps(data)}"