tinybird 0.0.1.dev306__py3-none-any.whl → 1.0.5__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.
Files changed (45) hide show
  1. tinybird/datafile/common.py +4 -1
  2. tinybird/feedback_manager.py +3 -0
  3. tinybird/service_datasources.py +57 -8
  4. tinybird/sql_template.py +1 -1
  5. tinybird/sql_template_fmt.py +14 -4
  6. tinybird/tb/__cli__.py +2 -2
  7. tinybird/tb/cli.py +1 -0
  8. tinybird/tb/client.py +104 -22
  9. tinybird/tb/modules/agent/tools/execute_query.py +1 -1
  10. tinybird/tb/modules/agent/tools/request_endpoint.py +1 -1
  11. tinybird/tb/modules/branch.py +150 -0
  12. tinybird/tb/modules/build.py +51 -10
  13. tinybird/tb/modules/build_common.py +4 -2
  14. tinybird/tb/modules/cli.py +32 -10
  15. tinybird/tb/modules/common.py +161 -134
  16. tinybird/tb/modules/connection.py +125 -194
  17. tinybird/tb/modules/connection_kafka.py +382 -0
  18. tinybird/tb/modules/copy.py +3 -1
  19. tinybird/tb/modules/create.py +11 -0
  20. tinybird/tb/modules/datafile/build.py +1 -1
  21. tinybird/tb/modules/datafile/format_pipe.py +44 -5
  22. tinybird/tb/modules/datafile/playground.py +1 -1
  23. tinybird/tb/modules/datasource.py +475 -324
  24. tinybird/tb/modules/deployment.py +2 -0
  25. tinybird/tb/modules/deployment_common.py +81 -43
  26. tinybird/tb/modules/deprecations.py +4 -4
  27. tinybird/tb/modules/dev_server.py +33 -12
  28. tinybird/tb/modules/info.py +50 -7
  29. tinybird/tb/modules/job_common.py +15 -0
  30. tinybird/tb/modules/local.py +91 -21
  31. tinybird/tb/modules/local_common.py +320 -13
  32. tinybird/tb/modules/local_logs.py +209 -0
  33. tinybird/tb/modules/login.py +3 -2
  34. tinybird/tb/modules/login_common.py +252 -9
  35. tinybird/tb/modules/open.py +10 -5
  36. tinybird/tb/modules/project.py +14 -5
  37. tinybird/tb/modules/shell.py +14 -6
  38. tinybird/tb/modules/sink.py +3 -1
  39. tinybird/tb/modules/telemetry.py +7 -3
  40. tinybird/tb_cli_modules/telemetry.py +1 -1
  41. {tinybird-0.0.1.dev306.dist-info → tinybird-1.0.5.dist-info}/METADATA +29 -4
  42. {tinybird-0.0.1.dev306.dist-info → tinybird-1.0.5.dist-info}/RECORD +45 -41
  43. {tinybird-0.0.1.dev306.dist-info → tinybird-1.0.5.dist-info}/WHEEL +1 -1
  44. {tinybird-0.0.1.dev306.dist-info → tinybird-1.0.5.dist-info}/entry_points.txt +0 -0
  45. {tinybird-0.0.1.dev306.dist-info → tinybird-1.0.5.dist-info}/top_level.txt +0 -0
@@ -382,6 +382,7 @@ def create_deployment_cmd(
382
382
  template: Optional[str] = None,
383
383
  verbose: bool = False,
384
384
  ) -> None:
385
+ output = ctx.ensure_object(dict)["output"]
385
386
  project: Project = ctx.ensure_object(dict)["project"]
386
387
  if template:
387
388
  if project.get_project_files():
@@ -416,6 +417,7 @@ def create_deployment_cmd(
416
417
  check,
417
418
  allow_destructive_operations,
418
419
  ingest_hint=not is_web_analytics_starter_kit,
420
+ output=output,
419
421
  )
420
422
  show_web_analytics_starter_kit_hints(client, is_web_analytics_starter_kit)
421
423
 
@@ -10,11 +10,13 @@ import requests
10
10
 
11
11
  from tinybird.tb.client import TinyB
12
12
  from tinybird.tb.modules.common import (
13
+ echo_json,
13
14
  echo_safe_humanfriendly_tables_format_smart_table,
14
15
  get_display_cloud_host,
15
16
  sys_exit,
16
17
  )
17
18
  from tinybird.tb.modules.feedback_manager import FeedbackManager, bcolors
19
+ from tinybird.tb.modules.job_common import echo_job_url
18
20
  from tinybird.tb.modules.project import Project
19
21
 
20
22
 
@@ -59,9 +61,14 @@ def api_post(
59
61
  params: Optional[dict] = None,
60
62
  ) -> dict:
61
63
  r = requests.post(url, headers=headers, files=files, params=params)
62
- if r.status_code < 300:
63
- logging.debug(json.dumps(r.json(), indent=2))
64
- return r.json()
64
+ try:
65
+ if r.status_code < 300:
66
+ logging.debug(json.dumps(r.json(), indent=2))
67
+ return r.json()
68
+ except json.JSONDecodeError:
69
+ message = "Error parsing response from API"
70
+ click.echo(FeedbackManager.error(message=message))
71
+ sys_exit("deployment_error", message)
65
72
 
66
73
  # Try to parse and print the error from the response
67
74
  try:
@@ -152,8 +159,6 @@ def promote_deployment(host: Optional[str], headers: dict, wait: bool, ingest_hi
152
159
  )
153
160
 
154
161
 
155
- # TODO(eclbg): This logic should be in the server, and there should be a dedicated endpoint for discarding a
156
- # deployment
157
162
  def discard_deployment(host: Optional[str], headers: dict, wait: bool) -> None:
158
163
  TINYBIRD_API_URL = f"{host}/v1/deployments"
159
164
  result = api_fetch(TINYBIRD_API_URL, headers=headers)
@@ -164,29 +169,25 @@ def discard_deployment(host: Optional[str], headers: dict, wait: bool) -> None:
164
169
  return
165
170
 
166
171
  if len(deployments) < 2:
167
- click.echo(FeedbackManager.error(message="Only one deployment found"))
172
+ click.echo(FeedbackManager.error(message="Only one deployment found. Cannot discard the only deployment."))
168
173
  return
169
174
 
170
- previous_deployment, current_deployment = deployments[0], deployments[1]
175
+ current_deployment, deployment_to_discard = deployments[0], deployments[1]
171
176
 
172
- if previous_deployment.get("status") != "data_ready":
177
+ # NOTE(eclbg): we never get here. We wrote this code when we though we'd enable promoting back and forth between
178
+ # staging and live, but the current CLI commands don't allow getting in that state
179
+ if current_deployment.get("status") != "data_ready":
173
180
  click.echo(FeedbackManager.error(message="Previous deployment is not ready"))
174
- deploy_errors = previous_deployment.get("errors", [])
181
+ deploy_errors = current_deployment.get("errors", [])
175
182
  for deploy_error in deploy_errors:
176
183
  click.echo(FeedbackManager.error(message=f"* {deploy_error}"))
177
184
  return
178
185
 
179
- if previous_deployment.get("live"):
180
- click.echo(FeedbackManager.error(message="Previous deployment is already live"))
181
- else:
182
- click.echo(FeedbackManager.success(message="Promoting previous deployment"))
186
+ to_discard_status = deployment_to_discard.get("status")
187
+ verb = "Canceling" if to_discard_status in {"calculating", "creating_schema", "schema_ready"} else "Removing"
188
+ click.echo(FeedbackManager.success(message=f"{verb} deployment {deployment_to_discard['id']}"))
183
189
 
184
- TINYBIRD_API_URL = f"{host}/v1/deployments/{previous_deployment.get('id')}/set-live"
185
- result = api_post(TINYBIRD_API_URL, headers=headers)
186
-
187
- click.echo(FeedbackManager.success(message="Removing current deployment"))
188
-
189
- TINYBIRD_API_URL = f"{host}/v1/deployments/{current_deployment.get('id')}"
190
+ TINYBIRD_API_URL = f"{host}/v1/deployments/{deployment_to_discard.get('id')}"
190
191
  r = requests.delete(TINYBIRD_API_URL, headers=headers)
191
192
  result = r.json()
192
193
  logging.debug(json.dumps(result, indent=2))
@@ -194,15 +195,16 @@ def discard_deployment(host: Optional[str], headers: dict, wait: bool) -> None:
194
195
  click.echo(FeedbackManager.error(message=result.get("error")))
195
196
  sys_exit("deployment_error", result.get("error", "Unknown error"))
196
197
 
198
+ deployment_to_discard = deployments[1]
197
199
  click.echo(FeedbackManager.success(message="Discard process successfully started"))
198
200
 
199
201
  if wait:
200
202
  while True:
201
- TINYBIRD_API_URL = f"{host}/v1/deployments/{current_deployment.get('id')}"
203
+ TINYBIRD_API_URL = f"{host}/v1/deployments/{deployment_to_discard.get('id')}"
202
204
  result = api_fetch(TINYBIRD_API_URL, headers)
203
205
 
204
- current_deployment = result.get("deployment")
205
- if current_deployment and current_deployment.get("status") == "deleted":
206
+ deployment_to_discard = result.get("deployment")
207
+ if deployment_to_discard and deployment_to_discard.get("status") == "deleted":
206
208
  click.echo(FeedbackManager.success(message="Discard process successfully completed"))
207
209
  break
208
210
  time.sleep(5)
@@ -218,6 +220,8 @@ def create_deployment(
218
220
  check: Optional[bool] = None,
219
221
  allow_destructive_operations: Optional[bool] = None,
220
222
  ingest_hint: Optional[bool] = True,
223
+ output: Optional[str] = "human",
224
+ env: Optional[str] = "cloud",
221
225
  ) -> None:
222
226
  # TODO: This code is duplicated in build_server.py
223
227
  # Should be refactored to be shared
@@ -269,7 +273,10 @@ def create_deployment(
269
273
 
270
274
  result = api_post(TINYBIRD_API_URL, headers=HEADERS, files=files, params=params)
271
275
 
272
- print_changes(result, project)
276
+ print_changes(result, project, output)
277
+
278
+ if output == "json" and check:
279
+ echo_json(result.get("deployment", {}), 8)
273
280
 
274
281
  deployment = result.get("deployment", {})
275
282
  feedback = deployment.get("feedback", [])
@@ -315,13 +322,30 @@ def create_deployment(
315
322
  )
316
323
 
317
324
  status = result.get("result")
318
- if status == "success":
319
- host = get_display_cloud_host(client.host)
325
+ host = get_display_cloud_host(client.host)
326
+ if status in ["success", "failed"]:
320
327
  click.echo(
321
- FeedbackManager.info(message="Deployment URL: ")
328
+ FeedbackManager.gray(message="Deployment URL: ")
322
329
  + f"{bcolors.UNDERLINE}{host}/{config.get('name')}/deployments/{deployment.get('id')}{bcolors.ENDC}"
323
330
  )
331
+ jobs = client.jobs()
332
+ deployment_job = next(
333
+ (
334
+ job
335
+ for job in jobs
336
+ if job.get("kind") == "deployment" and job.get("deployment_id") == deployment.get("id")
337
+ ),
338
+ None,
339
+ )
340
+ if deployment_job:
341
+ echo_job_url(
342
+ token=client.token,
343
+ host=client.host,
344
+ workspace_name=config.get("name") or "",
345
+ job_url=deployment_job.get("job_url") or "",
346
+ )
324
347
 
348
+ if status == "success":
325
349
  if wait:
326
350
  click.echo(FeedbackManager.info(message="\n* Deployment submitted"))
327
351
  else:
@@ -354,26 +378,31 @@ def create_deployment(
354
378
  sys_exit("deployment_error", "Error parsing deployment from response")
355
379
  return
356
380
 
357
- if deployment.get("status") == "failed":
358
- click.echo(FeedbackManager.error(message="Deployment failed"))
359
- deploy_errors = deployment.get("errors")
360
- for deploy_error in deploy_errors:
361
- click.echo(FeedbackManager.error(message=f"* {deploy_error}"))
381
+ status = deployment.get("status")
382
+ errors = deployment.get("errors")
383
+ feedback = deployment.get("feedback")
362
384
 
363
- if auto:
364
- click.echo(FeedbackManager.error(message="Rolling back deployment"))
365
- discard_deployment(client.host, HEADERS, wait=wait)
366
- sys_exit(
367
- "deployment_error",
368
- f"Deployment failed. Errors: {str(deployment.get('errors') + deployment.get('feedback', []))}",
369
- )
385
+ if status == "failed":
386
+ # Just wait until we poll again and see deleting or deleted to report errors
387
+ pass
370
388
 
371
- if deployment.get("status") == "data_ready":
389
+ if status == "data_ready":
372
390
  break
373
391
 
374
- if deployment.get("status") in ["deleting", "deleted"]:
375
- click.echo(FeedbackManager.error(message="Deployment was deleted by another process"))
376
- sys_exit("deployment_error", "Deployment was deleted by another process")
392
+ if status in ["deleting", "deleted"]:
393
+ errors = deployment.get("errors")
394
+ if errors:
395
+ verb = "is being" if status == "deleting" else "was"
396
+ click.echo(
397
+ FeedbackManager.error(
398
+ message=f"Deployment failed and {verb} deleted automatically. Deployment errors:"
399
+ )
400
+ )
401
+ for error in errors:
402
+ click.echo(FeedbackManager.error(message=f"* {error}"))
403
+ sys_exit(
404
+ "deployment_error", f"Deployment deleted after failure. Errors: {str(errors + (feedback or []))}"
405
+ )
377
406
 
378
407
  time.sleep(5)
379
408
 
@@ -381,6 +410,15 @@ def create_deployment(
381
410
 
382
411
  if auto:
383
412
  promote_deployment(client.host, HEADERS, wait=wait, ingest_hint=ingest_hint)
413
+ # Fetch the final deployment state after promotion for JSON output
414
+ if output == "json":
415
+ url = f"{client.host}/v1/deployments/{deployment.get('id')}"
416
+ res = api_fetch(url, HEADERS)
417
+ deployment = res.get("deployment")
418
+
419
+ # Output JSON at the appropriate time based on the execution path
420
+ if output == "json" and deployment:
421
+ echo_json(deployment, 8)
384
422
 
385
423
 
386
424
  def _build_data_movement_message(kind: str, source_mv_name: Optional[str]) -> str:
@@ -392,7 +430,7 @@ def _build_data_movement_message(kind: str, source_mv_name: Optional[str]) -> st
392
430
  return ""
393
431
 
394
432
 
395
- def print_changes(result: dict, project: Project) -> None:
433
+ def print_changes(result: dict, project: Project, output: Optional[str] = "human") -> None:
396
434
  deployment = result.get("deployment", {})
397
435
  resources_columns = ["status", "name", "type", "path"]
398
436
  resources: list[list[Union[str, None]]] = []
@@ -29,20 +29,20 @@ def auth(args) -> None:
29
29
 
30
30
 
31
31
  @cli.command(
32
- name="branch",
32
+ name="environment",
33
33
  context_settings=dict(
34
34
  ignore_unknown_options=True,
35
35
  ),
36
36
  hidden=True,
37
37
  )
38
38
  @click.argument("args", nargs=-1, type=click.UNPROCESSED)
39
- def branch(args) -> None:
39
+ def environment(args) -> None:
40
40
  """
41
- `tb branch` is deprecated. You should rely on your version control system to manage branches.
41
+ `tb environment` has been renamed to `tb branch`.
42
42
  """
43
43
  click.echo(
44
44
  FeedbackManager.warning(
45
- message="This command is deprecated. You should rely on your version control system to manage branches."
45
+ message=f"`tb environment` has been renamed to `tb branch`. Please use `tb branch {args[0]}` instead."
46
46
  )
47
47
  )
48
48
  click.echo(
@@ -6,8 +6,10 @@ from pathlib import Path
6
6
  from typing import Callable, Optional
7
7
 
8
8
  import click
9
+ import requests
9
10
 
10
11
  from tinybird.tb.client import TinyB
12
+ from tinybird.tb.config import get_display_cloud_host
11
13
  from tinybird.tb.modules.common import sys_exit
12
14
  from tinybird.tb.modules.feedback_manager import FeedbackManager
13
15
  from tinybird.tb.modules.local_common import TB_LOCAL_PORT
@@ -33,6 +35,7 @@ class DevServer(http.server.HTTPServer):
33
35
  project: Project,
34
36
  build_status: BuildStatus,
35
37
  tb_client: TinyB,
38
+ branch: Optional[str] = None,
36
39
  ):
37
40
  port = 49161
38
41
  self.project = project
@@ -40,19 +43,32 @@ class DevServer(http.server.HTTPServer):
40
43
  self.process = process
41
44
  self.build_status = build_status
42
45
  self.port = port
46
+ base_ui_url = get_display_cloud_host(tb_client.host)
47
+ if branch:
48
+ ui_url = f"{base_ui_url}/{project.workspace_name}~{branch}/project"
49
+ else:
50
+ ui_url = f"{base_ui_url}/{project.workspace_name}/project"
43
51
 
44
52
  try:
45
53
  super().__init__(("", port), DevHandler)
46
- click.echo(FeedbackManager.success(message=f"✓ Dev server running on http://localhost:{port}"))
47
- click.echo(
48
- FeedbackManager.info(
49
- message=f"* Access your project at https://cloud.tinybird.co/local/{TB_LOCAL_PORT}/{project.workspace_name}/project"
50
- )
51
- )
54
+ click.echo(FeedbackManager.info(message=f"✓ Access your project at {ui_url}\n"))
52
55
  except OSError as e:
53
56
  if e.errno == 48: # Address already in use
54
- click.echo(FeedbackManager.error(message=f"Port {port} is already in use. Try a different port."))
55
- sys_exit("port_in_use", f"Port {port} is already in use")
57
+ dev_server_already_running = False
58
+ try:
59
+ response = requests.get(f"http://localhost:{port}")
60
+ if response.status_code == 200 and "Tinybird Dev Server" in response.text:
61
+ dev_server_already_running = True
62
+ except Exception:
63
+ pass
64
+ if dev_server_already_running:
65
+ message = f"Dev server is already running on http://localhost:{port}. Skipping..."
66
+ click.echo(FeedbackManager.warning(message=message))
67
+ sys_exit("dev_server_already_running", message)
68
+ else:
69
+ message = f"Port {port} is already in use. Check if another instance of the server is running or release the port."
70
+ click.echo(FeedbackManager.error(message=message))
71
+ sys_exit("port_in_use", message)
56
72
  else:
57
73
  click.echo(FeedbackManager.error_exception(error=e))
58
74
  sys_exit("server_error", str(e))
@@ -137,7 +153,10 @@ class DevHandler(http.server.SimpleHTTPRequestHandler):
137
153
  self.wfile.write(
138
154
  json.dumps(
139
155
  {
140
- "files": [f.replace(f"{project.folder}/", "") for f in project_files],
156
+ "files": [
157
+ {"path": f.replace(f"{project.folder}/", ""), "content": Path(f).read_text()}
158
+ for f in project_files
159
+ ],
141
160
  "root": project.folder,
142
161
  "workspace_name": project.workspace_name,
143
162
  }
@@ -232,7 +251,9 @@ class DevHandler(http.server.SimpleHTTPRequestHandler):
232
251
  pass
233
252
 
234
253
 
235
- def start_server(project: Project, tb_client: TinyB, process: Callable, build_status: BuildStatus):
254
+ def start_server(
255
+ project: Project, tb_client: TinyB, process: Callable, build_status: BuildStatus, branch: Optional[str] = None
256
+ ):
236
257
  """Start a development server for the project.
237
258
 
238
259
  Args:
@@ -240,10 +261,10 @@ def start_server(project: Project, tb_client: TinyB, process: Callable, build_st
240
261
  """
241
262
 
242
263
  try:
243
- click.echo(FeedbackManager.highlight(message="\n» Starting Tinybird dev server...\n"))
264
+ click.echo(FeedbackManager.highlight(message="» Exposing your project to Tinybird UI..."))
244
265
 
245
266
  # Create and start the server
246
- server = DevServer(process, project, build_status, tb_client)
267
+ server = DevServer(process, project, build_status, tb_client, branch)
247
268
  server.serve_forever()
248
269
 
249
270
  # Run the server in the main thread
@@ -14,8 +14,9 @@ from tinybird.tb.modules.project import Project
14
14
 
15
15
 
16
16
  @cli.command(name="info")
17
+ @click.option("--skip-local", is_flag=True, default=False, help="Skip local info")
17
18
  @click.pass_context
18
- def info(ctx: click.Context) -> None:
19
+ def info(ctx: click.Context, skip_local: bool) -> None:
19
20
  """Get information about the project that is currently being used"""
20
21
  ctx_config = ctx.ensure_object(dict)["config"]
21
22
  project: Project = ctx.ensure_object(dict)["project"]
@@ -27,23 +28,41 @@ def info(ctx: click.Context) -> None:
27
28
 
28
29
  click.echo(FeedbackManager.highlight(message="» Tinybird Cloud:"))
29
30
  cloud_table, cloud_columns = get_cloud_info(ctx_config)
30
- click.echo(FeedbackManager.highlight(message="\n» Tinybird Local:"))
31
- local_table, local_columns = get_local_info(ctx_config)
31
+
32
+ if not skip_local:
33
+ click.echo(FeedbackManager.highlight(message="\n» Tinybird Local:"))
34
+ local_table, local_columns = get_local_info(ctx_config)
35
+
32
36
  click.echo(FeedbackManager.highlight(message="\n» Project:"))
33
37
  project_table, project_columns = get_project_info(project.folder)
38
+
34
39
  if output == "json":
40
+ response: dict[str, Any] = {}
41
+
35
42
  cloud_data = {}
36
43
  if cloud_columns and cloud_table and isinstance(cloud_table, list) and len(cloud_table) > 0:
37
44
  cloud_data = {column: cloud_table[0][i] for i, column in enumerate(cloud_columns)}
45
+ response["cloud"] = cloud_data
38
46
 
39
- local_data = {}
40
- if local_columns and local_table and isinstance(local_table, list) and len(local_table) > 0:
41
- local_data = {column: local_table[0][i] for i, column in enumerate(local_columns)}
47
+ if not skip_local:
48
+ local_data = {}
49
+ if local_columns and local_table and isinstance(local_table, list) and len(local_table) > 0:
50
+ local_data = {column: local_table[0][i] for i, column in enumerate(local_columns)}
51
+ response["local"] = local_data
42
52
 
43
53
  project_data = {}
44
54
  if project_columns and project_table and isinstance(project_table, list) and len(project_table) > 0:
45
55
  project_data = {column: project_table[0][i] for i, column in enumerate(project_columns)}
46
- echo_json({"cloud": cloud_data, "local": local_data, "project": project_data})
56
+ response["project"] = project_data
57
+
58
+ branches = get_branches(ctx_config)
59
+ if branches:
60
+ branch_data: dict[str, dict[str, str]] = {}
61
+ for branch in branches:
62
+ branch_data[branch["name"]] = get_branch_info(branch)
63
+ response["branches"] = branch_data
64
+
65
+ echo_json(response)
47
66
 
48
67
 
49
68
  def get_cloud_info(ctx_config: Dict[str, Any]) -> Tuple[Iterable[Any], List[str]]:
@@ -67,6 +86,20 @@ def get_cloud_info(ctx_config: Dict[str, Any]) -> Tuple[Iterable[Any], List[str]
67
86
  return [], []
68
87
 
69
88
 
89
+ def get_branch_info(branch: Dict[str, Any]) -> Dict[str, Any]:
90
+ try:
91
+ token = branch.get("token") or "No token found"
92
+
93
+ return {
94
+ "token": token,
95
+ }
96
+
97
+ except Exception:
98
+ return {
99
+ "token": "No token found",
100
+ }
101
+
102
+
70
103
  def get_local_info(config: Dict[str, Any]) -> Tuple[Iterable[Any], List[str]]:
71
104
  try:
72
105
  local_config = get_tinybird_local_config(config, test=False, silent=False)
@@ -169,3 +202,13 @@ def get_project_info(project_path: Optional[str] = None) -> Tuple[Iterable[Any],
169
202
  table: Iterable[Any] = [(current_path, tinyb_path, project_path)]
170
203
  click.echo(format_robust_table(table, column_names=columns))
171
204
  return table, columns
205
+
206
+
207
+ def get_branches(ctx_config: Dict[str, Any]) -> List[Dict[str, Any]]:
208
+ try:
209
+ config = CLIConfig.get_project_config()
210
+ client = config.get_client()
211
+ response = client.branches()
212
+ return response["environments"]
213
+ except Exception:
214
+ return []
@@ -0,0 +1,15 @@
1
+ import click
2
+
3
+ from tinybird.tb.config import get_display_cloud_host
4
+ from tinybird.tb.modules.feedback_manager import FeedbackManager
5
+ from tinybird.tb.modules.local_common import TB_LOCAL_ADDRESS
6
+
7
+
8
+ def echo_job_url(token: str, host: str, workspace_name: str, job_url: str):
9
+ if "localhost" in host:
10
+ job_url = f"{job_url.replace('http://localhost:8001', TB_LOCAL_ADDRESS)}?token={token}"
11
+ click.echo(FeedbackManager.gray(message="Job API URL: ") + FeedbackManager.info(message=f"{job_url}"))
12
+ ui_host = get_display_cloud_host(host)
13
+ click.echo(
14
+ FeedbackManager.gray(message="Jobs URL: ") + FeedbackManager.info(message=f"{ui_host}/{workspace_name}/jobs")
15
+ )
@@ -10,7 +10,9 @@ from docker.client import DockerClient
10
10
 
11
11
  from tinybird.tb.modules.cli import cli
12
12
  from tinybird.tb.modules.common import echo_json, update_cli
13
+ from tinybird.tb.modules.exceptions import CLILocalException
13
14
  from tinybird.tb.modules.feedback_manager import FeedbackManager
15
+ from tinybird.tb.modules.info import get_local_info
14
16
  from tinybird.tb.modules.local_common import (
15
17
  TB_CONTAINER_NAME,
16
18
  TB_LOCAL_ADDRESS,
@@ -18,6 +20,19 @@ from tinybird.tb.modules.local_common import (
18
20
  get_existing_container_with_matching_env,
19
21
  start_tinybird_local,
20
22
  )
23
+ from tinybird.tb.modules.local_logs import (
24
+ check_memory_sufficient,
25
+ clickhouse_is_ready,
26
+ container_is_ready,
27
+ container_is_starting,
28
+ container_is_stopping,
29
+ container_is_unhealthy,
30
+ container_stats,
31
+ events_is_ready,
32
+ local_authentication_is_ready,
33
+ redis_is_ready,
34
+ server_is_ready,
35
+ )
21
36
 
22
37
 
23
38
  def stop_tinybird_local(docker_client: DockerClient) -> None:
@@ -76,30 +91,69 @@ def stop() -> None:
76
91
  @click.pass_context
77
92
  def status(ctx: click.Context) -> None:
78
93
  """Check status of Tinybird Local"""
94
+
95
+ click.echo(FeedbackManager.highlight(message="» Checking status..."))
79
96
  docker_client = get_docker_client()
80
97
  container = get_existing_container_with_matching_env(docker_client, TB_CONTAINER_NAME, {})
81
98
 
82
- if container:
83
- status = container.status
84
- health = container.attrs.get("State", {}).get("Health", {}).get("Status")
85
-
86
- if status == "running" and health == "healthy":
87
- click.echo(FeedbackManager.success(message="✓ Tinybird Local is ready!"))
88
- click.echo(FeedbackManager.highlight(message="\n» Tinybird Local:"))
89
- from tinybird.tb.modules.info import get_local_info
90
-
91
- config = ctx.ensure_object(dict).get("config", {})
92
- get_local_info(config)
93
- elif status == "restarting" or (status == "running" and health == "starting"):
94
- click.echo(FeedbackManager.highlight(message="* Tinybird Local is starting..."))
95
- elif status == "removing":
96
- click.echo(FeedbackManager.highlight(message="* Tinybird Local is stopping..."))
99
+ try:
100
+ if container:
101
+ if container_is_ready(container):
102
+ stats = container_stats(container, docker_client)
103
+ click.echo(FeedbackManager.info(message=f" Tinybird Local container ({stats})"))
104
+
105
+ # Check memory sufficiency
106
+ is_sufficient, warning_msg = check_memory_sufficient(container, docker_client)
107
+ if not is_sufficient and warning_msg:
108
+ click.echo(FeedbackManager.warning(message=f" {warning_msg}"))
109
+
110
+ if not clickhouse_is_ready(container):
111
+ raise Exception("Clickhouse is not ready.")
112
+ click.echo(FeedbackManager.info(message="✓ Clickhouse"))
113
+
114
+ if not redis_is_ready(container):
115
+ raise Exception("Redis is not ready.")
116
+ click.echo(FeedbackManager.info(message="✓ Redis"))
117
+
118
+ if not server_is_ready(container):
119
+ raise Exception("Server is not ready.")
120
+ click.echo(FeedbackManager.info(message="✓ Server"))
121
+
122
+ if not events_is_ready(container):
123
+ raise Exception("Events is not ready.")
124
+ click.echo(FeedbackManager.info(message="✓ Events"))
125
+
126
+ if not local_authentication_is_ready(container):
127
+ raise Exception("Tinybird Local authentication is not ready.")
128
+ click.echo(FeedbackManager.info(message="✓ Tinybird Local authentication"))
129
+
130
+ click.echo(FeedbackManager.success(message="✓ Tinybird Local is ready!"))
131
+ click.echo(FeedbackManager.highlight(message="\n» Tinybird Local:"))
132
+ config = ctx.ensure_object(dict).get("config", {})
133
+ get_local_info(config)
134
+ elif container_is_starting(container):
135
+ click.echo(FeedbackManager.highlight(message="* Tinybird Local is starting..."))
136
+ elif container_is_stopping(container):
137
+ click.echo(FeedbackManager.highlight(message="* Tinybird Local is stopping..."))
138
+ elif container_is_unhealthy(container):
139
+ is_sufficient, warning_msg = check_memory_sufficient(container, docker_client)
140
+ if not is_sufficient and warning_msg:
141
+ click.echo(FeedbackManager.warning(message=f"△ {warning_msg}"))
142
+ click.echo(
143
+ FeedbackManager.error(
144
+ message="* Tinybird Local is unhealthy. Try running `tb local restart` in a few seconds."
145
+ )
146
+ )
147
+ else:
148
+ click.echo(
149
+ FeedbackManager.error(message="✗ Tinybird Local is not running. Run 'tb local start' to start it")
150
+ )
97
151
  else:
98
152
  click.echo(
99
- FeedbackManager.info(message="✗ Tinybird Local is not running. Run 'tb local start' to start it")
153
+ FeedbackManager.error(message="✗ Tinybird Local is not running. Run 'tb local start' to start it")
100
154
  )
101
- else:
102
- click.echo(FeedbackManager.info(message="Tinybird Local is not running. Run 'tb local start' to start it"))
155
+ except Exception as e:
156
+ raise CLILocalException(FeedbackManager.error(message=f"Tinybird Local is not ready. Reason: {e}"))
103
157
 
104
158
 
105
159
  @local.command()
@@ -141,8 +195,20 @@ def remove() -> None:
141
195
  envvar="TB_LOCAL_WORKSPACE_TOKEN",
142
196
  help="Workspace token to use for the Tinybird Local container.",
143
197
  )
198
+ @click.option(
199
+ "--daemon/--watch",
200
+ "-d/-w",
201
+ default=False,
202
+ is_flag=True,
203
+ help="Run Tinybird Local in the background.",
204
+ )
144
205
  def start(
145
- use_aws_creds: bool, volumes_path: str, skip_new_version: bool, user_token: str, workspace_token: str
206
+ use_aws_creds: bool,
207
+ volumes_path: str,
208
+ skip_new_version: bool,
209
+ user_token: str,
210
+ workspace_token: str,
211
+ daemon: bool,
146
212
  ) -> None:
147
213
  """Start Tinybird Local"""
148
214
  if volumes_path is not None:
@@ -152,8 +218,12 @@ def start(
152
218
 
153
219
  click.echo(FeedbackManager.highlight(message="» Starting Tinybird Local..."))
154
220
  docker_client = get_docker_client()
155
- start_tinybird_local(docker_client, use_aws_creds, volumes_path, skip_new_version, user_token, workspace_token)
156
- click.echo(FeedbackManager.success(message="✓ Tinybird Local is ready!"))
221
+ watch = not daemon
222
+ start_tinybird_local(
223
+ docker_client, use_aws_creds, volumes_path, skip_new_version, user_token, workspace_token, watch=watch
224
+ )
225
+ if daemon:
226
+ click.echo(FeedbackManager.success(message="✓ Tinybird Local is ready!"))
157
227
 
158
228
 
159
229
  @local.command()