tinybird 0.0.1.dev245__py3-none-any.whl → 0.0.1.dev247__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/ch_utils/constants.py +2 -0
- tinybird/tb/__cli__.py +2 -2
- tinybird/tb/modules/agent/agent.py +136 -27
- tinybird/tb/modules/agent/models.py +6 -0
- tinybird/tb/modules/agent/prompts.py +71 -43
- tinybird/tb/modules/agent/tools/append.py +55 -0
- tinybird/tb/modules/agent/tools/build.py +20 -0
- tinybird/tb/modules/agent/tools/create_datafile.py +84 -5
- tinybird/tb/modules/agent/tools/deploy.py +45 -0
- tinybird/tb/modules/agent/tools/deploy_check.py +19 -0
- tinybird/tb/modules/agent/tools/mock.py +59 -0
- tinybird/tb/modules/agent/tools/plan.py +1 -1
- tinybird/tb/modules/agent/tools/read_fixture_data.py +28 -0
- tinybird/tb/modules/agent/utils.py +9 -2
- tinybird/tb/modules/build.py +4 -1
- tinybird/tb/modules/build_common.py +2 -3
- tinybird/tb/modules/cli.py +9 -1
- tinybird/tb/modules/create.py +1 -1
- tinybird/tb/modules/deployment.py +9 -381
- tinybird/tb/modules/deployment_common.py +413 -0
- tinybird/tb/modules/feedback_manager.py +8 -6
- tinybird/tb/modules/llm.py +1 -1
- tinybird/tb/modules/mock.py +3 -69
- tinybird/tb/modules/mock_common.py +71 -0
- tinybird/tb/modules/project.py +9 -0
- {tinybird-0.0.1.dev245.dist-info → tinybird-0.0.1.dev247.dist-info}/METADATA +1 -1
- {tinybird-0.0.1.dev245.dist-info → tinybird-0.0.1.dev247.dist-info}/RECORD +30 -22
- {tinybird-0.0.1.dev245.dist-info → tinybird-0.0.1.dev247.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev245.dist-info → tinybird-0.0.1.dev247.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev245.dist-info → tinybird-0.0.1.dev247.dist-info}/top_level.txt +0 -0
tinybird/ch_utils/constants.py
CHANGED
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.dev247'
|
|
8
|
+
__revision__ = '379a827'
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import subprocess
|
|
1
2
|
import sys
|
|
2
|
-
import uuid
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from functools import partial
|
|
5
|
+
from pathlib import Path
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
7
8
|
import click
|
|
@@ -28,29 +29,40 @@ from tinybird.tb.client import TinyB
|
|
|
28
29
|
from tinybird.tb.modules.agent.animations import ThinkingAnimation
|
|
29
30
|
from tinybird.tb.modules.agent.banner import display_banner
|
|
30
31
|
from tinybird.tb.modules.agent.memory import clear_history, load_history
|
|
31
|
-
from tinybird.tb.modules.agent.models import create_model
|
|
32
|
+
from tinybird.tb.modules.agent.models import create_model, model_costs
|
|
32
33
|
from tinybird.tb.modules.agent.prompts import (
|
|
33
34
|
datafile_instructions,
|
|
34
35
|
plan_instructions,
|
|
35
36
|
resources_prompt,
|
|
36
37
|
sql_instructions,
|
|
37
38
|
)
|
|
39
|
+
from tinybird.tb.modules.agent.tools.append import append
|
|
40
|
+
from tinybird.tb.modules.agent.tools.build import build
|
|
38
41
|
from tinybird.tb.modules.agent.tools.create_datafile import create_datafile
|
|
42
|
+
from tinybird.tb.modules.agent.tools.deploy import deploy
|
|
43
|
+
from tinybird.tb.modules.agent.tools.deploy_check import deploy_check
|
|
39
44
|
from tinybird.tb.modules.agent.tools.explore import explore_data
|
|
45
|
+
from tinybird.tb.modules.agent.tools.mock import mock
|
|
40
46
|
from tinybird.tb.modules.agent.tools.plan import plan
|
|
41
47
|
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
|
|
42
49
|
from tinybird.tb.modules.agent.utils import TinybirdAgentContext
|
|
43
50
|
from tinybird.tb.modules.build_common import process as build_process
|
|
44
|
-
from tinybird.tb.modules.
|
|
51
|
+
from tinybird.tb.modules.common import _analyze, _get_tb_client
|
|
52
|
+
from tinybird.tb.modules.config import CLIConfig
|
|
53
|
+
from tinybird.tb.modules.deployment_common import create_deployment
|
|
54
|
+
from tinybird.tb.modules.exceptions import CLIBuildException, CLIMockException
|
|
45
55
|
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
46
56
|
from tinybird.tb.modules.local_common import get_tinybird_local_client
|
|
57
|
+
from tinybird.tb.modules.mock_common import append_mock_data, create_mock_data
|
|
47
58
|
from tinybird.tb.modules.project import Project
|
|
48
59
|
|
|
49
60
|
|
|
50
61
|
class TinybirdAgent:
|
|
51
|
-
def __init__(self, token: str, host: str, project: Project):
|
|
62
|
+
def __init__(self, token: str, host: str, project: Project, dangerously_skip_permissions: bool):
|
|
52
63
|
self.token = token
|
|
53
64
|
self.host = host
|
|
65
|
+
self.dangerously_skip_permissions = dangerously_skip_permissions
|
|
54
66
|
self.project = project
|
|
55
67
|
self.messages: list[ModelMessage] = []
|
|
56
68
|
self.agent = Agent(
|
|
@@ -86,7 +98,12 @@ You have access to the following tools:
|
|
|
86
98
|
2. `preview_datafile` - Preview the content of a datafile (datasource, endpoint, materialized, sink, copy, connection).
|
|
87
99
|
3. `create_datafile` - Create a file in the project folder. Confirmation will be asked by the tool before creating the file.
|
|
88
100
|
4. `plan` - Plan the creation or update of resources.
|
|
89
|
-
|
|
101
|
+
5. `build` - Build the project.
|
|
102
|
+
6. `deploy` - Deploy the project to Tinybird Cloud.
|
|
103
|
+
7. `deploy_check` - Check if the project can be deployed to Tinybird Cloud before deploying it.
|
|
104
|
+
8. `mock` - Create mock data for a landing datasource.
|
|
105
|
+
9. `read_fixture_data` - Read a fixture data file present in the project folder.
|
|
106
|
+
10. `append` - Append existing fixture to a datasource.
|
|
90
107
|
|
|
91
108
|
# When creating or updating datafiles:
|
|
92
109
|
1. Use `plan` tool to plan the creation or update of resources.
|
|
@@ -140,6 +157,12 @@ Today is {datetime.now().strftime("%Y-%m-%d")}
|
|
|
140
157
|
Tool(preview_datafile, docstring_format="google", require_parameter_descriptions=True, takes_ctx=False),
|
|
141
158
|
Tool(create_datafile, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
142
159
|
Tool(plan, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
160
|
+
Tool(build, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
161
|
+
Tool(deploy, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
162
|
+
Tool(deploy_check, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
163
|
+
Tool(mock, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
164
|
+
Tool(read_fixture_data, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
165
|
+
Tool(append, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
143
166
|
],
|
|
144
167
|
)
|
|
145
168
|
|
|
@@ -147,44 +170,65 @@ Today is {datetime.now().strftime("%Y-%m-%d")}
|
|
|
147
170
|
"""Keep only the last 5 messages to manage token usage."""
|
|
148
171
|
return self.messages[-5:] if len(self.messages) > 5 else self.messages
|
|
149
172
|
|
|
150
|
-
def run(self, user_prompt: str, project: Project) -> None:
|
|
151
|
-
user_prompt = f"{user_prompt}\n\n
|
|
173
|
+
def run(self, user_prompt: str, config: dict[str, Any], project: Project) -> None:
|
|
174
|
+
user_prompt = f"{user_prompt}\n\n{resources_prompt(project)}"
|
|
152
175
|
client = TinyB(token=self.token, host=self.host)
|
|
153
176
|
folder = self.project.folder
|
|
177
|
+
|
|
154
178
|
thinking_animation = ThinkingAnimation(message="Chirping", delay=0.15)
|
|
155
179
|
thinking_animation.start()
|
|
156
|
-
|
|
157
180
|
result = self.agent.run_sync(
|
|
158
181
|
user_prompt,
|
|
159
182
|
deps=TinybirdAgentContext(
|
|
160
183
|
# context does not support the whole client, so we need to pass only the functions we need
|
|
161
184
|
explore_data=client.explore_data,
|
|
162
|
-
build_project=partial(build_project,
|
|
185
|
+
build_project=partial(build_project, project=project, config=config),
|
|
186
|
+
deploy_project=partial(deploy_project, project=project, config=config),
|
|
187
|
+
deploy_check_project=partial(deploy_check_project, project=project, config=config),
|
|
188
|
+
mock_data=partial(mock_data, project=project, config=config),
|
|
189
|
+
append_data=partial(append_data, config=config),
|
|
190
|
+
analyze_fixture=partial(analyze_fixture, config=config),
|
|
163
191
|
get_project_files=project.get_project_files,
|
|
164
192
|
folder=folder,
|
|
165
193
|
thinking_animation=thinking_animation,
|
|
194
|
+
workspace_name=self.project.workspace_name,
|
|
195
|
+
dangerously_skip_permissions=self.dangerously_skip_permissions,
|
|
166
196
|
),
|
|
167
197
|
message_history=self.messages,
|
|
168
198
|
)
|
|
169
199
|
new_messages = result.new_messages()
|
|
170
200
|
self.messages.extend(new_messages)
|
|
171
201
|
thinking_animation.stop()
|
|
172
|
-
|
|
202
|
+
usage = result.usage()
|
|
203
|
+
request_tokens = usage.request_tokens or 0
|
|
204
|
+
response_tokens = usage.response_tokens or 0
|
|
205
|
+
total_tokens = usage.total_tokens or 0
|
|
206
|
+
cost = (
|
|
207
|
+
request_tokens * model_costs["input_cost_per_token"]
|
|
208
|
+
+ response_tokens * model_costs["output_cost_per_token"]
|
|
209
|
+
)
|
|
210
|
+
click.echo()
|
|
173
211
|
click.echo(result.output)
|
|
174
212
|
click.echo("\n")
|
|
213
|
+
click.echo(f"Input tokens: {request_tokens}")
|
|
214
|
+
click.echo(f"Output tokens: {response_tokens}")
|
|
215
|
+
click.echo(f"Total tokens: {total_tokens}")
|
|
216
|
+
click.echo(f"Cost: ${cost:.6f}")
|
|
175
217
|
|
|
176
218
|
|
|
177
|
-
def run_agent(config: dict[str, Any], project: Project):
|
|
219
|
+
def run_agent(config: dict[str, Any], project: Project, dangerously_skip_permissions: bool):
|
|
178
220
|
display_banner()
|
|
179
221
|
|
|
180
222
|
try:
|
|
181
223
|
token = config["token"]
|
|
182
224
|
host = config["host"]
|
|
183
|
-
agent = TinybirdAgent(token, host, project)
|
|
225
|
+
agent = TinybirdAgent(token, host, project, dangerously_skip_permissions)
|
|
184
226
|
click.echo()
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
227
|
+
if config.get("token"):
|
|
228
|
+
click.echo(FeedbackManager.info(message="Describe what you want to create and I'll help you build it"))
|
|
229
|
+
click.echo(FeedbackManager.info(message="Run /help for more commands"))
|
|
230
|
+
else:
|
|
231
|
+
click.echo(FeedbackManager.info(message="Run /login to authenticate"))
|
|
188
232
|
click.echo()
|
|
189
233
|
|
|
190
234
|
except Exception as e:
|
|
@@ -207,25 +251,29 @@ def run_agent(config: dict[str, Any], project: Project):
|
|
|
207
251
|
),
|
|
208
252
|
)
|
|
209
253
|
|
|
210
|
-
if user_input.lower() in ["exit", "quit"]:
|
|
254
|
+
if user_input.lower() in ["/exit", "/quit"]:
|
|
211
255
|
click.echo(FeedbackManager.info(message="Goodbye!"))
|
|
212
256
|
break
|
|
213
|
-
elif user_input.lower() == "clear":
|
|
257
|
+
elif user_input.lower() == "/clear":
|
|
214
258
|
clear_history()
|
|
215
259
|
continue
|
|
216
|
-
elif user_input.lower() == "
|
|
260
|
+
elif user_input.lower() == "/login":
|
|
261
|
+
click.echo()
|
|
262
|
+
subprocess.run(["tb", "login"], check=True)
|
|
263
|
+
click.echo()
|
|
264
|
+
continue
|
|
265
|
+
elif user_input.lower() == "/help":
|
|
217
266
|
click.echo()
|
|
218
|
-
click.echo(FeedbackManager.info(message="Tinybird Code Help:"))
|
|
219
267
|
click.echo("• Describe what you want to create: 'Create a user analytics system'")
|
|
220
268
|
click.echo("• Ask for specific resources: 'Create a pipe to aggregate daily clicks'")
|
|
221
|
-
click.echo("•
|
|
222
|
-
click.echo("• Type 'exit' or 'quit' to leave")
|
|
269
|
+
click.echo("• Connect to external services: 'Set up a Kafka connection for events'")
|
|
270
|
+
click.echo("• Type '/exit' or '/quit' to leave")
|
|
223
271
|
click.echo()
|
|
224
272
|
continue
|
|
225
273
|
elif user_input.strip() == "":
|
|
226
274
|
continue
|
|
227
275
|
else:
|
|
228
|
-
agent.run(user_input, project)
|
|
276
|
+
agent.run(user_input, config, project)
|
|
229
277
|
|
|
230
278
|
except KeyboardInterrupt:
|
|
231
279
|
click.echo(FeedbackManager.info(message="Goodbye!"))
|
|
@@ -239,10 +287,71 @@ def run_agent(config: dict[str, Any], project: Project):
|
|
|
239
287
|
sys.exit(1)
|
|
240
288
|
|
|
241
289
|
|
|
242
|
-
def build_project(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
290
|
+
def build_project(config: dict[str, Any], project: Project, silent: bool = True, test: bool = True) -> None:
|
|
291
|
+
local_client = get_tinybird_local_client(config, test=test, silent=silent)
|
|
292
|
+
build_error = build_process(
|
|
293
|
+
project=project, tb_client=local_client, watch=False, silent=silent, exit_on_error=False
|
|
294
|
+
)
|
|
247
295
|
if build_error:
|
|
248
296
|
raise CLIBuildException(build_error)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def deploy_project(config: dict[str, Any], project: Project) -> None:
|
|
300
|
+
client = _get_tb_client(config["token"], config["host"])
|
|
301
|
+
create_deployment(
|
|
302
|
+
project=project,
|
|
303
|
+
client=client,
|
|
304
|
+
config=config,
|
|
305
|
+
wait=True,
|
|
306
|
+
auto=True,
|
|
307
|
+
allow_destructive_operations=False,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def deploy_check_project(config: dict[str, Any], project: Project) -> None:
|
|
312
|
+
client = _get_tb_client(config["token"], config["host"])
|
|
313
|
+
create_deployment(
|
|
314
|
+
project=project,
|
|
315
|
+
client=client,
|
|
316
|
+
config=config,
|
|
317
|
+
check=True,
|
|
318
|
+
wait=True,
|
|
319
|
+
auto=True,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def append_data(config: dict[str, Any], datasource_name: str, path: str) -> None:
|
|
324
|
+
client = get_tinybird_local_client(config, test=False, silent=False)
|
|
325
|
+
append_mock_data(client, datasource_name, path)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def mock_data(
|
|
329
|
+
config: dict[str, Any], project: Project, datasource_name: str, data_format: str, rows: int
|
|
330
|
+
) -> list[dict[str, Any]]:
|
|
331
|
+
client = get_tinybird_local_client(config, test=False, silent=False)
|
|
332
|
+
cli_config = CLIConfig.get_project_config()
|
|
333
|
+
datasource_path = project.get_resource_path(datasource_name, "datasource")
|
|
334
|
+
|
|
335
|
+
if not datasource_path:
|
|
336
|
+
raise CLIMockException(f"Datasource {datasource_name} not found")
|
|
337
|
+
|
|
338
|
+
datasource_content = Path(datasource_path).read_text()
|
|
339
|
+
prompt = ""
|
|
340
|
+
return create_mock_data(
|
|
341
|
+
datasource_name,
|
|
342
|
+
datasource_content,
|
|
343
|
+
rows,
|
|
344
|
+
prompt,
|
|
345
|
+
cli_config,
|
|
346
|
+
config,
|
|
347
|
+
cli_config.get_user_token() or "",
|
|
348
|
+
client,
|
|
349
|
+
data_format,
|
|
350
|
+
project.folder,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def analyze_fixture(config: dict[str, Any], fixture_path: str) -> dict[str, Any]:
|
|
355
|
+
local_client = get_tinybird_local_client(config, test=False, silent=True)
|
|
356
|
+
meta, _data = _analyze(fixture_path, local_client, Path(fixture_path).suffix.lstrip("."))
|
|
357
|
+
return meta
|
|
@@ -5,23 +5,29 @@ from tinybird.tb.modules.project import Project
|
|
|
5
5
|
plan_instructions = """
|
|
6
6
|
When asked to create a plan, you MUST respond with this EXACT format and NOTHING ELSE:
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
1.
|
|
12
|
-
2.
|
|
13
|
-
3.
|
|
14
|
-
4.
|
|
15
|
-
5.
|
|
16
|
-
6.
|
|
17
|
-
7.
|
|
18
|
-
|
|
8
|
+
Plan description: [One sentence describing what will be built]
|
|
9
|
+
|
|
10
|
+
Steps:
|
|
11
|
+
1. Connection: [name] - [description]
|
|
12
|
+
2. Datasource: [name] - [description] - Depends on: [connection_name (optional)]
|
|
13
|
+
3. Endpoint: [name] - [description] - Depends on: [resources]
|
|
14
|
+
4. Materialized pipe: [name] - [description] - Depends on: [resources]
|
|
15
|
+
5. Materialized datasource: [name] - [description] - Depends on: [resources]
|
|
16
|
+
6. Sink: [name] - [description] - Depends on: [resources]
|
|
17
|
+
7. Copy: [name] - [description] - Depends on: [resources]
|
|
18
|
+
8. Build project
|
|
19
|
+
9. Generate mock data: [datasource_name]
|
|
20
|
+
10. Append existing fixture: [fixture_pathname] - Target: [datasource_name]
|
|
21
|
+
|
|
22
|
+
<dev_notes>
|
|
19
23
|
You can skip steps where resources will not be created or updated.
|
|
24
|
+
Always add 'Build project' step after generating resources.
|
|
25
|
+
Always add 'Generate mock data' step after building project if a landing datasource was created.
|
|
26
|
+
Always add 'Append existing fixture' step after building project if a fixture file was provided at the beginning of the plan.
|
|
27
|
+
</dev_notes>
|
|
20
28
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
RESOURCE_DEPENDENCIES:
|
|
24
|
-
[resource_name]: [resource_name]
|
|
29
|
+
Resource dependencies:
|
|
30
|
+
[resource_name]: [resources]
|
|
25
31
|
"""
|
|
26
32
|
|
|
27
33
|
|
|
@@ -89,31 +95,53 @@ datafile_instructions = """
|
|
|
89
95
|
|
|
90
96
|
def resources_prompt(project: Project) -> str:
|
|
91
97
|
files = project.get_project_files()
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
98
|
+
fixture_files = project.get_fixture_files()
|
|
99
|
+
|
|
100
|
+
resources_content = "# Existing resources in the project:\n"
|
|
101
|
+
if files:
|
|
102
|
+
paths = [Path(file_path) for file_path in files]
|
|
103
|
+
|
|
104
|
+
resources_content += "\n".join(
|
|
105
|
+
[
|
|
106
|
+
f"""
|
|
107
|
+
<resource>
|
|
108
|
+
<path>{file_path.relative_to(project.folder)}</path>
|
|
109
|
+
<type>{get_resource_type(file_path)}</type>
|
|
110
|
+
<name>{file_path.stem}</name>
|
|
111
|
+
<content>{file_path.read_text()}</content>
|
|
112
|
+
</resource>
|
|
113
|
+
"""
|
|
114
|
+
for file_path in paths
|
|
115
|
+
]
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
resources_content += "No resources found"
|
|
119
|
+
|
|
120
|
+
fixture_content = "# Fixture files in the project:\n"
|
|
121
|
+
if fixture_files:
|
|
122
|
+
paths = [Path(file_path) for file_path in fixture_files]
|
|
123
|
+
fixture_content += "\n".join(
|
|
124
|
+
[
|
|
125
|
+
f"""
|
|
126
|
+
<fixture>
|
|
127
|
+
<path>{file_path.relative_to(project.folder)}</path>
|
|
128
|
+
<name>{file_path.stem}</name>
|
|
129
|
+
</fixture>
|
|
130
|
+
"""
|
|
131
|
+
for file_path in paths
|
|
132
|
+
]
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
fixture_content += "No fixture files found"
|
|
136
|
+
|
|
137
|
+
return resources_content + "\n" + fixture_content
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def get_resource_type(path: Path) -> str:
|
|
141
|
+
if path.suffix.lower() == ".pipe":
|
|
142
|
+
return Project.get_pipe_type(str(path))
|
|
143
|
+
elif path.suffix.lower() == ".datasource":
|
|
144
|
+
return "datasource"
|
|
145
|
+
elif path.suffix.lower() == ".connection":
|
|
146
|
+
return "connection"
|
|
147
|
+
return "unknown"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from pydantic_ai import RunContext
|
|
3
|
+
|
|
4
|
+
from tinybird.tb.modules.agent.utils import TinybirdAgentContext, show_options
|
|
5
|
+
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_append_confirmation(datasource_name: str) -> bool:
|
|
9
|
+
"""Get user confirmation for appending existing fixture"""
|
|
10
|
+
while True:
|
|
11
|
+
result = show_options(
|
|
12
|
+
options=["Yes, append existing fixture", "No, and tell Tinybird Code what to do"],
|
|
13
|
+
title=f"Do you want to append existing fixture for datasource {datasource_name}?",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
if result is None: # Cancelled
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
if result.startswith("Yes"):
|
|
20
|
+
return True
|
|
21
|
+
elif result.startswith("No"):
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def append(ctx: RunContext[TinybirdAgentContext], datasource_name: str, fixture_pathname: str) -> str:
|
|
28
|
+
"""Append existing fixture to a datasource
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
datasource_name: Name of the datasource to append fixture to
|
|
32
|
+
fixture_pathname: Path to the fixture file to append
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
str: Message indicating the success or failure of the appending
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
ctx.deps.thinking_animation.stop()
|
|
39
|
+
confirmation = ctx.deps.dangerously_skip_permissions or get_append_confirmation(datasource_name)
|
|
40
|
+
ctx.deps.thinking_animation.start()
|
|
41
|
+
|
|
42
|
+
if not confirmation:
|
|
43
|
+
return "User rejected appending existing fixture. Skipping..."
|
|
44
|
+
|
|
45
|
+
ctx.deps.thinking_animation.stop()
|
|
46
|
+
click.echo(FeedbackManager.highlight(message=f"\n» Appending {fixture_pathname} to {datasource_name}..."))
|
|
47
|
+
ctx.deps.append_data(datasource_name=datasource_name, path=fixture_pathname)
|
|
48
|
+
click.echo(FeedbackManager.success(message=f"✓ Data appended to {datasource_name}"))
|
|
49
|
+
ctx.deps.thinking_animation.start()
|
|
50
|
+
return f"Data appended to {datasource_name}"
|
|
51
|
+
except Exception as e:
|
|
52
|
+
ctx.deps.thinking_animation.stop()
|
|
53
|
+
click.echo(FeedbackManager.error(message=e))
|
|
54
|
+
ctx.deps.thinking_animation.start()
|
|
55
|
+
return f"Error appending fixture {fixture_pathname} to {datasource_name}: {e}"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from pydantic_ai import RunContext
|
|
3
|
+
|
|
4
|
+
from tinybird.tb.modules.agent.utils import TinybirdAgentContext
|
|
5
|
+
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build(ctx: RunContext[TinybirdAgentContext]) -> str:
|
|
9
|
+
"""Build the project"""
|
|
10
|
+
try:
|
|
11
|
+
ctx.deps.thinking_animation.stop()
|
|
12
|
+
click.echo(FeedbackManager.highlight(message="\n» Building project..."))
|
|
13
|
+
ctx.deps.build_project(test=False, silent=False)
|
|
14
|
+
ctx.deps.thinking_animation.start()
|
|
15
|
+
return "Project built successfully"
|
|
16
|
+
except Exception as e:
|
|
17
|
+
ctx.deps.thinking_animation.stop()
|
|
18
|
+
click.echo(FeedbackManager.error(message=e))
|
|
19
|
+
ctx.deps.thinking_animation.start()
|
|
20
|
+
return f"Error building project: {e}"
|
|
@@ -1,5 +1,18 @@
|
|
|
1
|
+
import difflib
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
|
|
4
|
+
try:
|
|
5
|
+
from colorama import Back, Fore, Style, init
|
|
6
|
+
|
|
7
|
+
init()
|
|
8
|
+
except ImportError: # fallback so that the imported classes always exist
|
|
9
|
+
|
|
10
|
+
class ColorFallback:
|
|
11
|
+
def __getattr__(self, name):
|
|
12
|
+
return ""
|
|
13
|
+
|
|
14
|
+
Fore = Back = Style = ColorFallback()
|
|
15
|
+
|
|
3
16
|
import click
|
|
4
17
|
from pydantic_ai import RunContext
|
|
5
18
|
|
|
@@ -8,6 +21,67 @@ from tinybird.tb.modules.exceptions import CLIBuildException
|
|
|
8
21
|
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
9
22
|
|
|
10
23
|
|
|
24
|
+
def create_line_numbered_diff(original_content: str, new_content: str, filename: str) -> str:
|
|
25
|
+
"""Create a diff with line numbers similar to the example format"""
|
|
26
|
+
original_lines = original_content.splitlines()
|
|
27
|
+
new_lines = new_content.splitlines()
|
|
28
|
+
|
|
29
|
+
# Create a SequenceMatcher to find the differences
|
|
30
|
+
matcher = difflib.SequenceMatcher(None, original_lines, new_lines)
|
|
31
|
+
|
|
32
|
+
result = []
|
|
33
|
+
result.append(f"╭{'─' * 88}╮")
|
|
34
|
+
result.append(f"│ {filename:<86} │")
|
|
35
|
+
result.append(f"│{' ' * 88}│")
|
|
36
|
+
|
|
37
|
+
# Process the opcodes to build the diff
|
|
38
|
+
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
|
|
39
|
+
if tag == "equal":
|
|
40
|
+
# Show context lines
|
|
41
|
+
for i, line in enumerate(original_lines[i1:i2]):
|
|
42
|
+
line_num = i1 + i + 1
|
|
43
|
+
result.append(f"│ {line_num:4} {line:<74} │")
|
|
44
|
+
elif tag == "replace":
|
|
45
|
+
# Show removed lines
|
|
46
|
+
for i, line in enumerate(original_lines[i1:i2]):
|
|
47
|
+
line_num = i1 + i + 1
|
|
48
|
+
result.append(f"│ {Back.RED}{line_num:4} - {line:<74}{Back.RESET} │")
|
|
49
|
+
# Show added lines
|
|
50
|
+
for i, line in enumerate(new_lines[j1:j2]):
|
|
51
|
+
line_num = i1 + i + 1
|
|
52
|
+
result.append(f"│ {Back.GREEN}{line_num:4} + {line:<74}{Back.RESET} │")
|
|
53
|
+
elif tag == "delete":
|
|
54
|
+
# Show removed lines
|
|
55
|
+
for i, line in enumerate(original_lines[i1:i2]):
|
|
56
|
+
line_num = i1 + i + 1
|
|
57
|
+
result.append(f"│ {Back.RED}{line_num:4} - {line:<74}{Back.RESET} │")
|
|
58
|
+
elif tag == "insert":
|
|
59
|
+
# Show added lines
|
|
60
|
+
for i, line in enumerate(new_lines[j1:j2]):
|
|
61
|
+
# Use the line number from the original position
|
|
62
|
+
line_num = i1 + i + 1
|
|
63
|
+
result.append(f"│ {Back.GREEN}{line_num:4} + {line:<74}{Back.RESET} │")
|
|
64
|
+
|
|
65
|
+
result.append(f"╰{'─' * 88}╯")
|
|
66
|
+
return "\n".join(result)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def create_line_numbered_content(content: str, filename: str) -> str:
|
|
70
|
+
"""Create a formatted display of file content with line numbers"""
|
|
71
|
+
lines = content.splitlines()
|
|
72
|
+
|
|
73
|
+
result = []
|
|
74
|
+
result.append(f"╭{'─' * 88}╮")
|
|
75
|
+
result.append(f"│ {filename:<86} │")
|
|
76
|
+
result.append(f"│{' ' * 88}│")
|
|
77
|
+
|
|
78
|
+
for i, line in enumerate(lines, 1):
|
|
79
|
+
result.append(f"│ {i:4} {line:<74} │")
|
|
80
|
+
|
|
81
|
+
result.append(f"╰{'─' * 88}╯")
|
|
82
|
+
return "\n".join(result)
|
|
83
|
+
|
|
84
|
+
|
|
11
85
|
def get_resource_confirmation(resource: Datafile, exists: bool) -> bool:
|
|
12
86
|
"""Get user confirmation for creating a resource"""
|
|
13
87
|
while True:
|
|
@@ -39,22 +113,27 @@ def create_datafile(ctx: RunContext[TinybirdAgentContext], resource: Datafile) -
|
|
|
39
113
|
"""
|
|
40
114
|
try:
|
|
41
115
|
ctx.deps.thinking_animation.stop()
|
|
42
|
-
click.echo(resource.content)
|
|
43
116
|
resource.pathname = resource.pathname.removeprefix("/")
|
|
44
117
|
path = Path(ctx.deps.folder) / resource.pathname
|
|
118
|
+
content = resource.content
|
|
45
119
|
exists = str(path) in ctx.deps.get_project_files()
|
|
46
|
-
|
|
47
|
-
|
|
120
|
+
if exists:
|
|
121
|
+
content = create_line_numbered_diff(path.read_text(), resource.content, resource.pathname)
|
|
122
|
+
else:
|
|
123
|
+
content = create_line_numbered_content(resource.content, resource.pathname)
|
|
124
|
+
click.echo(content)
|
|
125
|
+
confirmation = ctx.deps.dangerously_skip_permissions or get_resource_confirmation(resource, exists)
|
|
48
126
|
|
|
49
127
|
if not confirmation:
|
|
128
|
+
ctx.deps.thinking_animation.start()
|
|
50
129
|
return f"Resource {resource.pathname} was not created. User cancelled creation."
|
|
51
130
|
|
|
52
131
|
folder_path = path.parent
|
|
53
132
|
folder_path.mkdir(parents=True, exist_ok=True)
|
|
54
133
|
path.touch(exist_ok=True)
|
|
55
|
-
|
|
56
134
|
path.write_text(resource.content)
|
|
57
|
-
ctx.deps.build_project()
|
|
135
|
+
ctx.deps.build_project(test=True, silent=True)
|
|
136
|
+
ctx.deps.thinking_animation.start()
|
|
58
137
|
return f"Created {resource.pathname}"
|
|
59
138
|
|
|
60
139
|
except CLIBuildException as e:
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from pydantic_ai import RunContext
|
|
3
|
+
|
|
4
|
+
from tinybird.tb.modules.agent.utils import TinybirdAgentContext, show_options
|
|
5
|
+
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_deploy_confirmation() -> bool:
|
|
9
|
+
"""Get user confirmation for deploying the project"""
|
|
10
|
+
while True:
|
|
11
|
+
result = show_options(
|
|
12
|
+
options=["Yes, deploy the project", "No, and tell Tinybird Code what to do"],
|
|
13
|
+
title="Do you want to deploy the project?",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
if result is None: # Cancelled
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
if result.startswith("Yes"):
|
|
20
|
+
return True
|
|
21
|
+
elif result.startswith("No"):
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def deploy(ctx: RunContext[TinybirdAgentContext]) -> str:
|
|
28
|
+
"""Deploy the project"""
|
|
29
|
+
try:
|
|
30
|
+
ctx.deps.thinking_animation.stop()
|
|
31
|
+
confirmation = ctx.deps.dangerously_skip_permissions or get_deploy_confirmation()
|
|
32
|
+
ctx.deps.thinking_animation.start()
|
|
33
|
+
|
|
34
|
+
if not confirmation:
|
|
35
|
+
return "User cancelled deployment."
|
|
36
|
+
|
|
37
|
+
ctx.deps.thinking_animation.stop()
|
|
38
|
+
ctx.deps.deploy_project()
|
|
39
|
+
ctx.deps.thinking_animation.start()
|
|
40
|
+
return "Project deployed successfully"
|
|
41
|
+
except Exception as e:
|
|
42
|
+
ctx.deps.thinking_animation.stop()
|
|
43
|
+
click.echo(FeedbackManager.error(message=e))
|
|
44
|
+
ctx.deps.thinking_animation.start()
|
|
45
|
+
return f"Error depoying project: {e}"
|