feat: unify structured error output
This commit is contained in:
@@ -55,6 +55,22 @@ def test_cli_commands_wrap_client_creation_errors(monkeypatch, args) -> None:
|
|||||||
assert type(result.exception).__name__ == "SystemExit"
|
assert type(result.exception).__name__ == "SystemExit"
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_user_error_yaml(monkeypatch) -> None:
|
||||||
|
monkeypatch.setenv("OUTPUT", "auto")
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"twitter_cli.cli._get_client",
|
||||||
|
lambda config=None: (_ for _ in ()).throw(RuntimeError("User not found")),
|
||||||
|
)
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
result = runner.invoke(cli, ["user", "alice", "--yaml"])
|
||||||
|
|
||||||
|
assert result.exit_code == 1
|
||||||
|
payload = yaml.safe_load(result.output)
|
||||||
|
assert payload["ok"] is False
|
||||||
|
assert payload["error"]["code"] == "api_error"
|
||||||
|
|
||||||
|
|
||||||
def test_cli_tweet_accepts_shared_url_with_query(monkeypatch) -> None:
|
def test_cli_tweet_accepts_shared_url_with_query(monkeypatch) -> None:
|
||||||
class FakeClient:
|
class FakeClient:
|
||||||
def fetch_tweet_detail(self, tweet_id: str, max_count: int):
|
def fetch_tweet_detail(self, tweet_id: str, max_count: int):
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ from .formatter import (
|
|||||||
from .models import UserProfile
|
from .models import UserProfile
|
||||||
from .output import (
|
from .output import (
|
||||||
default_structured_format,
|
default_structured_format,
|
||||||
|
emit_error,
|
||||||
emit_structured,
|
emit_structured,
|
||||||
error_payload,
|
error_payload,
|
||||||
structured_output_options,
|
structured_output_options,
|
||||||
@@ -144,6 +145,8 @@ def _get_client_for_output(config=None, quiet=False):
|
|||||||
|
|
||||||
def _exit_with_error(exc):
|
def _exit_with_error(exc):
|
||||||
# type: (RuntimeError) -> None
|
# type: (RuntimeError) -> None
|
||||||
|
if emit_error("api_error", str(exc)):
|
||||||
|
sys.exit(1)
|
||||||
console.print("[red]❌ %s[/red]" % exc)
|
console.print("[red]❌ %s[/red]" % exc)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|||||||
@@ -99,3 +99,30 @@ def _normalize_success_payload(data: Any) -> Any:
|
|||||||
if isinstance(data, dict) and data.get("schema_version") == _SCHEMA_VERSION and "ok" in data:
|
if isinstance(data, dict) and data.get("schema_version") == _SCHEMA_VERSION and "ok" in data:
|
||||||
return data
|
return data
|
||||||
return success_payload(data)
|
return success_payload(data)
|
||||||
|
|
||||||
|
|
||||||
|
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 = default_structured_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":
|
||||||
|
click.echo(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||||
|
else:
|
||||||
|
click.echo(yaml.safe_dump(payload, allow_unicode=True, sort_keys=False, default_flow_style=False))
|
||||||
|
return True
|
||||||
|
|||||||
Reference in New Issue
Block a user