Files
nanocode/skills/xiaohongshu-cli/xhs_cli/formatter_utils.py
T
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

188 lines
5.5 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.
"""Base formatting utilities shared across formatter modules.
This module breaks the circular dependency between formatter.py and
formatter_renderers.py by providing the shared primitives both need.
"""
import json
import os
import sys
from typing import Any
import click
import yaml
from rich.console import Console
console = Console(stderr=True)
error_console = Console(stderr=True)
_stdout = Console()
_OUTPUT_ENV = "OUTPUT"
_SCHEMA_VERSION = "1"
# ─── Output format resolution ──────────────────────────────────────────────
def resolve_output_format(*, as_json: bool, as_yaml: bool) -> str | None:
"""Resolve explicit flags first, then env override, then TTY default."""
if as_json and as_yaml:
raise click.UsageError("Use only one of --json or --yaml.")
if as_yaml:
return "yaml"
if as_json:
return "json"
output_mode = os.getenv(_OUTPUT_ENV, "auto").strip().lower()
if output_mode == "yaml":
return "yaml"
if output_mode == "json":
return "json"
if output_mode == "rich":
return None
if not sys.stdout.isatty():
return "yaml"
return None
# ─── Structured output ─────────────────────────────────────────────────────
def print_json(data: Any) -> None:
"""Print raw JSON output to stdout."""
_stdout.print_json(json.dumps(data, ensure_ascii=False, indent=2))
def print_yaml(data: Any) -> None:
"""Print raw YAML output to stdout."""
click.echo(
yaml.safe_dump(
data,
allow_unicode=True,
sort_keys=False,
default_flow_style=False,
)
)
def success_payload(data: Any) -> dict[str, Any]:
"""Wrap structured success data in the shared agent schema."""
return {
"ok": True,
"schema_version": _SCHEMA_VERSION,
"data": data,
}
def error_payload(code: str, message: str, *, details: Any | None = None) -> dict[str, Any]:
"""Wrap structured error data in the shared agent schema."""
error = {
"code": code,
"message": message,
}
if details is not None:
error["details"] = details
return {
"ok": False,
"schema_version": _SCHEMA_VERSION,
"error": error,
}
def _normalize_success_payload(data: Any) -> Any:
"""Wrap plain structured data in the shared agent success schema."""
if isinstance(data, dict) and data.get("schema_version") == _SCHEMA_VERSION and "ok" in data:
return data
return success_payload(data)
def maybe_print_structured(data: Any, *, as_json: bool, as_yaml: bool) -> bool:
"""Print structured output when requested or when stdout is non-TTY."""
fmt = resolve_output_format(as_json=as_json, as_yaml=as_yaml)
if not fmt:
return False
payload = _normalize_success_payload(data)
if fmt == "json":
print_json(payload)
else:
print_yaml(payload)
return True
def emit_error(
code: str,
message: str,
*,
as_json: bool | None = None,
as_yaml: bool | None = None,
details: Any | None = None,
) -> bool:
"""Emit a structured error when the active output mode is machine-readable."""
if as_json is None or as_yaml is None:
ctx = click.get_current_context(silent=True)
params = ctx.params if ctx is not None else {}
as_json = bool(params.get("as_json", False)) if as_json is None else as_json
as_yaml = bool(params.get("as_yaml", False)) if as_yaml is None else as_yaml
fmt = resolve_output_format(as_json=bool(as_json), as_yaml=bool(as_yaml))
if fmt is None:
return False
payload = error_payload(code, message, details=details)
if fmt == "json":
print_json(payload)
else:
print_yaml(payload)
return True
# ─── UI helpers ─────────────────────────────────────────────────────────────
def print_error(message: str) -> None:
"""Print error message."""
if emit_error("api_error", message):
return
error_console.print(f"[red]✗[/red] {message}")
def print_success(message: str) -> None:
"""Print success message."""
console.print(f"[green]✓[/green] {message}")
def print_info(message: str) -> None:
"""Print info message."""
console.print(f"[dim]ℹ[/dim] {message}")
# ─── Data formatting ───────────────────────────────────────────────────────
def coerce_int(value: Any, default: int = 0) -> int:
"""Best-effort integer coercion for reverse-engineered API fields."""
if isinstance(value, bool):
return int(value)
if isinstance(value, int):
return value
if isinstance(value, str):
try:
return int(value.strip())
except ValueError:
return default
return default
def format_count(n: int | str) -> str:
"""Format number for display (e.g., 12345 → 1.2万)."""
if isinstance(n, str):
try:
n = int(n)
except ValueError:
return str(n)
if n >= 100_000_000:
return f"{n / 100_000_000:.1f}亿"
if n >= 10_000:
return f"{n / 10_000:.1f}万"
return str(n)