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 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.dev245'
8
- __revision__ = 'f0c439a'
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, folder=folder),
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
- click.echo(FeedbackManager.success(message="Welcome to Tinybird Code"))
186
- click.echo(FeedbackManager.info(message="Describe what you want to create and I'll help you build it"))
187
- click.echo(FeedbackManager.info(message="Commands: 'exit', 'quit', 'help', or Ctrl+C to exit"))
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() == "help":
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("• Request data sources: 'Set up a Kafka connection for events'")
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(folder: str) -> None:
243
- workspace_name = f"tmp_workspace_{uuid.uuid4()}"
244
- project = Project(folder, workspace_name=workspace_name)
245
- local_client = get_tinybird_local_client({"path": folder, "name": workspace_name}, test=True, silent=True)
246
- build_error = build_process(project=project, tb_client=local_client, watch=False, silent=True, exit_on_error=False)
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
- PLAN_DESCRIPTION: [One sentence describing what will be built]
9
-
10
- STEPS:
11
- 1. CREATE_DATASOURCE: [name] - [description] - DEPENDS_ON: none
12
- 2. CREATE_ENDPOINT: [name] - [description] - DEPENDS_ON: [resource_name]
13
- 3. CREATE_MATERIALIZED_PIPE: [name] - [description] - DEPENDS_ON: [resource_name]
14
- 4. CREATE_MATERIALIZED_DATASOURCE: [name] - [description] - DEPENDS_ON: [resource_name]
15
- 5. CREATE_SINK: [name] - [description] - DEPENDS_ON: [resource_name]
16
- 6. CREATE_COPY: [name] - [description] - DEPENDS_ON: [resource_name]
17
- 7. CREATE_CONNECTION: [name] - [description] - DEPENDS_ON: none
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
- ESTIMATED_RESOURCES: [total number of steps]
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
- build_project: Callable[[], None]
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, Tuple, Union
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.feedback_manager import FeedbackManager, bcolors
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
- TINYBIRD_API_URL = f"{client.host}/v1/deploy"
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 and select:
635
- - "Storage Object Viewer" for GCS Data Source (reading from GCS)
636
- - For GCS Sink (writing to GCS), select all three roles:
637
- "Storage Object Creator" - Allows users to create objects
638
- • "Storage Object Viewer" - Grants access to view objects and their metadata
639
- • "Storage Bucket Viewer" - Grants access to view buckets and their metadata
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
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: tinybird
3
- Version: 0.0.1.dev245
3
+ Version: 0.0.1.dev246
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/forward/commands
6
6
  Author: Tinybird
@@ -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=lO7Woz0azArhNCFKAWijw8uJo4jrQDbI-rUCxK_YeO0,247
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=ByXIgEvwxB49pJEKKj0EJIfORWyflCYr04k8961nBkA,28391
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=5N2S_ymq0nJPQcFetzoQOWfR6hhx8_gaTp318pe76zU,77966
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=h0va31JtVzwxaQ6kD1V8cwclNhpbzJeJbtKOps5MGIg,11864
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=rAbcqkrw7BKVuV2sNRGpbLXxfo95suKWRLPwgNx7fdM,5784
74
- tinybird/tb/modules/agent/utils.py,sha256=SSfAmPTO-pv4UtRrsAbERHjjeqZ3Mkx6Y_eRD5QXr9M,13154
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/create_datafile.py,sha256=e7xMaPziVw5JPaQ8rmeEnNz6SCkR3MP1ZBvvXb6QOwE,2314
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.dev245.dist-info/METADATA,sha256=1Qqly3uB1KyENHgniVShFyw6tTeJRz7UZWntvH9AR24,1733
101
- tinybird-0.0.1.dev245.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
102
- tinybird-0.0.1.dev245.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
103
- tinybird-0.0.1.dev245.dist-info/top_level.txt,sha256=VqqqEmkAy7UNaD8-V51FCoMMWXjLUlR0IstvK7tJYVY,54
104
- tinybird-0.0.1.dev245.dist-info/RECORD,,
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,,