fix: format cli operation errors

This commit is contained in:
lda
2026-06-05 17:18:39 +07:00 Verified
parent e3bd28968e
commit 04491b2550
14 changed files with 288 additions and 66 deletions
+9
View File
@@ -48,13 +48,22 @@ def root(
float | None,
typer.Option("--timeout", min=0.1, help="Override RPC timeout seconds."),
] = None,
verbose: Annotated[
bool,
typer.Option(
"--verbose",
help="Show full tracebacks for unexpected internal errors.",
),
] = False,
) -> None:
"""Run workflow platform commands."""
app.pretty_exceptions_short = not verbose
ctx.obj = CliTyperState(
config_path=config,
force_local=local,
rpc_url=url,
rpc_timeout_seconds=timeout,
verbose=verbose,
)
+4 -5
View File
@@ -1,12 +1,11 @@
from __future__ import annotations
import asyncio
from typing import Annotated
import typer
from wf_cli.context import load_cli_context_from_typer
from wf_cli.formats import ListOutputFormat, emit_list_payload
from wf_cli.remote_errors import run_cli_operation
from . import source_registry
@@ -28,7 +27,7 @@ def list_connections(
) -> None:
"""List configured upstream connections known to the target."""
context = load_cli_context_from_typer(ctx)
payload = asyncio.run(context.admin.list_connections())
payload = run_cli_operation(context, context.admin.list_connections())
emit_list_payload(
payload,
collection_key="connections",
@@ -47,7 +46,7 @@ def get_connection_statuses(
) -> None:
"""List connection catalog/status summaries."""
context = load_cli_context_from_typer(ctx)
payload = asyncio.run(context.admin.get_connection_statuses())
payload = run_cli_operation(context, context.admin.get_connection_statuses())
emit_list_payload(
payload,
collection_key="statuses",
@@ -66,7 +65,7 @@ def list_events(
) -> None:
"""List recorded workflow platform events."""
context = load_cli_context_from_typer(ctx)
payload = asyncio.run(context.admin.list_events())
payload = run_cli_operation(context, context.admin.list_events())
emit_list_payload(
payload,
collection_key="events",
+7 -6
View File
@@ -1,6 +1,4 @@
from __future__ import annotations
import asyncio
from typing import Annotated, Literal
import typer
@@ -8,6 +6,7 @@ import typer
from wf_cli.context import load_cli_context_from_typer as load_cli_context
from wf_cli.formats import ListOutputFormat, emit_list_payload
from wf_cli.io import emit_json
from wf_cli.remote_errors import run_cli_operation
app = typer.Typer(
name="artifact",
@@ -38,13 +37,14 @@ def list_artifacts(
) -> None:
"""List compact saved artifact summaries."""
context = load_cli_context(ctx)
payload = asyncio.run(
payload = run_cli_operation(
context,
context.handlers.list_artifacts(
query=query,
kind=kind,
cursor=cursor,
limit=limit,
)
),
)
emit_list_payload(
payload,
@@ -64,7 +64,8 @@ def inspect_artifact(
"""Inspect one saved artifact version."""
context = load_cli_context(ctx)
emit_json(
asyncio.run(
context.handlers.inspect_artifact(artifact_id=artifact_id, version=version)
run_cli_operation(
context,
context.handlers.inspect_artifact(artifact_id=artifact_id, version=version),
)
)
+7 -6
View File
@@ -1,6 +1,4 @@
from __future__ import annotations
import asyncio
from typing import Annotated
import typer
@@ -8,6 +6,7 @@ import typer
from wf_cli.context import load_cli_context_from_typer
from wf_cli.formats import ListOutputFormat, emit_list_payload
from wf_cli.io import emit_json
from wf_cli.remote_errors import run_cli_operation
app = typer.Typer(
name="cap",
@@ -38,13 +37,14 @@ def list_capabilities(
) -> None:
"""List compact planner-visible workflow capabilities."""
context = load_cli_context_from_typer(ctx)
payload = asyncio.run(
payload = run_cli_operation(
context,
context.handlers.list_capabilities(
query=query,
source_id=source_id,
cursor=cursor,
limit=limit,
)
),
)
emit_list_payload(
payload,
@@ -62,7 +62,8 @@ def inspect_capability(
) -> None:
"""Inspect one workflow capability contract."""
context = load_cli_context_from_typer(ctx)
payload = asyncio.run(
context.handlers.inspect_capability(qualified_name=qualified_name)
payload = run_cli_operation(
context,
context.handlers.inspect_capability(qualified_name=qualified_name),
)
emit_json(payload)
+14 -8
View File
@@ -1,6 +1,4 @@
from __future__ import annotations
import asyncio
from pathlib import Path
from typing import Annotated
@@ -9,6 +7,7 @@ import typer
from wf_cli.context import load_cli_context_from_typer as load_cli_context
from wf_cli.formats import ListOutputFormat, emit_list_payload
from wf_cli.io import CliInputError, emit_json, parse_bindings, parse_json_input
from wf_cli.remote_errors import run_cli_operation
app = typer.Typer(
name="deploy",
@@ -31,11 +30,12 @@ def validate_deployment(
) -> None:
"""Validate one saved workflow deployment."""
context = load_cli_context(ctx)
payload = asyncio.run(
payload = run_cli_operation(
context,
context.handlers.validate_deployment(
deployment_id=deployment_id,
live_check=live,
)
),
)
emit_json(payload)
@@ -49,7 +49,7 @@ def list_deployments(
) -> None:
"""List saved workflow deployments."""
context = load_cli_context(ctx)
payload = asyncio.run(context.handlers.list_deployments())
payload = run_cli_operation(context, context.handlers.list_deployments())
emit_list_payload(
payload,
collection_key="deployments",
@@ -67,7 +67,10 @@ def inspect_deployment(
"""Inspect one saved deployment."""
context = load_cli_context(ctx)
emit_json(
asyncio.run(context.handlers.inspect_deployment(deployment_id=deployment_id))
run_cli_operation(
context,
context.handlers.inspect_deployment(deployment_id=deployment_id),
)
)
@@ -107,7 +110,7 @@ def save_deployment(
except CliInputError as exc:
raise typer.BadParameter(str(exc)) from exc
context = load_cli_context(ctx)
emit_json(asyncio.run(context.handlers.save_deployment(payload)))
emit_json(run_cli_operation(context, context.handlers.save_deployment(payload)))
@app.command("delete")
@@ -118,7 +121,10 @@ def delete_deployment(
"""Delete one saved deployment."""
context = load_cli_context(ctx)
emit_json(
asyncio.run(context.handlers.delete_deployment(deployment_id=deployment_id))
run_cli_operation(
context,
context.handlers.delete_deployment(deployment_id=deployment_id),
)
)
+20 -15
View File
@@ -1,6 +1,4 @@
from __future__ import annotations
import asyncio
from pathlib import Path
from typing import Annotated, Literal
@@ -9,6 +7,7 @@ import typer
from wf_cli.context import load_cli_context_from_typer as load_cli_context
from wf_cli.formats import ListOutputFormat, emit_list_payload
from wf_cli.io import CliInputError, emit_json, parse_bindings, parse_json_value
from wf_cli.remote_errors import run_cli_operation
app = typer.Typer(
name="draft",
@@ -26,7 +25,7 @@ def list_drafts(
) -> None:
"""List stored draft workspaces."""
context = load_cli_context(ctx)
payload = asyncio.run(context.handlers.list_draft_workspaces())
payload = run_cli_operation(context, context.handlers.list_draft_workspaces())
emit_list_payload(
payload,
collection_key="workspaces",
@@ -47,11 +46,12 @@ def inspect_draft(
"""Inspect one draft workspace."""
context = load_cli_context(ctx)
emit_json(
asyncio.run(
run_cli_operation(
context,
context.handlers.get_draft_workspace(
workspace_id=workspace_id,
include_draft=include_draft,
)
),
)
)
@@ -71,13 +71,14 @@ def create_from_capability(
"""Bootstrap a draft workspace from inspect_capability wrapper hints."""
context = load_cli_context(ctx)
emit_json(
asyncio.run(
run_cli_operation(
context,
context.handlers.create_draft_workspace_from_capability(
workspace_id=workspace_id,
capability_name=capability_name,
name=name,
title=title,
)
),
)
)
@@ -105,12 +106,13 @@ def patch_draft(
raise typer.BadParameter("draft patch input must be a JSON array")
context = load_cli_context(ctx)
emit_json(
asyncio.run(
run_cli_operation(
context,
context.handlers.patch_draft_workspace(
workspace_id=workspace_id,
revision=revision,
patch=patch,
)
),
)
)
@@ -123,8 +125,9 @@ def validate_draft(
"""Validate one stored draft workspace."""
context = load_cli_context(ctx)
emit_json(
asyncio.run(
context.handlers.validate_draft_workspace(workspace_id=workspace_id)
run_cli_operation(
context,
context.handlers.validate_draft_workspace(workspace_id=workspace_id),
)
)
@@ -158,7 +161,8 @@ def save_draft(
raise typer.BadParameter(str(exc)) from exc
context = load_cli_context(ctx)
if kind == "wrapper":
payload = asyncio.run(
payload = run_cli_operation(
context,
context.handlers.create_wrapper_from_workspace(
workspace_id=workspace_id,
artifact_id=artifact_id,
@@ -167,10 +171,11 @@ def save_draft(
outcomes=tuple(outcome or ["ok"]),
description=description,
source_bindings=source_bindings or None,
)
),
)
else:
payload = asyncio.run(
payload = run_cli_operation(
context,
context.handlers.create_artifact_from_workspace(
workspace_id=workspace_id,
artifact_id=artifact_id,
@@ -180,6 +185,6 @@ def save_draft(
kind=kind,
description=description,
source_bindings=source_bindings or None,
)
),
)
emit_json(payload)
+11 -9
View File
@@ -1,6 +1,4 @@
from __future__ import annotations
import asyncio
from pathlib import Path
from typing import Annotated
@@ -8,6 +6,7 @@ import typer
from wf_cli.context import load_cli_context_from_typer
from wf_cli.io import CliInputError, emit_json, parse_json_input
from wf_cli.remote_errors import run_cli_operation
from wf_api import TraceRange
app = typer.Typer(
@@ -47,12 +46,13 @@ def start_run(
raise typer.BadParameter(str(exc)) from exc
context = load_cli_context_from_typer(ctx)
trace_range = _optional_trace_range(start=trace_from, limit=trace_limit)
payload = asyncio.run(
payload = run_cli_operation(
context,
context.handlers.run_deployment(
deployment_id=deployment_id,
workflow_input=workflow_input,
trace_range=trace_range,
)
),
)
emit_json(payload)
@@ -64,7 +64,7 @@ def inspect_run(
) -> None:
"""Inspect a durable run without trace entries."""
context = load_cli_context_from_typer(ctx)
emit_json(asyncio.run(context.handlers.inspect_run(run_id=run_id)))
emit_json(run_cli_operation(context, context.handlers.inspect_run(run_id=run_id)))
@app.command("resume")
@@ -108,13 +108,14 @@ def resume_run(
raise typer.BadParameter(str(exc)) from exc
context = load_cli_context_from_typer(ctx)
trace_range = _optional_trace_range(start=trace_from, limit=trace_limit)
payload = asyncio.run(
payload = run_cli_operation(
context,
context.handlers.resume_run(
run_id=run_id,
resume_payload=resume_payload,
resume_outcome=outcome,
trace_range=trace_range,
)
),
)
emit_json(payload)
@@ -134,11 +135,12 @@ def trace_run(
) -> None:
"""Read a bounded debug trace slice."""
context = load_cli_context_from_typer(ctx)
payload = asyncio.run(
payload = run_cli_operation(
context,
context.handlers.read_run_trace(
run_id=run_id,
trace_range=TraceRange(start=trace_from, limit=limit),
)
),
)
emit_json(payload)
+27 -10
View File
@@ -1,6 +1,4 @@
from __future__ import annotations
import asyncio
import json
from pathlib import Path
from typing import Annotated, Any
@@ -10,6 +8,7 @@ import typer
from wf_cli.context import CliContext, load_cli_context_from_typer
from wf_cli.formats import ListOutputFormat, emit_list_payload
from wf_cli.io import emit_json
from wf_cli.remote_errors import run_cli_operation
app = typer.Typer(
name="registry",
@@ -43,7 +42,10 @@ def list_registry_entries(
"""List desired persisted source registry entries."""
context = load_cli_context_from_typer(ctx)
admin = _require_registry_admin(context)
payload = asyncio.run(admin.list_registry_entries(cursor=cursor, limit=limit))
payload = run_cli_operation(
context,
admin.list_registry_entries(cursor=cursor, limit=limit),
)
emit_list_payload(
payload,
collection_key="entries",
@@ -61,7 +63,10 @@ def inspect_registry_entry(
"""Inspect one desired persisted source registry entry."""
context = load_cli_context_from_typer(ctx)
admin = _require_registry_admin(context)
payload = asyncio.run(admin.inspect_registry_entry(source_id=source_id))
payload = run_cli_operation(
context,
admin.inspect_registry_entry(source_id=source_id),
)
emit_json(payload)
@@ -80,7 +85,7 @@ def add_registry_entry(
context = load_cli_context_from_typer(ctx)
admin = _require_registry_admin(context)
entry = _read_json_arg(input_json, input_file, "--input/--input-file")
payload = asyncio.run(admin.add_registry_entry(entry=entry))
payload = run_cli_operation(context, admin.add_registry_entry(entry=entry))
emit_json(payload)
@@ -99,7 +104,10 @@ def update_registry_entry(
context = load_cli_context_from_typer(ctx)
admin = _require_registry_admin(context)
patch = _read_json_arg(patch_json, patch_file, "--patch/--patch-file")
payload = asyncio.run(admin.update_registry_entry(source_id=source_id, patch=patch))
payload = run_cli_operation(
context,
admin.update_registry_entry(source_id=source_id, patch=patch),
)
emit_json(payload)
@@ -111,7 +119,10 @@ def enable_registry_entry(
"""Enable a desired source registry entry."""
context = load_cli_context_from_typer(ctx)
admin = _require_registry_admin(context)
payload = asyncio.run(admin.enable_registry_entry(source_id=source_id))
payload = run_cli_operation(
context,
admin.enable_registry_entry(source_id=source_id),
)
emit_json(payload)
@@ -123,7 +134,10 @@ def disable_registry_entry(
"""Disable a desired source registry entry."""
context = load_cli_context_from_typer(ctx)
admin = _require_registry_admin(context)
payload = asyncio.run(admin.disable_registry_entry(source_id=source_id))
payload = run_cli_operation(
context,
admin.disable_registry_entry(source_id=source_id),
)
emit_json(payload)
@@ -140,7 +154,10 @@ def remove_registry_entry(
admin = _require_registry_admin(context)
if not confirm:
raise typer.BadParameter("removal requires --confirm flag")
payload = asyncio.run(admin.remove_registry_entry(source_id=source_id))
payload = run_cli_operation(
context,
admin.remove_registry_entry(source_id=source_id),
)
emit_json(payload)
@@ -149,7 +166,7 @@ def apply_registry_changes(ctx: typer.Context) -> None:
"""Apply desired registry state to the running server."""
context = load_cli_context_from_typer(ctx)
admin = _require_registry_admin(context)
payload = asyncio.run(admin.apply_registry_changes())
payload = run_cli_operation(context, admin.apply_registry_changes())
emit_json(payload)
+9 -4
View File
@@ -1,6 +1,4 @@
from __future__ import annotations
import asyncio
from typing import Annotated
import typer
@@ -8,6 +6,7 @@ import typer
from wf_cli.context import load_cli_context_from_typer
from wf_cli.formats import ListOutputFormat, emit_list_payload
from wf_cli.io import emit_json
from wf_cli.remote_errors import run_cli_operation
app = typer.Typer(
name="source",
@@ -31,7 +30,10 @@ def list_sources(
) -> None:
"""List compact workflow source summaries."""
context = load_cli_context_from_typer(ctx)
payload = asyncio.run(context.source_admin.list_sources(cursor=cursor, limit=limit))
payload = run_cli_operation(
context,
context.source_admin.list_sources(cursor=cursor, limit=limit),
)
emit_list_payload(
payload,
collection_key="sources",
@@ -48,5 +50,8 @@ def inspect_source(
) -> None:
"""Inspect one workflow source inventory."""
context = load_cli_context_from_typer(ctx)
payload = asyncio.run(context.source_admin.inspect_source(source_id=source_id))
payload = run_cli_operation(
context,
context.source_admin.inspect_source(source_id=source_id),
)
emit_json(payload)
+9
View File
@@ -40,6 +40,7 @@ class CliContext:
source_admin: WorkflowSourceAdminSurface
admin: WorkflowAdminSurface
source_registry_admin: WorkflowSourceRegistrySurface | None = None
verbose: bool = False
@dataclass(frozen=True)
@@ -63,6 +64,7 @@ class CliTyperState:
force_local: bool = False
rpc_url: str | None = None
rpc_timeout_seconds: float | None = None
verbose: bool = False
@classmethod
def from_context(cls, ctx: typer.Context) -> CliTyperState:
@@ -87,6 +89,7 @@ class CliTyperState:
rpc_timeout_seconds=(
float(timeout) if isinstance(timeout, float | int) else None
),
verbose=bool(obj.get("verbose", cls.verbose)),
)
@@ -101,6 +104,7 @@ def load_cli_context(
force_local: bool = False,
rpc_url: str | None = None,
rpc_timeout_seconds: float | None = None,
verbose: bool = False,
) -> CliContext:
"""Load config and build workflow-surface handlers for CLI commands."""
resolved_config_path = Path(config_path)
@@ -123,6 +127,7 @@ def load_cli_context(
source_admin=client,
admin=client,
source_registry_admin=client,
verbose=verbose,
)
if _is_legacy_mcp_config(resolved_config_path):
@@ -137,6 +142,7 @@ def load_cli_context(
connections=service.connection_service,
events=service.events,
),
verbose=verbose,
)
config = load_workflow_config(resolved_config_path)
@@ -152,6 +158,7 @@ def load_cli_context(
handlers=server.api,
source_admin=server.source_admin,
admin=server.admin,
verbose=verbose,
)
if isinstance(target, RpcHttpTargetConfig):
client = RpcWorkflowApiClient(
@@ -169,6 +176,7 @@ def load_cli_context(
source_admin=client,
admin=client,
source_registry_admin=client,
verbose=verbose,
)
raise ValueError(f"unsupported workflow target {target!r}")
@@ -218,6 +226,7 @@ def load_cli_context_from_typer(ctx: typer.Context) -> CliContext:
force_local=force_local_from_context(ctx),
rpc_url=rpc_url_from_context(ctx),
rpc_timeout_seconds=rpc_timeout_from_context(ctx),
verbose=CliTyperState.from_context(ctx).verbose,
)
except ValueError as exc:
raise typer.BadParameter(str(exc)) from exc
+36
View File
@@ -0,0 +1,36 @@
from __future__ import annotations
import asyncio
from collections.abc import Coroutine
from typing import Any, TypeVar
import click
import httpx
from wf_cli.context import CliContext
T = TypeVar("T")
def run_cli_operation(context: CliContext, operation: Coroutine[Any, Any, T]) -> T:
"""Run a CLI async operation and format non-verbose operation errors.
Non-verbose CLI output should be useful to users, not a Python crash report.
`--verbose` preserves the raw exception path so developers can still debug
internal failures with a traceback.
"""
try:
return asyncio.run(operation)
except (RuntimeError, httpx.HTTPError) as exc:
if context.verbose:
raise
raise click.ClickException(_operation_error_message(exc)) from exc
def _operation_error_message(exc: RuntimeError | httpx.HTTPError) -> str:
"""Return a stable non-empty message for compact CLI error output."""
message = str(exc)
if message:
return message
return exc.__class__.__name__