tinybird 0.0.1.dev245__py3-none-any.whl → 0.0.1.dev246__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/tb/__cli__.py +2 -2
- tinybird/tb/modules/agent/agent.py +63 -20
- tinybird/tb/modules/agent/prompts.py +14 -14
- tinybird/tb/modules/agent/tools/build.py +19 -0
- tinybird/tb/modules/agent/tools/create_datafile.py +3 -3
- tinybird/tb/modules/agent/tools/deploy.py +45 -0
- tinybird/tb/modules/agent/tools/deploy_check.py +19 -0
- tinybird/tb/modules/agent/utils.py +5 -2
- tinybird/tb/modules/deployment.py +9 -381
- tinybird/tb/modules/deployment_common.py +413 -0
- tinybird/tb/modules/feedback_manager.py +7 -6
- {tinybird-0.0.1.dev245.dist-info → tinybird-0.0.1.dev246.dist-info}/METADATA +1 -1
- {tinybird-0.0.1.dev245.dist-info → tinybird-0.0.1.dev246.dist-info}/RECORD +16 -12
- {tinybird-0.0.1.dev245.dist-info → tinybird-0.0.1.dev246.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev245.dist-info → tinybird-0.0.1.dev246.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev245.dist-info → tinybird-0.0.1.dev246.dist-info}/top_level.txt +0 -0
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.dev246'
|
|
8
|
+
__revision__ = '5c11131'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import subprocess
|
|
1
2
|
import sys
|
|
2
|
-
import uuid
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from functools import partial
|
|
5
5
|
from typing import Any
|
|
@@ -35,12 +35,17 @@ from tinybird.tb.modules.agent.prompts import (
|
|
|
35
35
|
resources_prompt,
|
|
36
36
|
sql_instructions,
|
|
37
37
|
)
|
|
38
|
+
from tinybird.tb.modules.agent.tools.build import build
|
|
38
39
|
from tinybird.tb.modules.agent.tools.create_datafile import create_datafile
|
|
40
|
+
from tinybird.tb.modules.agent.tools.deploy import deploy
|
|
41
|
+
from tinybird.tb.modules.agent.tools.deploy_check import deploy_check
|
|
39
42
|
from tinybird.tb.modules.agent.tools.explore import explore_data
|
|
40
43
|
from tinybird.tb.modules.agent.tools.plan import plan
|
|
41
44
|
from tinybird.tb.modules.agent.tools.preview_datafile import preview_datafile
|
|
42
45
|
from tinybird.tb.modules.agent.utils import TinybirdAgentContext
|
|
43
46
|
from tinybird.tb.modules.build_common import process as build_process
|
|
47
|
+
from tinybird.tb.modules.common import _get_tb_client
|
|
48
|
+
from tinybird.tb.modules.deployment_common import create_deployment
|
|
44
49
|
from tinybird.tb.modules.exceptions import CLIBuildException
|
|
45
50
|
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
46
51
|
from tinybird.tb.modules.local_common import get_tinybird_local_client
|
|
@@ -86,7 +91,9 @@ You have access to the following tools:
|
|
|
86
91
|
2. `preview_datafile` - Preview the content of a datafile (datasource, endpoint, materialized, sink, copy, connection).
|
|
87
92
|
3. `create_datafile` - Create a file in the project folder. Confirmation will be asked by the tool before creating the file.
|
|
88
93
|
4. `plan` - Plan the creation or update of resources.
|
|
89
|
-
|
|
94
|
+
5. `build` - Build the project.
|
|
95
|
+
6. `deploy` - Deploy the project to Tinybird Cloud.
|
|
96
|
+
7. `deploy_check` - Check if the project can be deployed to Tinybird Cloud before deploying it.
|
|
90
97
|
|
|
91
98
|
# When creating or updating datafiles:
|
|
92
99
|
1. Use `plan` tool to plan the creation or update of resources.
|
|
@@ -140,6 +147,9 @@ Today is {datetime.now().strftime("%Y-%m-%d")}
|
|
|
140
147
|
Tool(preview_datafile, docstring_format="google", require_parameter_descriptions=True, takes_ctx=False),
|
|
141
148
|
Tool(create_datafile, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
142
149
|
Tool(plan, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
150
|
+
Tool(build, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
151
|
+
Tool(deploy, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
152
|
+
Tool(deploy_check, docstring_format="google", require_parameter_descriptions=True, takes_ctx=True),
|
|
143
153
|
],
|
|
144
154
|
)
|
|
145
155
|
|
|
@@ -147,22 +157,25 @@ Today is {datetime.now().strftime("%Y-%m-%d")}
|
|
|
147
157
|
"""Keep only the last 5 messages to manage token usage."""
|
|
148
158
|
return self.messages[-5:] if len(self.messages) > 5 else self.messages
|
|
149
159
|
|
|
150
|
-
def run(self, user_prompt: str, project: Project) -> None:
|
|
160
|
+
def run(self, user_prompt: str, config: dict[str, Any], project: Project) -> None:
|
|
151
161
|
user_prompt = f"{user_prompt}\n\n# Existing resources in the project:\n{resources_prompt(project)}"
|
|
152
162
|
client = TinyB(token=self.token, host=self.host)
|
|
153
163
|
folder = self.project.folder
|
|
164
|
+
|
|
154
165
|
thinking_animation = ThinkingAnimation(message="Chirping", delay=0.15)
|
|
155
166
|
thinking_animation.start()
|
|
156
|
-
|
|
157
167
|
result = self.agent.run_sync(
|
|
158
168
|
user_prompt,
|
|
159
169
|
deps=TinybirdAgentContext(
|
|
160
170
|
# context does not support the whole client, so we need to pass only the functions we need
|
|
161
171
|
explore_data=client.explore_data,
|
|
162
|
-
build_project=partial(build_project,
|
|
172
|
+
build_project=partial(build_project, project=project, config=config),
|
|
173
|
+
deploy_project=partial(deploy_project, project=project, config=config),
|
|
174
|
+
deploy_check_project=partial(deploy_check_project, project=project, config=config),
|
|
163
175
|
get_project_files=project.get_project_files,
|
|
164
176
|
folder=folder,
|
|
165
177
|
thinking_animation=thinking_animation,
|
|
178
|
+
workspace_name=self.project.workspace_name,
|
|
166
179
|
),
|
|
167
180
|
message_history=self.messages,
|
|
168
181
|
)
|
|
@@ -182,9 +195,11 @@ def run_agent(config: dict[str, Any], project: Project):
|
|
|
182
195
|
host = config["host"]
|
|
183
196
|
agent = TinybirdAgent(token, host, project)
|
|
184
197
|
click.echo()
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
198
|
+
if config.get("token"):
|
|
199
|
+
click.echo(FeedbackManager.info(message="Describe what you want to create and I'll help you build it"))
|
|
200
|
+
click.echo(FeedbackManager.info(message="Run /help for more commands"))
|
|
201
|
+
else:
|
|
202
|
+
click.echo(FeedbackManager.info(message="Run /login to authenticate"))
|
|
188
203
|
click.echo()
|
|
189
204
|
|
|
190
205
|
except Exception as e:
|
|
@@ -207,25 +222,29 @@ def run_agent(config: dict[str, Any], project: Project):
|
|
|
207
222
|
),
|
|
208
223
|
)
|
|
209
224
|
|
|
210
|
-
if user_input.lower() in ["exit", "quit"]:
|
|
225
|
+
if user_input.lower() in ["/exit", "/quit"]:
|
|
211
226
|
click.echo(FeedbackManager.info(message="Goodbye!"))
|
|
212
227
|
break
|
|
213
|
-
elif user_input.lower() == "clear":
|
|
228
|
+
elif user_input.lower() == "/clear":
|
|
214
229
|
clear_history()
|
|
215
230
|
continue
|
|
216
|
-
elif user_input.lower() == "
|
|
231
|
+
elif user_input.lower() == "/login":
|
|
232
|
+
click.echo()
|
|
233
|
+
subprocess.run(["tb", "login"], check=True)
|
|
234
|
+
click.echo()
|
|
235
|
+
continue
|
|
236
|
+
elif user_input.lower() == "/help":
|
|
217
237
|
click.echo()
|
|
218
|
-
click.echo(FeedbackManager.info(message="Tinybird Code Help:"))
|
|
219
238
|
click.echo("• Describe what you want to create: 'Create a user analytics system'")
|
|
220
239
|
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")
|
|
240
|
+
click.echo("• Connect to external services: 'Set up a Kafka connection for events'")
|
|
241
|
+
click.echo("• Type '/exit' or '/quit' to leave")
|
|
223
242
|
click.echo()
|
|
224
243
|
continue
|
|
225
244
|
elif user_input.strip() == "":
|
|
226
245
|
continue
|
|
227
246
|
else:
|
|
228
|
-
agent.run(user_input, project)
|
|
247
|
+
agent.run(user_input, config, project)
|
|
229
248
|
|
|
230
249
|
except KeyboardInterrupt:
|
|
231
250
|
click.echo(FeedbackManager.info(message="Goodbye!"))
|
|
@@ -239,10 +258,34 @@ def run_agent(config: dict[str, Any], project: Project):
|
|
|
239
258
|
sys.exit(1)
|
|
240
259
|
|
|
241
260
|
|
|
242
|
-
def build_project(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
261
|
+
def build_project(config: dict[str, Any], project: Project, silent: bool = True, test: bool = True) -> None:
|
|
262
|
+
local_client = get_tinybird_local_client(config, test=test, silent=silent)
|
|
263
|
+
build_error = build_process(
|
|
264
|
+
project=project, tb_client=local_client, watch=False, silent=silent, exit_on_error=False
|
|
265
|
+
)
|
|
247
266
|
if build_error:
|
|
248
267
|
raise CLIBuildException(build_error)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def deploy_project(config: dict[str, Any], project: Project) -> None:
|
|
271
|
+
client = _get_tb_client(config["token"], config["host"])
|
|
272
|
+
create_deployment(
|
|
273
|
+
project=project,
|
|
274
|
+
client=client,
|
|
275
|
+
config=config,
|
|
276
|
+
wait=True,
|
|
277
|
+
auto=True,
|
|
278
|
+
allow_destructive_operations=False,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def deploy_check_project(config: dict[str, Any], project: Project) -> None:
|
|
283
|
+
client = _get_tb_client(config["token"], config["host"])
|
|
284
|
+
create_deployment(
|
|
285
|
+
project=project,
|
|
286
|
+
client=client,
|
|
287
|
+
config=config,
|
|
288
|
+
check=True,
|
|
289
|
+
wait=True,
|
|
290
|
+
auto=True,
|
|
291
|
+
)
|
|
@@ -5,23 +5,23 @@ 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.
|
|
8
|
+
Plan description: [One sentence describing what will be built]
|
|
9
|
+
|
|
10
|
+
Steps:
|
|
11
|
+
1. Connection: [name] - [description] - Depends on: none
|
|
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
|
|
18
19
|
|
|
19
20
|
You can skip steps where resources will not be created or updated.
|
|
21
|
+
Always add BUILD_PROJECT step at the end of the plan.
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
RESOURCE_DEPENDENCIES:
|
|
24
|
-
[resource_name]: [resource_name]
|
|
23
|
+
Resource dependencies:
|
|
24
|
+
[resource_name]: [resources]
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
27
|
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
ctx.deps.build_project(test=False, silent=False)
|
|
13
|
+
ctx.deps.thinking_animation.start()
|
|
14
|
+
return "Project built successfully"
|
|
15
|
+
except Exception as e:
|
|
16
|
+
ctx.deps.thinking_animation.stop()
|
|
17
|
+
click.echo(FeedbackManager.error(message=e))
|
|
18
|
+
ctx.deps.thinking_animation.start()
|
|
19
|
+
return f"Error building project: {e}"
|
|
@@ -44,17 +44,17 @@ def create_datafile(ctx: RunContext[TinybirdAgentContext], resource: Datafile) -
|
|
|
44
44
|
path = Path(ctx.deps.folder) / resource.pathname
|
|
45
45
|
exists = str(path) in ctx.deps.get_project_files()
|
|
46
46
|
confirmation = get_resource_confirmation(resource, exists)
|
|
47
|
-
ctx.deps.thinking_animation.start()
|
|
48
47
|
|
|
49
48
|
if not confirmation:
|
|
49
|
+
ctx.deps.thinking_animation.start()
|
|
50
50
|
return f"Resource {resource.pathname} was not created. User cancelled creation."
|
|
51
51
|
|
|
52
52
|
folder_path = path.parent
|
|
53
53
|
folder_path.mkdir(parents=True, exist_ok=True)
|
|
54
54
|
path.touch(exist_ok=True)
|
|
55
|
-
|
|
56
55
|
path.write_text(resource.content)
|
|
57
|
-
ctx.deps.build_project()
|
|
56
|
+
ctx.deps.build_project(test=True, silent=True)
|
|
57
|
+
ctx.deps.thinking_animation.start()
|
|
58
58
|
return f"Created {resource.pathname}"
|
|
59
59
|
|
|
60
60
|
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 = 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}"
|
|
@@ -0,0 +1,19 @@
|
|
|
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 deploy_check(ctx: RunContext[TinybirdAgentContext]) -> str:
|
|
9
|
+
"""Check that project can be deployed"""
|
|
10
|
+
try:
|
|
11
|
+
ctx.deps.thinking_animation.stop()
|
|
12
|
+
ctx.deps.deploy_check_project()
|
|
13
|
+
ctx.deps.thinking_animation.start()
|
|
14
|
+
return "Project can be deployed"
|
|
15
|
+
except Exception as e:
|
|
16
|
+
ctx.deps.thinking_animation.stop()
|
|
17
|
+
click.echo(FeedbackManager.error(message=e))
|
|
18
|
+
ctx.deps.thinking_animation.start()
|
|
19
|
+
return f"Project cannot be deployed: {e}"
|
|
@@ -18,11 +18,14 @@ from pydantic import BaseModel, Field
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class TinybirdAgentContext(BaseModel):
|
|
21
|
-
explore_data: Callable[[str], str]
|
|
22
21
|
folder: str
|
|
23
|
-
|
|
22
|
+
workspace_name: str
|
|
24
23
|
thinking_animation: Any
|
|
25
24
|
get_project_files: Callable[[], List[str]]
|
|
25
|
+
explore_data: Callable[[str], str]
|
|
26
|
+
build_project: Callable[..., None]
|
|
27
|
+
deploy_project: Callable[[], None]
|
|
28
|
+
deploy_check_project: Callable[[], None]
|
|
26
29
|
|
|
27
30
|
|
|
28
31
|
default_style = Style.from_dict(
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
|
-
import sys
|
|
4
|
-
import time
|
|
5
3
|
from datetime import datetime
|
|
6
4
|
from pathlib import Path
|
|
7
|
-
from typing import Any, Dict, Optional
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
8
6
|
|
|
9
7
|
import click
|
|
10
8
|
import requests
|
|
@@ -12,10 +10,14 @@ import requests
|
|
|
12
10
|
from tinybird.tb.modules.cli import cli
|
|
13
11
|
from tinybird.tb.modules.common import (
|
|
14
12
|
echo_safe_humanfriendly_tables_format_smart_table,
|
|
15
|
-
get_display_cloud_host,
|
|
16
13
|
sys_exit,
|
|
17
14
|
)
|
|
18
|
-
from tinybird.tb.modules.
|
|
15
|
+
from tinybird.tb.modules.deployment_common import (
|
|
16
|
+
create_deployment,
|
|
17
|
+
discard_deployment,
|
|
18
|
+
promote_deployment,
|
|
19
|
+
)
|
|
20
|
+
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
19
21
|
from tinybird.tb.modules.project import Project
|
|
20
22
|
|
|
21
23
|
|
|
@@ -148,156 +150,6 @@ def api_fetch(url: str, headers: dict) -> dict:
|
|
|
148
150
|
return {}
|
|
149
151
|
|
|
150
152
|
|
|
151
|
-
def api_post(
|
|
152
|
-
url: str,
|
|
153
|
-
headers: dict,
|
|
154
|
-
files: Optional[list] = None,
|
|
155
|
-
params: Optional[dict] = None,
|
|
156
|
-
) -> dict:
|
|
157
|
-
r = requests.post(url, headers=headers, files=files, params=params)
|
|
158
|
-
if r.status_code < 300:
|
|
159
|
-
logging.debug(json.dumps(r.json(), indent=2))
|
|
160
|
-
return r.json()
|
|
161
|
-
# Try to parse and print the error from the response
|
|
162
|
-
try:
|
|
163
|
-
result = r.json()
|
|
164
|
-
logging.debug(json.dumps(result, indent=2))
|
|
165
|
-
error = result.get("error")
|
|
166
|
-
if error:
|
|
167
|
-
click.echo(FeedbackManager.error(message=f"Error: {error}"))
|
|
168
|
-
sys_exit("deployment_error", error)
|
|
169
|
-
return result
|
|
170
|
-
except Exception:
|
|
171
|
-
message = "Error parsing response from API"
|
|
172
|
-
click.echo(FeedbackManager.error(message=message))
|
|
173
|
-
sys_exit("deployment_error", message)
|
|
174
|
-
return {}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
# TODO(eclbg): This logic should be in the server, and there should be a dedicated endpoint for promoting a deployment
|
|
178
|
-
# potato
|
|
179
|
-
def promote_deployment(host: Optional[str], headers: dict, wait: bool) -> None:
|
|
180
|
-
TINYBIRD_API_URL = f"{host}/v1/deployments"
|
|
181
|
-
result = api_fetch(TINYBIRD_API_URL, headers)
|
|
182
|
-
|
|
183
|
-
deployments = result.get("deployments")
|
|
184
|
-
if not deployments:
|
|
185
|
-
message = "No deployments found"
|
|
186
|
-
click.echo(FeedbackManager.error(message=message))
|
|
187
|
-
sys_exit("deployment_error", message)
|
|
188
|
-
return
|
|
189
|
-
|
|
190
|
-
if len(deployments) < 2:
|
|
191
|
-
message = "Only one deployment found"
|
|
192
|
-
click.echo(FeedbackManager.error(message=message))
|
|
193
|
-
sys_exit("deployment_error", message)
|
|
194
|
-
return
|
|
195
|
-
|
|
196
|
-
last_deployment, candidate_deployment = deployments[0], deployments[1]
|
|
197
|
-
|
|
198
|
-
if candidate_deployment.get("status") != "data_ready":
|
|
199
|
-
click.echo(FeedbackManager.error(message="Current deployment is not ready"))
|
|
200
|
-
deploy_errors = candidate_deployment.get("errors", [])
|
|
201
|
-
for deploy_error in deploy_errors:
|
|
202
|
-
click.echo(FeedbackManager.error(message=f"* {deploy_error}"))
|
|
203
|
-
sys_exit("deployment_error", "Current deployment is not ready: " + str(deploy_errors))
|
|
204
|
-
return
|
|
205
|
-
|
|
206
|
-
if candidate_deployment.get("live"):
|
|
207
|
-
click.echo(FeedbackManager.error(message="Candidate deployment is already live"))
|
|
208
|
-
else:
|
|
209
|
-
TINYBIRD_API_URL = f"{host}/v1/deployments/{candidate_deployment.get('id')}/set-live"
|
|
210
|
-
result = api_post(TINYBIRD_API_URL, headers=headers)
|
|
211
|
-
|
|
212
|
-
click.echo(FeedbackManager.highlight(message="» Removing old deployment"))
|
|
213
|
-
|
|
214
|
-
TINYBIRD_API_URL = f"{host}/v1/deployments/{last_deployment.get('id')}"
|
|
215
|
-
r = requests.delete(TINYBIRD_API_URL, headers=headers)
|
|
216
|
-
result = r.json()
|
|
217
|
-
logging.debug(json.dumps(result, indent=2))
|
|
218
|
-
if result.get("error"):
|
|
219
|
-
click.echo(FeedbackManager.error(message=result.get("error")))
|
|
220
|
-
sys_exit("deployment_error", result.get("error", "Unknown error"))
|
|
221
|
-
click.echo(FeedbackManager.info(message="✓ Old deployment removed"))
|
|
222
|
-
|
|
223
|
-
click.echo(FeedbackManager.highlight(message="» Waiting for deployment to be promoted..."))
|
|
224
|
-
|
|
225
|
-
if wait:
|
|
226
|
-
while True:
|
|
227
|
-
TINYBIRD_API_URL = f"{host}/v1/deployments/{last_deployment.get('id')}"
|
|
228
|
-
result = api_fetch(TINYBIRD_API_URL, headers=headers)
|
|
229
|
-
|
|
230
|
-
last_deployment = result.get("deployment")
|
|
231
|
-
if last_deployment.get("status") == "deleted":
|
|
232
|
-
click.echo(FeedbackManager.success(message=f"✓ Deployment #{candidate_deployment.get('id')} is live!"))
|
|
233
|
-
break
|
|
234
|
-
|
|
235
|
-
time.sleep(5)
|
|
236
|
-
if last_deployment.get("id") == "0":
|
|
237
|
-
# This is the first deployment, so we prompt the user to ingest data
|
|
238
|
-
click.echo(
|
|
239
|
-
FeedbackManager.info(
|
|
240
|
-
message="A deployment with no data is useless. Learn how to ingest at https://www.tinybird.co/docs/forward/get-data-in"
|
|
241
|
-
)
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
# TODO(eclbg): This logic should be in the server, and there should be a dedicated endpoint for discarding a
|
|
246
|
-
# deployment
|
|
247
|
-
def discard_deployment(host: Optional[str], headers: dict, wait: bool) -> None:
|
|
248
|
-
TINYBIRD_API_URL = f"{host}/v1/deployments"
|
|
249
|
-
result = api_fetch(TINYBIRD_API_URL, headers=headers)
|
|
250
|
-
|
|
251
|
-
deployments = result.get("deployments")
|
|
252
|
-
if not deployments:
|
|
253
|
-
click.echo(FeedbackManager.error(message="No deployments found"))
|
|
254
|
-
return
|
|
255
|
-
|
|
256
|
-
if len(deployments) < 2:
|
|
257
|
-
click.echo(FeedbackManager.error(message="Only one deployment found"))
|
|
258
|
-
return
|
|
259
|
-
|
|
260
|
-
previous_deployment, current_deployment = deployments[0], deployments[1]
|
|
261
|
-
|
|
262
|
-
if previous_deployment.get("status") != "data_ready":
|
|
263
|
-
click.echo(FeedbackManager.error(message="Previous deployment is not ready"))
|
|
264
|
-
deploy_errors = previous_deployment.get("errors", [])
|
|
265
|
-
for deploy_error in deploy_errors:
|
|
266
|
-
click.echo(FeedbackManager.error(message=f"* {deploy_error}"))
|
|
267
|
-
return
|
|
268
|
-
|
|
269
|
-
if previous_deployment.get("live"):
|
|
270
|
-
click.echo(FeedbackManager.error(message="Previous deployment is already live"))
|
|
271
|
-
else:
|
|
272
|
-
click.echo(FeedbackManager.success(message="Promoting previous deployment"))
|
|
273
|
-
|
|
274
|
-
TINYBIRD_API_URL = f"{host}/v1/deployments/{previous_deployment.get('id')}/set-live"
|
|
275
|
-
result = api_post(TINYBIRD_API_URL, headers=headers)
|
|
276
|
-
|
|
277
|
-
click.echo(FeedbackManager.success(message="Removing current deployment"))
|
|
278
|
-
|
|
279
|
-
TINYBIRD_API_URL = f"{host}/v1/deployments/{current_deployment.get('id')}"
|
|
280
|
-
r = requests.delete(TINYBIRD_API_URL, headers=headers)
|
|
281
|
-
result = r.json()
|
|
282
|
-
logging.debug(json.dumps(result, indent=2))
|
|
283
|
-
if result.get("error"):
|
|
284
|
-
click.echo(FeedbackManager.error(message=result.get("error")))
|
|
285
|
-
sys_exit("deployment_error", result.get("error", "Unknown error"))
|
|
286
|
-
|
|
287
|
-
click.echo(FeedbackManager.success(message="Discard process successfully started"))
|
|
288
|
-
|
|
289
|
-
if wait:
|
|
290
|
-
while True:
|
|
291
|
-
TINYBIRD_API_URL = f"{host}/v1/deployments/{current_deployment.get('id')}"
|
|
292
|
-
result = api_fetch(TINYBIRD_API_URL, headers)
|
|
293
|
-
|
|
294
|
-
current_deployment = result.get("deployment")
|
|
295
|
-
if current_deployment.get("status") == "deleted":
|
|
296
|
-
click.echo(FeedbackManager.success(message="Discard process successfully completed"))
|
|
297
|
-
break
|
|
298
|
-
time.sleep(5)
|
|
299
|
-
|
|
300
|
-
|
|
301
153
|
@cli.group(name="deployment")
|
|
302
154
|
def deployment_group() -> None:
|
|
303
155
|
"""
|
|
@@ -482,8 +334,8 @@ def create_deployment_cmd(
|
|
|
482
334
|
allow_destructive_operations: Optional[bool] = None,
|
|
483
335
|
template: Optional[str] = None,
|
|
484
336
|
) -> None:
|
|
337
|
+
project: Project = ctx.ensure_object(dict)["project"]
|
|
485
338
|
if template:
|
|
486
|
-
project = ctx.ensure_object(dict)["project"]
|
|
487
339
|
if project.get_project_files():
|
|
488
340
|
click.echo(
|
|
489
341
|
FeedbackManager.error(
|
|
@@ -503,230 +355,6 @@ def create_deployment_cmd(
|
|
|
503
355
|
click.echo(FeedbackManager.error(message=f"Error downloading template: {str(e)}"))
|
|
504
356
|
sys_exit("deployment_error", f"Failed to download template {template}")
|
|
505
357
|
click.echo(FeedbackManager.success(message="Template downloaded successfully"))
|
|
506
|
-
|
|
507
|
-
create_deployment(ctx, wait, auto, check, allow_destructive_operations)
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
def create_deployment(
|
|
511
|
-
ctx: click.Context,
|
|
512
|
-
wait: bool,
|
|
513
|
-
auto: bool,
|
|
514
|
-
check: Optional[bool] = None,
|
|
515
|
-
allow_destructive_operations: Optional[bool] = None,
|
|
516
|
-
) -> None:
|
|
517
|
-
# TODO: This code is duplicated in build_server.py
|
|
518
|
-
# Should be refactored to be shared
|
|
519
|
-
MULTIPART_BOUNDARY_DATA_PROJECT = "data_project://"
|
|
520
|
-
DATAFILE_TYPE_TO_CONTENT_TYPE = {
|
|
521
|
-
".datasource": "text/plain",
|
|
522
|
-
".pipe": "text/plain",
|
|
523
|
-
".connection": "text/plain",
|
|
524
|
-
}
|
|
525
|
-
project: Project = ctx.ensure_object(dict)["project"]
|
|
526
358
|
client = ctx.ensure_object(dict)["client"]
|
|
527
359
|
config: Dict[str, Any] = ctx.ensure_object(dict)["config"]
|
|
528
|
-
|
|
529
|
-
TINYBIRD_API_KEY = client.token
|
|
530
|
-
|
|
531
|
-
if project.has_deeper_level():
|
|
532
|
-
click.echo(
|
|
533
|
-
FeedbackManager.warning(
|
|
534
|
-
message="\nYour project contains directories nested deeper than the default scan depth (max_depth=3). "
|
|
535
|
-
"Files in these deeper directories will not be processed. "
|
|
536
|
-
"To include all nested directories, run `tb --max-depth <depth> <cmd>` with a higher depth value."
|
|
537
|
-
)
|
|
538
|
-
)
|
|
539
|
-
|
|
540
|
-
files = [
|
|
541
|
-
("context://", ("cli-version", "1.0.0", "text/plain")),
|
|
542
|
-
]
|
|
543
|
-
for file_path in project.get_project_files():
|
|
544
|
-
relative_path = Path(file_path).relative_to(project.path).as_posix()
|
|
545
|
-
with open(file_path, "rb") as fd:
|
|
546
|
-
content_type = DATAFILE_TYPE_TO_CONTENT_TYPE.get(Path(file_path).suffix, "application/unknown")
|
|
547
|
-
files.append((MULTIPART_BOUNDARY_DATA_PROJECT, (relative_path, fd.read().decode("utf-8"), content_type)))
|
|
548
|
-
|
|
549
|
-
deployment = None
|
|
550
|
-
try:
|
|
551
|
-
HEADERS = {"Authorization": f"Bearer {TINYBIRD_API_KEY}"}
|
|
552
|
-
params = {}
|
|
553
|
-
if check:
|
|
554
|
-
click.echo(FeedbackManager.highlight(message="\n» Validating deployment...\n"))
|
|
555
|
-
params["check"] = "true"
|
|
556
|
-
if allow_destructive_operations:
|
|
557
|
-
params["allow_destructive_operations"] = "true"
|
|
558
|
-
|
|
559
|
-
result = api_post(TINYBIRD_API_URL, headers=HEADERS, files=files, params=params)
|
|
560
|
-
|
|
561
|
-
print_changes(result, project)
|
|
562
|
-
|
|
563
|
-
deployment = result.get("deployment", {})
|
|
564
|
-
feedback = deployment.get("feedback", [])
|
|
565
|
-
for f in feedback:
|
|
566
|
-
if f.get("level", "").upper() == "ERROR":
|
|
567
|
-
feedback_func = FeedbackManager.error
|
|
568
|
-
feedback_icon = ""
|
|
569
|
-
else:
|
|
570
|
-
feedback_func = FeedbackManager.warning
|
|
571
|
-
feedback_icon = "△ "
|
|
572
|
-
resource = f.get("resource")
|
|
573
|
-
resource_bit = f"{resource}: " if resource else ""
|
|
574
|
-
click.echo(feedback_func(message=f"{feedback_icon}{f.get('level')}: {resource_bit}{f.get('message')}"))
|
|
575
|
-
|
|
576
|
-
deploy_errors = deployment.get("errors")
|
|
577
|
-
for deploy_error in deploy_errors:
|
|
578
|
-
if deploy_error.get("filename", None):
|
|
579
|
-
click.echo(
|
|
580
|
-
FeedbackManager.error(message=f"{deploy_error.get('filename')}\n\n{deploy_error.get('error')}")
|
|
581
|
-
)
|
|
582
|
-
else:
|
|
583
|
-
click.echo(FeedbackManager.error(message=f"{deploy_error.get('error')}"))
|
|
584
|
-
click.echo("") # For spacing
|
|
585
|
-
|
|
586
|
-
status = result.get("result")
|
|
587
|
-
if check:
|
|
588
|
-
if status == "success":
|
|
589
|
-
click.echo(FeedbackManager.success(message="\n✓ Deployment is valid"))
|
|
590
|
-
sys.exit(0)
|
|
591
|
-
elif status == "no_changes":
|
|
592
|
-
sys.exit(0)
|
|
593
|
-
|
|
594
|
-
click.echo(FeedbackManager.error(message="\n✗ Deployment is not valid"))
|
|
595
|
-
sys_exit(
|
|
596
|
-
"deployment_error",
|
|
597
|
-
f"Deployment is not valid: {str(deployment.get('errors') + deployment.get('feedback', []))}",
|
|
598
|
-
)
|
|
599
|
-
|
|
600
|
-
status = result.get("result")
|
|
601
|
-
if status == "success":
|
|
602
|
-
host = get_display_cloud_host(client.host)
|
|
603
|
-
click.echo(
|
|
604
|
-
FeedbackManager.info(message="Deployment URL: ")
|
|
605
|
-
+ f"{bcolors.UNDERLINE}{host}/{config.get('name')}/deployments/{deployment.get('id')}{bcolors.ENDC}"
|
|
606
|
-
)
|
|
607
|
-
|
|
608
|
-
if wait:
|
|
609
|
-
click.echo(FeedbackManager.info(message="\n* Deployment submitted"))
|
|
610
|
-
else:
|
|
611
|
-
click.echo(FeedbackManager.success(message="\n✓ Deployment submitted successfully"))
|
|
612
|
-
elif status == "no_changes":
|
|
613
|
-
click.echo(FeedbackManager.warning(message="△ Not deploying. No changes."))
|
|
614
|
-
sys.exit(0)
|
|
615
|
-
elif status == "failed":
|
|
616
|
-
click.echo(FeedbackManager.error(message="Deployment failed"))
|
|
617
|
-
sys_exit(
|
|
618
|
-
"deployment_error",
|
|
619
|
-
f"Deployment failed. Errors: {str(deployment.get('errors') + deployment.get('feedback', []))}",
|
|
620
|
-
)
|
|
621
|
-
else:
|
|
622
|
-
click.echo(FeedbackManager.error(message=f"Unknown deployment result {status}"))
|
|
623
|
-
except Exception as e:
|
|
624
|
-
click.echo(FeedbackManager.error_exception(error=e))
|
|
625
|
-
|
|
626
|
-
if not deployment and not check:
|
|
627
|
-
sys_exit("deployment_error", "Deployment failed")
|
|
628
|
-
|
|
629
|
-
if deployment and wait and not check:
|
|
630
|
-
click.echo(FeedbackManager.highlight(message="» Waiting for deployment to be ready..."))
|
|
631
|
-
while True:
|
|
632
|
-
url = f"{client.host}/v1/deployments/{deployment.get('id')}"
|
|
633
|
-
res = api_fetch(url, HEADERS)
|
|
634
|
-
deployment = res.get("deployment")
|
|
635
|
-
if not deployment:
|
|
636
|
-
click.echo(FeedbackManager.error(message="Error parsing deployment from response"))
|
|
637
|
-
sys_exit("deployment_error", "Error parsing deployment from response")
|
|
638
|
-
if deployment.get("status") == "failed":
|
|
639
|
-
click.echo(FeedbackManager.error(message="Deployment failed"))
|
|
640
|
-
deploy_errors = deployment.get("errors")
|
|
641
|
-
for deploy_error in deploy_errors:
|
|
642
|
-
click.echo(FeedbackManager.error(message=f"* {deploy_error}"))
|
|
643
|
-
|
|
644
|
-
if auto:
|
|
645
|
-
click.echo(FeedbackManager.error(message="Rolling back deployment"))
|
|
646
|
-
discard_deployment(client.host, HEADERS, wait=wait)
|
|
647
|
-
sys_exit(
|
|
648
|
-
"deployment_error",
|
|
649
|
-
f"Deployment failed. Errors: {str(deployment.get('errors') + deployment.get('feedback', []))}",
|
|
650
|
-
)
|
|
651
|
-
|
|
652
|
-
if deployment.get("status") == "data_ready":
|
|
653
|
-
break
|
|
654
|
-
|
|
655
|
-
if deployment.get("status") in ["deleting", "deleted"]:
|
|
656
|
-
click.echo(FeedbackManager.error(message="Deployment was deleted by another process"))
|
|
657
|
-
sys_exit("deployment_error", "Deployment was deleted by another process")
|
|
658
|
-
|
|
659
|
-
time.sleep(5)
|
|
660
|
-
|
|
661
|
-
click.echo(FeedbackManager.info(message="✓ Deployment is ready"))
|
|
662
|
-
|
|
663
|
-
if auto:
|
|
664
|
-
promote_deployment(client.host, HEADERS, wait=wait)
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
def print_changes(result: dict, project: Project) -> None:
|
|
668
|
-
deployment = result.get("deployment", {})
|
|
669
|
-
resources_columns = ["status", "name", "type", "path"]
|
|
670
|
-
resources: list[list[Union[str, None]]] = []
|
|
671
|
-
tokens_columns = ["Change", "Token name", "Added permissions", "Removed permissions"]
|
|
672
|
-
tokens: list[Tuple[str, str, str, str]] = []
|
|
673
|
-
|
|
674
|
-
for ds in deployment.get("new_datasource_names", []):
|
|
675
|
-
resources.append(["new", ds, "datasource", project.get_resource_path(ds, "datasource")])
|
|
676
|
-
|
|
677
|
-
for p in deployment.get("new_pipe_names", []):
|
|
678
|
-
path = project.get_resource_path(p, "pipe")
|
|
679
|
-
pipe_type = project.get_pipe_type(path)
|
|
680
|
-
resources.append(["new", p, pipe_type, path])
|
|
681
|
-
|
|
682
|
-
for dc in deployment.get("new_data_connector_names", []):
|
|
683
|
-
resources.append(["new", dc, "connection", project.get_resource_path(dc, "connection")])
|
|
684
|
-
|
|
685
|
-
for ds in deployment.get("changed_datasource_names", []):
|
|
686
|
-
resources.append(["modified", ds, "datasource", project.get_resource_path(ds, "datasource")])
|
|
687
|
-
|
|
688
|
-
for p in deployment.get("changed_pipe_names", []):
|
|
689
|
-
path = project.get_resource_path(p, "pipe")
|
|
690
|
-
pipe_type = project.get_pipe_type(path)
|
|
691
|
-
resources.append(["modified", p, pipe_type, path])
|
|
692
|
-
|
|
693
|
-
for dc in deployment.get("changed_data_connector_names", []):
|
|
694
|
-
resources.append(["modified", dc, "connection", project.get_resource_path(dc, "connection")])
|
|
695
|
-
|
|
696
|
-
for ds in deployment.get("disconnected_data_source_names", []):
|
|
697
|
-
resources.append(["modified", ds, "datasource", project.get_resource_path(ds, "datasource")])
|
|
698
|
-
|
|
699
|
-
for ds in deployment.get("deleted_datasource_names", []):
|
|
700
|
-
resources.append(["deleted", ds, "datasource", project.get_resource_path(ds, "datasource")])
|
|
701
|
-
|
|
702
|
-
for p in deployment.get("deleted_pipe_names", []):
|
|
703
|
-
path = project.get_resource_path(p, "pipe")
|
|
704
|
-
pipe_type = project.get_pipe_type(path)
|
|
705
|
-
resources.append(["deleted", p, pipe_type, path])
|
|
706
|
-
|
|
707
|
-
for dc in deployment.get("deleted_data_connector_names", []):
|
|
708
|
-
resources.append(["deleted", dc, "connection", project.get_resource_path(dc, "connection")])
|
|
709
|
-
|
|
710
|
-
for token_change in deployment.get("token_changes", []):
|
|
711
|
-
token_name = token_change.get("token_name")
|
|
712
|
-
change_type = token_change.get("change_type")
|
|
713
|
-
added_perms = []
|
|
714
|
-
removed_perms = []
|
|
715
|
-
permission_changes = token_change.get("permission_changes", {})
|
|
716
|
-
for perm in permission_changes.get("added_permissions", []):
|
|
717
|
-
added_perms.append(f"{perm['resource_name']}.{perm['resource_type']}:{perm['permission']}")
|
|
718
|
-
for perm in permission_changes.get("removed_permissions", []):
|
|
719
|
-
removed_perms.append(f"{perm['resource_name']}.{perm['resource_type']}:{perm['permission']}")
|
|
720
|
-
|
|
721
|
-
tokens.append((change_type, token_name, "\n".join(added_perms), "\n".join(removed_perms)))
|
|
722
|
-
|
|
723
|
-
if resources:
|
|
724
|
-
click.echo(FeedbackManager.info(message="\n* Changes to be deployed:"))
|
|
725
|
-
echo_safe_humanfriendly_tables_format_smart_table(resources, column_names=resources_columns)
|
|
726
|
-
else:
|
|
727
|
-
click.echo(FeedbackManager.gray(message="\n* No changes to be deployed"))
|
|
728
|
-
if tokens:
|
|
729
|
-
click.echo(FeedbackManager.info(message="\n* Changes in tokens to be deployed:"))
|
|
730
|
-
echo_safe_humanfriendly_tables_format_smart_table(tokens, column_names=tokens_columns)
|
|
731
|
-
else:
|
|
732
|
-
click.echo(FeedbackManager.gray(message="* No changes in tokens to be deployed"))
|
|
360
|
+
create_deployment(project, client, config, wait, auto, check, allow_destructive_operations)
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, Optional, Tuple, Union
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from tinybird.tb.client import TinyB
|
|
12
|
+
from tinybird.tb.modules.common import (
|
|
13
|
+
echo_safe_humanfriendly_tables_format_smart_table,
|
|
14
|
+
get_display_cloud_host,
|
|
15
|
+
sys_exit,
|
|
16
|
+
)
|
|
17
|
+
from tinybird.tb.modules.feedback_manager import FeedbackManager, bcolors
|
|
18
|
+
from tinybird.tb.modules.project import Project
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# TODO(eclbg): This should eventually end up in client.py, but we're not using it here yet.
|
|
22
|
+
def api_fetch(url: str, headers: dict) -> dict:
|
|
23
|
+
r = requests.get(url, headers=headers)
|
|
24
|
+
if r.status_code == 200:
|
|
25
|
+
logging.debug(json.dumps(r.json(), indent=2))
|
|
26
|
+
return r.json()
|
|
27
|
+
# Try to parse and print the error from the response
|
|
28
|
+
try:
|
|
29
|
+
result = r.json()
|
|
30
|
+
error = result.get("error")
|
|
31
|
+
logging.debug(json.dumps(result, indent=2))
|
|
32
|
+
click.echo(FeedbackManager.error(message=f"Error: {error}"))
|
|
33
|
+
sys_exit("deployment_error", error)
|
|
34
|
+
except Exception:
|
|
35
|
+
message = "Error parsing response from API"
|
|
36
|
+
click.echo(FeedbackManager.error(message=message))
|
|
37
|
+
sys_exit("deployment_error", message)
|
|
38
|
+
return {}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def api_post(
|
|
42
|
+
url: str,
|
|
43
|
+
headers: dict,
|
|
44
|
+
files: Optional[list] = None,
|
|
45
|
+
params: Optional[dict] = None,
|
|
46
|
+
) -> dict:
|
|
47
|
+
r = requests.post(url, headers=headers, files=files, params=params)
|
|
48
|
+
if r.status_code < 300:
|
|
49
|
+
logging.debug(json.dumps(r.json(), indent=2))
|
|
50
|
+
return r.json()
|
|
51
|
+
# Try to parse and print the error from the response
|
|
52
|
+
try:
|
|
53
|
+
result = r.json()
|
|
54
|
+
logging.debug(json.dumps(result, indent=2))
|
|
55
|
+
error = result.get("error")
|
|
56
|
+
if error:
|
|
57
|
+
click.echo(FeedbackManager.error(message=f"Error: {error}"))
|
|
58
|
+
sys_exit("deployment_error", error)
|
|
59
|
+
return result
|
|
60
|
+
except Exception:
|
|
61
|
+
message = "Error parsing response from API"
|
|
62
|
+
click.echo(FeedbackManager.error(message=message))
|
|
63
|
+
sys_exit("deployment_error", message)
|
|
64
|
+
return {}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# TODO(eclbg): This logic should be in the server, and there should be a dedicated endpoint for promoting a deployment
|
|
68
|
+
# potato
|
|
69
|
+
def promote_deployment(host: Optional[str], headers: dict, wait: bool) -> None:
|
|
70
|
+
TINYBIRD_API_URL = f"{host}/v1/deployments"
|
|
71
|
+
result = api_fetch(TINYBIRD_API_URL, headers)
|
|
72
|
+
|
|
73
|
+
deployments = result.get("deployments")
|
|
74
|
+
if not deployments:
|
|
75
|
+
message = "No deployments found"
|
|
76
|
+
click.echo(FeedbackManager.error(message=message))
|
|
77
|
+
sys_exit("deployment_error", message)
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
if len(deployments) < 2:
|
|
81
|
+
message = "Only one deployment found"
|
|
82
|
+
click.echo(FeedbackManager.error(message=message))
|
|
83
|
+
sys_exit("deployment_error", message)
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
last_deployment, candidate_deployment = deployments[0], deployments[1]
|
|
87
|
+
|
|
88
|
+
if candidate_deployment.get("status") != "data_ready":
|
|
89
|
+
click.echo(FeedbackManager.error(message="Current deployment is not ready"))
|
|
90
|
+
deploy_errors = candidate_deployment.get("errors", [])
|
|
91
|
+
for deploy_error in deploy_errors:
|
|
92
|
+
click.echo(FeedbackManager.error(message=f"* {deploy_error}"))
|
|
93
|
+
sys_exit("deployment_error", "Current deployment is not ready: " + str(deploy_errors))
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
if candidate_deployment.get("live"):
|
|
97
|
+
click.echo(FeedbackManager.error(message="Candidate deployment is already live"))
|
|
98
|
+
else:
|
|
99
|
+
TINYBIRD_API_URL = f"{host}/v1/deployments/{candidate_deployment.get('id')}/set-live"
|
|
100
|
+
result = api_post(TINYBIRD_API_URL, headers=headers)
|
|
101
|
+
|
|
102
|
+
click.echo(FeedbackManager.highlight(message="» Removing old deployment"))
|
|
103
|
+
|
|
104
|
+
TINYBIRD_API_URL = f"{host}/v1/deployments/{last_deployment.get('id')}"
|
|
105
|
+
r = requests.delete(TINYBIRD_API_URL, headers=headers)
|
|
106
|
+
result = r.json()
|
|
107
|
+
logging.debug(json.dumps(result, indent=2))
|
|
108
|
+
if result.get("error"):
|
|
109
|
+
click.echo(FeedbackManager.error(message=result.get("error")))
|
|
110
|
+
sys_exit("deployment_error", result.get("error", "Unknown error"))
|
|
111
|
+
click.echo(FeedbackManager.info(message="✓ Old deployment removed"))
|
|
112
|
+
|
|
113
|
+
click.echo(FeedbackManager.highlight(message="» Waiting for deployment to be promoted..."))
|
|
114
|
+
|
|
115
|
+
if wait:
|
|
116
|
+
while True:
|
|
117
|
+
TINYBIRD_API_URL = f"{host}/v1/deployments/{last_deployment.get('id')}"
|
|
118
|
+
result = api_fetch(TINYBIRD_API_URL, headers=headers)
|
|
119
|
+
|
|
120
|
+
last_deployment = result.get("deployment")
|
|
121
|
+
if last_deployment.get("status") == "deleted":
|
|
122
|
+
click.echo(FeedbackManager.success(message=f"✓ Deployment #{candidate_deployment.get('id')} is live!"))
|
|
123
|
+
break
|
|
124
|
+
|
|
125
|
+
time.sleep(5)
|
|
126
|
+
if last_deployment.get("id") == "0":
|
|
127
|
+
# This is the first deployment, so we prompt the user to ingest data
|
|
128
|
+
click.echo(
|
|
129
|
+
FeedbackManager.info(
|
|
130
|
+
message="A deployment with no data is useless. Learn how to ingest at https://www.tinybird.co/docs/forward/get-data-in"
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# TODO(eclbg): This logic should be in the server, and there should be a dedicated endpoint for discarding a
|
|
136
|
+
# deployment
|
|
137
|
+
def discard_deployment(host: Optional[str], headers: dict, wait: bool) -> None:
|
|
138
|
+
TINYBIRD_API_URL = f"{host}/v1/deployments"
|
|
139
|
+
result = api_fetch(TINYBIRD_API_URL, headers=headers)
|
|
140
|
+
|
|
141
|
+
deployments = result.get("deployments")
|
|
142
|
+
if not deployments:
|
|
143
|
+
click.echo(FeedbackManager.error(message="No deployments found"))
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
if len(deployments) < 2:
|
|
147
|
+
click.echo(FeedbackManager.error(message="Only one deployment found"))
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
previous_deployment, current_deployment = deployments[0], deployments[1]
|
|
151
|
+
|
|
152
|
+
if previous_deployment.get("status") != "data_ready":
|
|
153
|
+
click.echo(FeedbackManager.error(message="Previous deployment is not ready"))
|
|
154
|
+
deploy_errors = previous_deployment.get("errors", [])
|
|
155
|
+
for deploy_error in deploy_errors:
|
|
156
|
+
click.echo(FeedbackManager.error(message=f"* {deploy_error}"))
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
if previous_deployment.get("live"):
|
|
160
|
+
click.echo(FeedbackManager.error(message="Previous deployment is already live"))
|
|
161
|
+
else:
|
|
162
|
+
click.echo(FeedbackManager.success(message="Promoting previous deployment"))
|
|
163
|
+
|
|
164
|
+
TINYBIRD_API_URL = f"{host}/v1/deployments/{previous_deployment.get('id')}/set-live"
|
|
165
|
+
result = api_post(TINYBIRD_API_URL, headers=headers)
|
|
166
|
+
|
|
167
|
+
click.echo(FeedbackManager.success(message="Removing current deployment"))
|
|
168
|
+
|
|
169
|
+
TINYBIRD_API_URL = f"{host}/v1/deployments/{current_deployment.get('id')}"
|
|
170
|
+
r = requests.delete(TINYBIRD_API_URL, headers=headers)
|
|
171
|
+
result = r.json()
|
|
172
|
+
logging.debug(json.dumps(result, indent=2))
|
|
173
|
+
if result.get("error"):
|
|
174
|
+
click.echo(FeedbackManager.error(message=result.get("error")))
|
|
175
|
+
sys_exit("deployment_error", result.get("error", "Unknown error"))
|
|
176
|
+
|
|
177
|
+
click.echo(FeedbackManager.success(message="Discard process successfully started"))
|
|
178
|
+
|
|
179
|
+
if wait:
|
|
180
|
+
while True:
|
|
181
|
+
TINYBIRD_API_URL = f"{host}/v1/deployments/{current_deployment.get('id')}"
|
|
182
|
+
result = api_fetch(TINYBIRD_API_URL, headers)
|
|
183
|
+
|
|
184
|
+
current_deployment = result.get("deployment")
|
|
185
|
+
if current_deployment.get("status") == "deleted":
|
|
186
|
+
click.echo(FeedbackManager.success(message="Discard process successfully completed"))
|
|
187
|
+
break
|
|
188
|
+
time.sleep(5)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def create_deployment(
|
|
192
|
+
project: Project,
|
|
193
|
+
client: TinyB,
|
|
194
|
+
config: Dict[str, Any],
|
|
195
|
+
wait: bool,
|
|
196
|
+
auto: bool,
|
|
197
|
+
check: Optional[bool] = None,
|
|
198
|
+
allow_destructive_operations: Optional[bool] = None,
|
|
199
|
+
) -> None:
|
|
200
|
+
# TODO: This code is duplicated in build_server.py
|
|
201
|
+
# Should be refactored to be shared
|
|
202
|
+
MULTIPART_BOUNDARY_DATA_PROJECT = "data_project://"
|
|
203
|
+
DATAFILE_TYPE_TO_CONTENT_TYPE = {
|
|
204
|
+
".datasource": "text/plain",
|
|
205
|
+
".pipe": "text/plain",
|
|
206
|
+
".connection": "text/plain",
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
TINYBIRD_API_URL = f"{client.host}/v1/deploy"
|
|
210
|
+
TINYBIRD_API_KEY = client.token
|
|
211
|
+
|
|
212
|
+
if project.has_deeper_level():
|
|
213
|
+
click.echo(
|
|
214
|
+
FeedbackManager.warning(
|
|
215
|
+
message="\nYour project contains directories nested deeper than the default scan depth (max_depth=3). "
|
|
216
|
+
"Files in these deeper directories will not be processed. "
|
|
217
|
+
"To include all nested directories, run `tb --max-depth <depth> <cmd>` with a higher depth value."
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
files = [
|
|
222
|
+
("context://", ("cli-version", "1.0.0", "text/plain")),
|
|
223
|
+
]
|
|
224
|
+
for file_path in project.get_project_files():
|
|
225
|
+
relative_path = Path(file_path).relative_to(project.path).as_posix()
|
|
226
|
+
with open(file_path, "rb") as fd:
|
|
227
|
+
content_type = DATAFILE_TYPE_TO_CONTENT_TYPE.get(Path(file_path).suffix, "application/unknown")
|
|
228
|
+
files.append((MULTIPART_BOUNDARY_DATA_PROJECT, (relative_path, fd.read().decode("utf-8"), content_type)))
|
|
229
|
+
|
|
230
|
+
deployment = None
|
|
231
|
+
try:
|
|
232
|
+
HEADERS = {"Authorization": f"Bearer {TINYBIRD_API_KEY}"}
|
|
233
|
+
params = {}
|
|
234
|
+
if check:
|
|
235
|
+
click.echo(FeedbackManager.highlight(message="\n» Validating deployment...\n"))
|
|
236
|
+
params["check"] = "true"
|
|
237
|
+
if allow_destructive_operations:
|
|
238
|
+
params["allow_destructive_operations"] = "true"
|
|
239
|
+
|
|
240
|
+
result = api_post(TINYBIRD_API_URL, headers=HEADERS, files=files, params=params)
|
|
241
|
+
|
|
242
|
+
print_changes(result, project)
|
|
243
|
+
|
|
244
|
+
deployment = result.get("deployment", {})
|
|
245
|
+
feedback = deployment.get("feedback", [])
|
|
246
|
+
for f in feedback:
|
|
247
|
+
if f.get("level", "").upper() == "ERROR":
|
|
248
|
+
feedback_func = FeedbackManager.error
|
|
249
|
+
feedback_icon = ""
|
|
250
|
+
else:
|
|
251
|
+
feedback_func = FeedbackManager.warning
|
|
252
|
+
feedback_icon = "△ "
|
|
253
|
+
resource = f.get("resource")
|
|
254
|
+
resource_bit = f"{resource}: " if resource else ""
|
|
255
|
+
click.echo(feedback_func(message=f"{feedback_icon}{f.get('level')}: {resource_bit}{f.get('message')}"))
|
|
256
|
+
|
|
257
|
+
deploy_errors = deployment.get("errors")
|
|
258
|
+
for deploy_error in deploy_errors:
|
|
259
|
+
if deploy_error.get("filename", None):
|
|
260
|
+
click.echo(
|
|
261
|
+
FeedbackManager.error(message=f"{deploy_error.get('filename')}\n\n{deploy_error.get('error')}")
|
|
262
|
+
)
|
|
263
|
+
else:
|
|
264
|
+
click.echo(FeedbackManager.error(message=f"{deploy_error.get('error')}"))
|
|
265
|
+
click.echo("") # For spacing
|
|
266
|
+
|
|
267
|
+
status = result.get("result")
|
|
268
|
+
if check:
|
|
269
|
+
if status == "success":
|
|
270
|
+
click.echo(FeedbackManager.success(message="\n✓ Deployment is valid"))
|
|
271
|
+
sys.exit(0)
|
|
272
|
+
elif status == "no_changes":
|
|
273
|
+
sys.exit(0)
|
|
274
|
+
|
|
275
|
+
click.echo(FeedbackManager.error(message="\n✗ Deployment is not valid"))
|
|
276
|
+
sys_exit(
|
|
277
|
+
"deployment_error",
|
|
278
|
+
f"Deployment is not valid: {str(deployment.get('errors') + deployment.get('feedback', []))}",
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
status = result.get("result")
|
|
282
|
+
if status == "success":
|
|
283
|
+
host = get_display_cloud_host(client.host)
|
|
284
|
+
click.echo(
|
|
285
|
+
FeedbackManager.info(message="Deployment URL: ")
|
|
286
|
+
+ f"{bcolors.UNDERLINE}{host}/{config.get('name')}/deployments/{deployment.get('id')}{bcolors.ENDC}"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
if wait:
|
|
290
|
+
click.echo(FeedbackManager.info(message="\n* Deployment submitted"))
|
|
291
|
+
else:
|
|
292
|
+
click.echo(FeedbackManager.success(message="\n✓ Deployment submitted successfully"))
|
|
293
|
+
elif status == "no_changes":
|
|
294
|
+
click.echo(FeedbackManager.warning(message="△ Not deploying. No changes."))
|
|
295
|
+
sys.exit(0)
|
|
296
|
+
elif status == "failed":
|
|
297
|
+
click.echo(FeedbackManager.error(message="Deployment failed"))
|
|
298
|
+
sys_exit(
|
|
299
|
+
"deployment_error",
|
|
300
|
+
f"Deployment failed. Errors: {str(deployment.get('errors') + deployment.get('feedback', []))}",
|
|
301
|
+
)
|
|
302
|
+
else:
|
|
303
|
+
click.echo(FeedbackManager.error(message=f"Unknown deployment result {status}"))
|
|
304
|
+
except Exception as e:
|
|
305
|
+
click.echo(FeedbackManager.error_exception(error=e))
|
|
306
|
+
|
|
307
|
+
if not deployment and not check:
|
|
308
|
+
sys_exit("deployment_error", "Deployment failed")
|
|
309
|
+
|
|
310
|
+
if deployment and wait and not check:
|
|
311
|
+
click.echo(FeedbackManager.highlight(message="» Waiting for deployment to be ready..."))
|
|
312
|
+
while True:
|
|
313
|
+
url = f"{client.host}/v1/deployments/{deployment.get('id')}"
|
|
314
|
+
res = api_fetch(url, HEADERS)
|
|
315
|
+
deployment = res.get("deployment")
|
|
316
|
+
if not deployment:
|
|
317
|
+
click.echo(FeedbackManager.error(message="Error parsing deployment from response"))
|
|
318
|
+
sys_exit("deployment_error", "Error parsing deployment from response")
|
|
319
|
+
if deployment.get("status") == "failed":
|
|
320
|
+
click.echo(FeedbackManager.error(message="Deployment failed"))
|
|
321
|
+
deploy_errors = deployment.get("errors")
|
|
322
|
+
for deploy_error in deploy_errors:
|
|
323
|
+
click.echo(FeedbackManager.error(message=f"* {deploy_error}"))
|
|
324
|
+
|
|
325
|
+
if auto:
|
|
326
|
+
click.echo(FeedbackManager.error(message="Rolling back deployment"))
|
|
327
|
+
discard_deployment(client.host, HEADERS, wait=wait)
|
|
328
|
+
sys_exit(
|
|
329
|
+
"deployment_error",
|
|
330
|
+
f"Deployment failed. Errors: {str(deployment.get('errors') + deployment.get('feedback', []))}",
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
if deployment.get("status") == "data_ready":
|
|
334
|
+
break
|
|
335
|
+
|
|
336
|
+
if deployment.get("status") in ["deleting", "deleted"]:
|
|
337
|
+
click.echo(FeedbackManager.error(message="Deployment was deleted by another process"))
|
|
338
|
+
sys_exit("deployment_error", "Deployment was deleted by another process")
|
|
339
|
+
|
|
340
|
+
time.sleep(5)
|
|
341
|
+
|
|
342
|
+
click.echo(FeedbackManager.info(message="✓ Deployment is ready"))
|
|
343
|
+
|
|
344
|
+
if auto:
|
|
345
|
+
promote_deployment(client.host, HEADERS, wait=wait)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def print_changes(result: dict, project: Project) -> None:
|
|
349
|
+
deployment = result.get("deployment", {})
|
|
350
|
+
resources_columns = ["status", "name", "type", "path"]
|
|
351
|
+
resources: list[list[Union[str, None]]] = []
|
|
352
|
+
tokens_columns = ["Change", "Token name", "Added permissions", "Removed permissions"]
|
|
353
|
+
tokens: list[Tuple[str, str, str, str]] = []
|
|
354
|
+
|
|
355
|
+
for ds in deployment.get("new_datasource_names", []):
|
|
356
|
+
resources.append(["new", ds, "datasource", project.get_resource_path(ds, "datasource")])
|
|
357
|
+
|
|
358
|
+
for p in deployment.get("new_pipe_names", []):
|
|
359
|
+
path = project.get_resource_path(p, "pipe")
|
|
360
|
+
pipe_type = project.get_pipe_type(path)
|
|
361
|
+
resources.append(["new", p, pipe_type, path])
|
|
362
|
+
|
|
363
|
+
for dc in deployment.get("new_data_connector_names", []):
|
|
364
|
+
resources.append(["new", dc, "connection", project.get_resource_path(dc, "connection")])
|
|
365
|
+
|
|
366
|
+
for ds in deployment.get("changed_datasource_names", []):
|
|
367
|
+
resources.append(["modified", ds, "datasource", project.get_resource_path(ds, "datasource")])
|
|
368
|
+
|
|
369
|
+
for p in deployment.get("changed_pipe_names", []):
|
|
370
|
+
path = project.get_resource_path(p, "pipe")
|
|
371
|
+
pipe_type = project.get_pipe_type(path)
|
|
372
|
+
resources.append(["modified", p, pipe_type, path])
|
|
373
|
+
|
|
374
|
+
for dc in deployment.get("changed_data_connector_names", []):
|
|
375
|
+
resources.append(["modified", dc, "connection", project.get_resource_path(dc, "connection")])
|
|
376
|
+
|
|
377
|
+
for ds in deployment.get("disconnected_data_source_names", []):
|
|
378
|
+
resources.append(["modified", ds, "datasource", project.get_resource_path(ds, "datasource")])
|
|
379
|
+
|
|
380
|
+
for ds in deployment.get("deleted_datasource_names", []):
|
|
381
|
+
resources.append(["deleted", ds, "datasource", project.get_resource_path(ds, "datasource")])
|
|
382
|
+
|
|
383
|
+
for p in deployment.get("deleted_pipe_names", []):
|
|
384
|
+
path = project.get_resource_path(p, "pipe")
|
|
385
|
+
pipe_type = project.get_pipe_type(path)
|
|
386
|
+
resources.append(["deleted", p, pipe_type, path])
|
|
387
|
+
|
|
388
|
+
for dc in deployment.get("deleted_data_connector_names", []):
|
|
389
|
+
resources.append(["deleted", dc, "connection", project.get_resource_path(dc, "connection")])
|
|
390
|
+
|
|
391
|
+
for token_change in deployment.get("token_changes", []):
|
|
392
|
+
token_name = token_change.get("token_name")
|
|
393
|
+
change_type = token_change.get("change_type")
|
|
394
|
+
added_perms = []
|
|
395
|
+
removed_perms = []
|
|
396
|
+
permission_changes = token_change.get("permission_changes", {})
|
|
397
|
+
for perm in permission_changes.get("added_permissions", []):
|
|
398
|
+
added_perms.append(f"{perm['resource_name']}.{perm['resource_type']}:{perm['permission']}")
|
|
399
|
+
for perm in permission_changes.get("removed_permissions", []):
|
|
400
|
+
removed_perms.append(f"{perm['resource_name']}.{perm['resource_type']}:{perm['permission']}")
|
|
401
|
+
|
|
402
|
+
tokens.append((change_type, token_name, "\n".join(added_perms), "\n".join(removed_perms)))
|
|
403
|
+
|
|
404
|
+
if resources:
|
|
405
|
+
click.echo(FeedbackManager.info(message="\n* Changes to be deployed:"))
|
|
406
|
+
echo_safe_humanfriendly_tables_format_smart_table(resources, column_names=resources_columns)
|
|
407
|
+
else:
|
|
408
|
+
click.echo(FeedbackManager.gray(message="\n* No changes to be deployed"))
|
|
409
|
+
if tokens:
|
|
410
|
+
click.echo(FeedbackManager.info(message="\n* Changes in tokens to be deployed:"))
|
|
411
|
+
echo_safe_humanfriendly_tables_format_smart_table(tokens, column_names=tokens_columns)
|
|
412
|
+
else:
|
|
413
|
+
click.echo(FeedbackManager.gray(message="* No changes in tokens to be deployed"))
|
|
@@ -631,12 +631,13 @@ STEP 2: CREATE GCP SERVICE ACCOUNT
|
|
|
631
631
|
1. Go to IAM & Admin > Service Accounts > + Create Service Account: https://console.cloud.google.com/iam-admin/serviceaccounts/create
|
|
632
632
|
2. Provide a service account name. Name the service account something meaningful (e.g., TinybirdGCS-{environment}-svc-account)
|
|
633
633
|
3. Click "Create and continue"
|
|
634
|
-
4. Click the "Select a role" drop down menu
|
|
635
|
-
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
634
|
+
4. Click the "Select a role" drop down menu:
|
|
635
|
+
- For Source (reading from GCS) select this role:
|
|
636
|
+
• "Storage Object Viewer" - Grants access to view objects and their metadata
|
|
637
|
+
- For Sink (writing to GCS) select all three roles:
|
|
638
|
+
• "Storage Object Creator" - Allows users to create objects
|
|
639
|
+
• "Storage Object Viewer" - Grants access to view objects and their metadata
|
|
640
|
+
• "Storage Bucket Viewer" - Grants access to view buckets and their metadata
|
|
640
641
|
(You can add IAM condition to provide access to selected buckets. More info in IAM Conditions: https://cloud.google.com/iam/docs/conditions-overview)
|
|
641
642
|
5. Click "Done"
|
|
642
643
|
"""
|
|
@@ -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=
|
|
20
|
+
tinybird/tb/__cli__.py,sha256=l43j0Pq0RUILb2kz7n3-G_Tbv8BEXXsW3BaMgzwCouk,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
|
|
@@ -32,12 +32,13 @@ tinybird/tb/modules/connection.py,sha256=-MY56NUAai6EMC4-wpi7bT0_nz_SA8QzTmHkV7H
|
|
|
32
32
|
tinybird/tb/modules/copy.py,sha256=dPZkcIDvxjJrlQUIvToO0vsEEEs4EYumbNV77-BzNoU,4404
|
|
33
33
|
tinybird/tb/modules/create.py,sha256=YYE9Bjqc000QGMmDnCG1UDTPs-Qeljr_RlGqM4RrPCA,23244
|
|
34
34
|
tinybird/tb/modules/datasource.py,sha256=cxq0VVjjidxq-v_JSIIAH7L90XNRctgNKsHRoQ_42OI,41632
|
|
35
|
-
tinybird/tb/modules/deployment.py,sha256=
|
|
35
|
+
tinybird/tb/modules/deployment.py,sha256=Fw9wSNqmLBGCpKwmZsn3KPsy-6kmQzI8YzSdXWoDb6k,12046
|
|
36
|
+
tinybird/tb/modules/deployment_common.py,sha256=Y0r3g-3d6AcihsVVa0OHer3ow3xHSV1VPskF1eI03KI,17644
|
|
36
37
|
tinybird/tb/modules/deprecations.py,sha256=rrszC1f_JJeJ8mUxGoCxckQTJFBCR8wREf4XXXN-PRc,4507
|
|
37
38
|
tinybird/tb/modules/dev_server.py,sha256=57FCKuWpErwYUYgHspYDkLWEm9F4pbvVOtMrFXX1fVU,10129
|
|
38
39
|
tinybird/tb/modules/endpoint.py,sha256=ksRj6mfDb9Xv63PhTkV_uKSosgysHElqagg3RTt21Do,11958
|
|
39
40
|
tinybird/tb/modules/exceptions.py,sha256=5jK91w1LPmtqIUfDpHe_Op5OxGz8-p1BPgtLREMIni0,5217
|
|
40
|
-
tinybird/tb/modules/feedback_manager.py,sha256=
|
|
41
|
+
tinybird/tb/modules/feedback_manager.py,sha256=Z8RyINWiPq_z-59oIZQW1qzFfHzU5JHbL09NVzhngb0,78029
|
|
41
42
|
tinybird/tb/modules/info.py,sha256=F5vY4kHS_kyO2uSBKac92HoOb447oDeRlzpwtAHTuKc,6872
|
|
42
43
|
tinybird/tb/modules/infra.py,sha256=JE9oLIyF4bi_JBoe-BgZ5HhKp_lQgSihuSV1KIS02Qs,32709
|
|
43
44
|
tinybird/tb/modules/job.py,sha256=wBsnu8UPTOha2rkLvucgmw4xYv73ubmui3eeSIF68ZM,3107
|
|
@@ -65,15 +66,18 @@ tinybird/tb/modules/watch.py,sha256=No0bK1M1_3CYuMaIgylxf7vYFJ72lTJe3brz6xQ-mJo,
|
|
|
65
66
|
tinybird/tb/modules/workspace.py,sha256=Q_8HcxMsNg8QG9aBlwcWS2umrDP5IkTIHqqz3sfmGuc,11341
|
|
66
67
|
tinybird/tb/modules/workspace_members.py,sha256=5JdkJgfuEwbq-t6vxkBhYwgsiTDxF790wsa6Xfif9nk,8608
|
|
67
68
|
tinybird/tb/modules/agent/__init__.py,sha256=i3oe3vDIWWPaicdCM0zs7D7BJ1W0k7th93ooskHAV00,54
|
|
68
|
-
tinybird/tb/modules/agent/agent.py,sha256=
|
|
69
|
+
tinybird/tb/modules/agent/agent.py,sha256=q6XMPQifrBpLFDja9SSSNZe2ExBjNETPzva3sn_ofsw,13713
|
|
69
70
|
tinybird/tb/modules/agent/animations.py,sha256=z0MNLf8TnUO8qAjgYvth_wc9a9283pNVz1Z4jl15Ggs,2558
|
|
70
71
|
tinybird/tb/modules/agent/banner.py,sha256=KX_e467uiy1gWOZ4ofTZt0GCFGQqHQ_8Ob27XLQqda0,3053
|
|
71
72
|
tinybird/tb/modules/agent/memory.py,sha256=H6SJK--2L5C87B7AJd_jMqsq3sCvFvZwZXmajuT0GBE,1171
|
|
72
73
|
tinybird/tb/modules/agent/models.py,sha256=mf8dRCdof6uEFZWh5xQ_D_FStk7eDds7qWRNSbDklUM,589
|
|
73
|
-
tinybird/tb/modules/agent/prompts.py,sha256=
|
|
74
|
-
tinybird/tb/modules/agent/utils.py,sha256=
|
|
74
|
+
tinybird/tb/modules/agent/prompts.py,sha256=rh0xqquzkwogdUmS9ychnKIcFT0YNzRPcZBZCug4Ow8,5760
|
|
75
|
+
tinybird/tb/modules/agent/utils.py,sha256=tLndW0MFtC9tS9Am1XfoYOvtPnrrumyMm22ZWVK6XNQ,13263
|
|
75
76
|
tinybird/tb/modules/agent/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
76
|
-
tinybird/tb/modules/agent/tools/
|
|
77
|
+
tinybird/tb/modules/agent/tools/build.py,sha256=oWrHrHU05JbE5RZY_EYEUcHiUVJlDQbNG8hhDxBuWGs,676
|
|
78
|
+
tinybird/tb/modules/agent/tools/create_datafile.py,sha256=zNqjrpMJFGXvto5KBRWy7Ek9jjLyS6EgjtrpmR4gc80,2383
|
|
79
|
+
tinybird/tb/modules/agent/tools/deploy.py,sha256=_xcP6F4ZIzvxgqi4jV_EkVy0UeHZtNWscGCAeWxvzqU,1402
|
|
80
|
+
tinybird/tb/modules/agent/tools/deploy_check.py,sha256=VqMYC7l3_cihmmM_pi8w1t8rJ3P0xDc7pHs_st9k-9Q,684
|
|
77
81
|
tinybird/tb/modules/agent/tools/explore.py,sha256=ihALc_kBcsjrKT3hZyicqyIowB0g_K3AtNNi-5uz9-8,412
|
|
78
82
|
tinybird/tb/modules/agent/tools/plan.py,sha256=CMSGrqjdVyhsJ0U1M5B2eRFLZXE7HqJ4K8tl1Ile0f0,1324
|
|
79
83
|
tinybird/tb/modules/agent/tools/preview_datafile.py,sha256=e9q5fR0afApcrntzFrnuHmd10ex7MG_GM6T0Pwc9bRI,850
|
|
@@ -97,8 +101,8 @@ tinybird/tb_cli_modules/config.py,sha256=IsgdtFRnUrkY8-Zo32lmk6O7u3bHie1QCxLwgp4
|
|
|
97
101
|
tinybird/tb_cli_modules/exceptions.py,sha256=pmucP4kTF4irIt7dXiG-FcnI-o3mvDusPmch1L8RCWk,3367
|
|
98
102
|
tinybird/tb_cli_modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
|
|
99
103
|
tinybird/tb_cli_modules/telemetry.py,sha256=Hh2Io8ZPROSunbOLuMvuIFU4TqwWPmQTqal4WS09K1A,10449
|
|
100
|
-
tinybird-0.0.1.
|
|
101
|
-
tinybird-0.0.1.
|
|
102
|
-
tinybird-0.0.1.
|
|
103
|
-
tinybird-0.0.1.
|
|
104
|
-
tinybird-0.0.1.
|
|
104
|
+
tinybird-0.0.1.dev246.dist-info/METADATA,sha256=io9aiUes21l9HWrsbDwhd88amU_HJnkKl6pleH0s5u4,1733
|
|
105
|
+
tinybird-0.0.1.dev246.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
106
|
+
tinybird-0.0.1.dev246.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
|
|
107
|
+
tinybird-0.0.1.dev246.dist-info/top_level.txt,sha256=VqqqEmkAy7UNaD8-V51FCoMMWXjLUlR0IstvK7tJYVY,54
|
|
108
|
+
tinybird-0.0.1.dev246.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|