Files
opensandbox-group--opensandbox/cli/src/opensandbox_cli/output.py
T
wehub-resource-sync e0e362d700
SDK Tests / changes (push) Waiting to run
SDK Tests / Python SDK Tests (sandbox) (push) Blocked by required conditions
SDK Tests / CLI Quality (push) Blocked by required conditions
SDK Tests / CLI Tests (push) Blocked by required conditions
SDK Tests / Python SDK Quality (code-interpreter) (push) Blocked by required conditions
SDK Tests / Python SDK Quality (sandbox) (push) Blocked by required conditions
SDK Tests / Python SDK Tests (code-interpreter) (push) Blocked by required conditions
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Blocked by required conditions
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Blocked by required conditions
Deploy Docs Pages / build (push) Waiting to run
Deploy Docs Pages / deploy (push) Blocked by required conditions
Real E2E Tests / changes (push) Waiting to run
Real E2E Tests / Python E2E (docker bridge) (push) Blocked by required conditions
Real E2E Tests / Java E2E (docker bridge) (push) Blocked by required conditions
Real E2E Tests / JavaScript E2E (docker bridge) (push) Blocked by required conditions
Real E2E Tests / C# E2E (docker bridge) (push) Blocked by required conditions
Real E2E Tests / Go E2E (docker bridge) (push) Blocked by required conditions
Real E2E Tests / Real E2E CI (push) Blocked by required conditions
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Blocked by required conditions
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Blocked by required conditions
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Blocked by required conditions
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Blocked by required conditions
SDK Tests / Go SDK Quality And Tests (push) Blocked by required conditions
SDK Tests / SDK CI (push) Blocked by required conditions
chore: import upstream snapshot with attribution
2026-07-13 13:39:33 +08:00

364 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Copyright 2026 Alibaba Group Holding Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Output formatting: table (rich), JSON, YAML."""
from __future__ import annotations
import json
import sys
from collections.abc import Generator, Sequence
from contextlib import contextmanager
from typing import Any
import click
try:
import yaml
except ImportError: # pragma: no cover
yaml = None # type: ignore[assignment]
from pydantic import BaseModel
from rich import box
from rich.console import Console
from rich.panel import Panel
from rich.status import Status
from rich.table import Table
from rich.text import Text
# ---------------------------------------------------------------------------
# Status badge styling (sandbox state → color + icon)
# ---------------------------------------------------------------------------
_STATUS_STYLES: dict[str, tuple[str, str]] = {
# state → (rich style, icon)
"running": ("bold green", "●"),
"ready": ("bold green", "●"),
"healthy": ("bold green", "●"),
"pending": ("bold yellow", "◐"),
"creating": ("bold yellow", "◐"),
"starting": ("bold yellow", "◐"),
"paused": ("bold blue", "⏸"),
"stopped": ("dim", "○"),
"terminated": ("dim", "○"),
"killed": ("dim", "○"),
"error": ("bold red", "✗"),
"failed": ("bold red", "✗"),
"unhealthy": ("bold red", "✗"),
"created": ("bold cyan", "✦"),
}
# Columns that contain status-like values
_STATUS_COLUMNS = {"status", "state", "healthy"}
# Columns that should be rendered in a dimmer style (long IDs, timestamps)
_DIM_COLUMNS = {"created_at", "expires_at", "modified_at", "updated_at"}
# Columns that are primary identifiers
_ID_COLUMNS = {"id", "sandbox_id", "execution_id", "context_id"}
def _style_value(col: str, value: str) -> Text:
"""Apply contextual styling to a cell value."""
lower = value.lower()
if col in _STATUS_COLUMNS:
style, icon = _STATUS_STYLES.get(lower, ("", ""))
if style:
return Text(f"{icon} {value}", style=style)
if col in _DIM_COLUMNS:
return Text(value, style="dim")
if col in _ID_COLUMNS:
return Text(value, style="bold cyan")
return Text(value)
class OutputFormatter:
"""Renders data in table / json / yaml format."""
def __init__(self, fmt: str = "table", *, color: bool = True) -> None:
self.fmt = fmt
self.color = color
self.console = Console(
stderr=False, no_color=not color, force_terminal=None
)
self._err_console = Console(
stderr=True, no_color=not color, force_terminal=None
)
# ------------------------------------------------------------------
# Status messages with icons
# ------------------------------------------------------------------
def success(self, msg: str) -> None:
"""Print a success message with ✅ icon."""
if self.fmt == "json":
self._print_json({"status": "ok", "message": msg})
return
if self.fmt == "yaml":
self._print_yaml({"status": "ok", "message": msg})
return
if self.color:
self.console.print(f" [bold green]✅ {msg}[/]")
else:
click.echo(f"OK: {msg}")
def info(self, msg: str) -> None:
"""Print an info message with ️ icon."""
if self.fmt == "json":
self._print_json({"status": "info", "message": msg})
return
if self.fmt == "yaml":
self._print_yaml({"status": "info", "message": msg})
return
if self.color:
self.console.print(f" [bold blue]{msg}[/]")
else:
click.echo(f"INFO: {msg}")
def warning(self, msg: str) -> None:
"""Print a warning message with ⚠️ icon."""
if self.fmt == "json":
self._print_json({"status": "warning", "message": msg})
return
if self.fmt == "yaml":
self._print_yaml({"status": "warning", "message": msg})
return
if self.color:
self._err_console.print(f" [bold yellow]⚠️ {msg}[/]")
else:
click.echo(f"WARN: {msg}", err=True)
def error(self, msg: str) -> None:
"""Print an error message with ❌ icon."""
if self.fmt == "json":
self._print_json({"status": "error", "message": msg})
return
if self.fmt == "yaml":
self._print_yaml({"status": "error", "message": msg})
return
if self.color:
self._err_console.print(f" [bold red]❌ {msg}[/]")
else:
click.echo(f"ERROR: {msg}", err=True)
def error_panel(self, msg: str, title: str = "Error") -> None:
"""Print an error with a bold header and message."""
if self.color:
self._err_console.print()
self._err_console.print(f" [bold red]{title}[/]")
self._err_console.print(f" [dim]{'─' * (len(title) + 2)}[/]")
for line in msg.splitlines():
self._err_console.print(f" {line}")
self._err_console.print()
else:
click.echo(f"ERROR [{title}]: {msg}", err=True)
# ------------------------------------------------------------------
# Spinner for long-running operations
# ------------------------------------------------------------------
@contextmanager
def spinner(self, msg: str) -> Generator[Status, None, None]:
"""Context manager that shows a spinner while work is in progress."""
if self.color and self.fmt == "table":
with self._err_console.status(f"[bold cyan]⏳ {msg}[/]", spinner="dots") as status:
yield status
else:
# No spinner in non-color or non-table mode
yield None # type: ignore[arg-type]
# ------------------------------------------------------------------
# Panel output
# ------------------------------------------------------------------
def panel(self, content: str, *, title: str | None = None, style: str = "cyan") -> None:
"""Print content inside a styled panel."""
if self.color:
self.console.print(Panel(
content,
title=title,
title_align="left",
border_style=style,
box=box.ROUNDED,
padding=(0, 1),
))
else:
if title:
click.echo(f"--- {title} ---")
click.echo(content)
def success_panel(self, data: dict[str, Any], *, title: str = "Success") -> None:
"""Print a success result with a header and indented key-value pairs."""
if self.fmt != "table":
if self.fmt == "json":
self._print_json(data)
elif self.fmt == "yaml":
self._print_yaml(data)
return
if self.color:
self.console.print()
self.console.print(f" [bold green]✓ {title}[/]")
self.console.print(f" [dim]{'─' * (len(title) + 2)}[/]")
for k, v in data.items():
self.console.print(f" [bold]{k}:[/] [cyan]{v}[/]")
self.console.print()
else:
click.echo(f"--- {title} ---")
for k, v in data.items():
click.echo(f" {k}: {v}")
# ------------------------------------------------------------------
# Public helpers
# ------------------------------------------------------------------
def print_model(self, model: BaseModel, title: str | None = None) -> None:
"""Print a single Pydantic model as key-value panel or JSON/YAML."""
data = _model_to_dict(model)
if self.fmt == "json":
self._print_json(data)
elif self.fmt == "yaml":
self._print_yaml(data)
else:
self._print_kv_table(data, title=title)
def print_models(
self,
models: Sequence[BaseModel],
columns: list[str],
*,
title: str | None = None,
) -> None:
"""Print a list of Pydantic models as a table or JSON/YAML."""
rows = [_model_to_dict(m) for m in models]
if self.fmt == "json":
self._print_json(rows)
elif self.fmt == "yaml":
self._print_yaml(rows)
else:
self._print_table(rows, columns, title=title)
def print_rows(
self,
rows: list[dict[str, Any]],
columns: list[str],
*,
title: str | None = None,
) -> None:
"""Print pre-processed rows (list of dicts) as a table or JSON/YAML."""
if self.fmt == "json":
self._print_json(rows)
elif self.fmt == "yaml":
self._print_yaml(rows)
else:
self._print_table(rows, columns, title=title)
def print_dict(self, data: dict[str, Any], title: str | None = None) -> None:
"""Print a flat dict."""
if self.fmt == "json":
self._print_json(data)
elif self.fmt == "yaml":
self._print_yaml(data)
else:
self._print_kv_table(data, title=title)
def print_text(self, text: str) -> None:
"""Print raw text (ignores format)."""
click.echo(text)
# ------------------------------------------------------------------
# Internal renderers
# ------------------------------------------------------------------
def _print_json(self, data: Any) -> None:
if self.color:
self.console.print_json(json.dumps(data, default=str))
else:
click.echo(json.dumps(data, indent=2, default=str))
def _print_yaml(self, data: Any) -> None:
if yaml is None:
click.secho(
"PyYAML is not installed. Use --output json instead.", fg="red", err=True
)
sys.exit(1)
click.echo(yaml.dump(data, default_flow_style=False, allow_unicode=True).rstrip())
def _print_kv_table(self, data: dict[str, Any], *, title: str | None = None) -> None:
table = Table(
title=title,
show_header=True,
header_style="bold magenta",
title_style="bold cyan",
box=box.ROUNDED,
border_style="bright_black",
padding=(0, 1),
show_lines=True,
)
table.add_column("Key", style="bold cyan", no_wrap=True)
table.add_column("Value")
for k, v in data.items():
val_text = _style_value(k, str(v)) if v is not None else Text("-", style="dim")
table.add_row(str(k), val_text)
self.console.print(table)
def _print_table(
self,
rows: list[dict[str, Any]],
columns: list[str],
*,
title: str | None = None,
) -> None:
table = Table(
title=title,
show_header=True,
header_style="bold magenta",
title_style="bold cyan",
box=box.ROUNDED,
border_style="bright_black",
padding=(0, 1),
row_styles=["", "dim"],
)
for col in columns:
style = ""
if col in _ID_COLUMNS:
style = "bold cyan"
elif col in _DIM_COLUMNS:
style = "dim"
table.add_column(col.upper(), style=style, no_wrap=(col in _ID_COLUMNS))
for row in rows:
cells: list[Text | str] = []
for col in columns:
val = str(row.get(col, "-"))
if col in _STATUS_COLUMNS:
cells.append(_style_value(col, val))
else:
cells.append(val)
table.add_row(*cells)
self.console.print(table)
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
def _model_to_dict(model: BaseModel) -> dict[str, Any]:
return model.model_dump(mode="json")