feat: replace draft input bindings from cli

This commit is contained in:
lda
2026-07-22 22:55:25 +07:00 Verified
parent 448895e831
commit 88e51901a7
5 changed files with 526 additions and 34 deletions
+8 -12
View File
@@ -42,6 +42,7 @@ from .draft_options import (
_parse_step_input_map_flags,
parse_json_file,
route_source,
validation_error_as_bad_parameter,
)
app = typer.Typer(
@@ -83,11 +84,6 @@ def _submit_step(
)
def _as_bad_parameter(exc: ValidationError) -> typer.BadParameter:
"""Keep model validation failures on Click's concise input-error surface."""
return typer.BadParameter(str(exc))
@app.command("capability")
def add_step_from_capability(
ctx: typer.Context,
@@ -270,7 +266,7 @@ def add_interrupt_step(
)
)
except ValidationError as exc:
raise _as_bad_parameter(exc) from exc
raise validation_error_as_bad_parameter(exc) from exc
_submit_step(
ctx,
@@ -365,7 +361,7 @@ def add_foreach_step(
)
)
except ValidationError as exc:
raise _as_bad_parameter(exc) from exc
raise validation_error_as_bad_parameter(exc) from exc
_submit_step(
ctx,
@@ -443,7 +439,7 @@ def add_end_step(
try:
step = DraftEndStep(end=DraftEndPayload(outcome=outcome))
except ValidationError as exc:
raise _as_bad_parameter(exc) from exc
raise validation_error_as_bad_parameter(exc) from exc
_submit_step(
ctx,
workspace_id=workspace_id,
@@ -494,7 +490,7 @@ def add_when_step(
)
)
except ValidationError as exc:
raise _as_bad_parameter(exc) from exc
raise validation_error_as_bad_parameter(exc) from exc
_submit_step(
ctx,
workspace_id=workspace_id,
@@ -543,7 +539,7 @@ def add_choose_step(
choose=DraftChoosePayload(clauses=clauses, default=default)
)
except ValidationError as exc:
raise _as_bad_parameter(exc) from exc
raise validation_error_as_bad_parameter(exc) from exc
_submit_step(
ctx,
workspace_id=workspace_id,
@@ -593,7 +589,7 @@ def add_match_step(
match=DraftMatchPayload(value=value, cases=cases, default=default)
)
except ValidationError as exc:
raise _as_bad_parameter(exc) from exc
raise validation_error_as_bad_parameter(exc) from exc
_submit_step(
ctx,
workspace_id=workspace_id,
@@ -724,7 +720,7 @@ def add_subgraph_step(
)
)
except ValidationError as exc:
raise _as_bad_parameter(exc) from exc
raise validation_error_as_bad_parameter(exc) from exc
_submit_step(
ctx,
workspace_id=workspace_id,
+84 -1
View File
@@ -5,9 +5,13 @@ from pathlib import Path
from typing import Any
import typer
from pydantic import TypeAdapter, ValidationError
from wf_api.surface import RouteSource
from wf_core.paths import LocalPath, PathResolutionError, StatePath
from wf_core.models.steps import InputBinding, InputPathBinding, InputValueBinding
from wf_core.paths import GraphSourcePath, LocalPath, PathResolutionError, StatePath
_INPUT_BINDINGS_ADAPTER = TypeAdapter(list[InputBinding])
def _parse_assignment_flags(
@@ -87,6 +91,85 @@ def _parse_step_input_map_flags(
return parsed
def validation_error_as_bad_parameter(
exc: ValidationError,
) -> typer.BadParameter:
"""Keep Pydantic failures on Click's concise input-error surface."""
return typer.BadParameter(str(exc))
def parse_step_input_binding_flags(
values: list[str] | None,
) -> list[InputPathBinding]:
"""Parse ordered canonical path bindings without collapsing source fan-out."""
bindings: list[InputPathBinding] = []
for item in values or []:
source, separator, target = item.partition("=")
if separator != "=" or not source or not target:
raise typer.BadParameter("--map must use GRAPH_SOURCE=LOCAL_TARGET")
if target.startswith("local."):
bare_target = target.removeprefix("local.")
raise typer.BadParameter(
"--map target must be a rootless node-local path; "
f"use {source}={bare_target}, not {source}={target}"
)
try:
source_path = GraphSourcePath.parse(source)
except PathResolutionError as exc:
raise typer.BadParameter(
f"--map source must be a graph source path: {exc}"
) from exc
try:
target_path = LocalPath.parse(target)
except PathResolutionError as exc:
raise typer.BadParameter(
f"--map target must be a rootless node-local path: {exc}"
) from exc
bindings.append(InputPathBinding(path=source_path, target=target_path))
return bindings
def parse_step_input_value_flags(
values: list[str] | None,
) -> list[InputValueBinding]:
"""Parse ordered canonical literal bindings from LOCAL_TARGET=JSON flags."""
bindings: list[InputValueBinding] = []
for item in values or []:
target, separator, raw_value = item.partition("=")
if separator != "=" or not target:
raise typer.BadParameter("--value must use LOCAL_TARGET=JSON")
if target.startswith("local."):
raise typer.BadParameter(
"--value target must be a rootless node-local path"
)
try:
value = json.loads(raw_value)
bindings.append(
InputValueBinding(target=LocalPath.parse(target), value=value)
)
except json.JSONDecodeError as exc:
raise typer.BadParameter(
f"--value for {target!r} is invalid JSON: {exc.msg}"
) from exc
except ValidationError as exc:
raise validation_error_as_bad_parameter(exc) from exc
except PathResolutionError as exc:
raise typer.BadParameter(
f"--value target must be a valid rootless local path: {exc}"
) from exc
return bindings
def parse_step_input_bindings_file(path: Path) -> list[InputBinding]:
"""Read and validate an ordered canonical input-binding list."""
try:
return _INPUT_BINDINGS_ADAPTER.validate_python(
parse_json_file(path, option_name="--bindings-file")
)
except ValidationError as exc:
raise validation_error_as_bad_parameter(exc) from exc
def _parse_route_flags(values: list[str] | None) -> dict[str, str]:
return _parse_assignment_flags(
values,
+78 -14
View File
@@ -12,6 +12,9 @@ from wf_cli.commands.draft_options import (
_parse_route_flags,
_parse_step_input_map_flags,
parse_json_object_file,
parse_step_input_binding_flags,
parse_step_input_bindings_file,
parse_step_input_value_flags,
)
from wf_cli.context import load_cli_context_from_typer as load_cli_context
from wf_cli.formats import ListOutputFormat, emit_list_payload
@@ -371,7 +374,7 @@ def set_draft_route(
@app.command("set-input")
def set_step_input_map(
def set_step_input(
ctx: typer.Context,
workspace_id: Annotated[str, typer.Argument(help="Draft workspace id.")],
revision: Annotated[
@@ -385,19 +388,40 @@ def set_step_input_map(
help="One input binding SOURCE=LOCAL_TARGET. Repeat in one command.",
),
] = None,
literal_values: Annotated[
list[str] | None,
typer.Option(
"--value",
help="Literal input binding LOCAL_TARGET=JSON. Repeat in one command.",
),
] = None,
bindings_file: Annotated[
Path | None,
typer.Option(
"--bindings-file",
help="JSON file containing the complete ordered canonical binding list.",
),
] = None,
clear: Annotated[
bool,
typer.Option("--clear", help="Replace the step's input bindings with []."),
] = False,
merge: Annotated[
bool,
typer.Option(
"--merge",
help="Preserve existing input bindings and add/update the passed --map entries.",
help=(
"Compatibility map-only mode: preserve existing bindings and "
"add/update --map entries."
),
),
] = False,
) -> None:
"""Set one step's input map without writing JSON Patch manually.
"""Replace one step's canonical inputs, or use compatibility map merge.
Default behavior replaces the full input map for this step. Pass all desired
--map entries in one command for a complete replacement. Use --merge only
when adding or updating entries across a later revision.
By default, repeated --map and --value flags replace the complete ordered
binding list. --bindings-file replaces from canonical JSON, while --clear
sends an empty list. Use --merge only with map-only compatibility edits.
Targets are rootless node-local paths. For example, use
`--map input.title=report.title`, not
@@ -408,18 +432,58 @@ def set_step_input_map(
Run `wf draft validate <workspace_id>` after map edits; validation reports
unresolved paths and conflicting writes.
"""
input_map = _parse_step_input_map_flags(mapping)
has_flags = bool(mapping or literal_values)
has_file = bindings_file is not None
selected_modes = sum((has_flags, has_file, clear))
if selected_modes == 0:
raise typer.BadParameter("provide --map/--value, --bindings-file, or --clear")
if selected_modes > 1:
raise typer.BadParameter(
"--bindings-file and --clear cannot be combined with --map or --value"
)
if merge and (literal_values or has_file or clear):
raise typer.BadParameter(
"--merge is supported only for compatibility map-only edits"
)
if merge:
input_map = _parse_step_input_map_flags(mapping)
bindings = None
else:
input_map = None
bindings = (
parse_step_input_bindings_file(bindings_file)
if bindings_file is not None
else []
if clear
else [
*parse_step_input_binding_flags(mapping),
*parse_step_input_value_flags(literal_values),
]
)
context = load_cli_context(ctx)
if merge:
assert input_map is not None
operation = context.handlers.set_step_input_map(
workspace_id=workspace_id,
revision=revision,
step_id=step_id,
input_map=input_map,
merge=True,
)
else:
assert bindings is not None
operation = context.handlers.set_step_input_bindings(
workspace_id=workspace_id,
revision=revision,
step_id=step_id,
bindings=bindings,
)
emit_json(
run_cli_operation(
context,
context.handlers.set_step_input_map(
workspace_id=workspace_id,
revision=revision,
step_id=step_id,
input_map=input_map,
merge=merge,
),
operation,
)
)