feat: replace draft input bindings from cli
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user