feat: replace workflow output bindings from cli

This commit is contained in:
lda
2026-07-26 21:26:06 +07:00 Verified
parent 54a32c7f41
commit b0a01a63b0
4 changed files with 582 additions and 29 deletions
+48 -6
View File
@@ -107,7 +107,16 @@ def validation_error_as_bad_parameter(
def parse_step_input_binding_flags(
values: list[str] | None,
) -> list[InputPathBinding]:
"""Parse ordered canonical path bindings without collapsing source fan-out."""
"""Parse ordered step-input path bindings without collapsing source fan-out."""
return _parse_input_path_binding_flags(values, target_label="node-local")
def _parse_input_path_binding_flags(
values: list[str] | None,
*,
target_label: str,
) -> list[InputPathBinding]:
"""Parse ordered GRAPH_SOURCE=LOCAL_TARGET bindings for one CLI audience."""
bindings: list[InputPathBinding] = []
for item in values or []:
source, separator, target = item.partition("=")
@@ -116,7 +125,7 @@ def parse_step_input_binding_flags(
if target.startswith("local."):
bare_target = target.removeprefix("local.")
raise typer.BadParameter(
"--map target must be a rootless node-local path; "
f"--map target must be a rootless {target_label} path; "
f"use {source}={bare_target}, not {source}={target}"
)
try:
@@ -129,7 +138,7 @@ def parse_step_input_binding_flags(
target_path = LocalPath.parse(target)
except PathResolutionError as exc:
raise typer.BadParameter(
f"--map target must be a rootless node-local path: {exc}"
f"--map target must be a rootless {target_label} path: {exc}"
) from exc
bindings.append(InputPathBinding(path=source_path, target=target_path))
return bindings
@@ -138,7 +147,16 @@ def parse_step_input_binding_flags(
def parse_step_input_value_flags(
values: list[str] | None,
) -> list[InputValueBinding]:
"""Parse ordered canonical literal bindings from LOCAL_TARGET=JSON flags."""
"""Parse ordered step-input literal bindings from LOCAL_TARGET=JSON flags."""
return _parse_input_value_binding_flags(values, target_label="node-local")
def _parse_input_value_binding_flags(
values: list[str] | None,
*,
target_label: str,
) -> list[InputValueBinding]:
"""Parse ordered LOCAL_TARGET=JSON bindings for one CLI audience."""
bindings: list[InputValueBinding] = []
for item in values or []:
target, separator, raw_value = item.partition("=")
@@ -146,7 +164,7 @@ def parse_step_input_value_flags(
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"
f"--value target must be a rootless {target_label} path"
)
try:
value = json.loads(raw_value)
@@ -161,7 +179,7 @@ def parse_step_input_value_flags(
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}"
f"--value target must be a valid rootless {target_label} path: {exc}"
) from exc
return bindings
@@ -176,6 +194,30 @@ def parse_step_input_bindings_file(path: Path) -> list[InputBinding]:
raise validation_error_as_bad_parameter(exc) from exc
def parse_workflow_output_binding_flags(
values: list[str] | None,
) -> list[InputPathBinding]:
"""Parse ordered canonical workflow-output path bindings."""
return _parse_input_path_binding_flags(values, target_label="workflow-output")
def parse_workflow_output_value_flags(
values: list[str] | None,
) -> list[InputValueBinding]:
"""Parse ordered canonical workflow-output literal bindings."""
return _parse_input_value_binding_flags(values, target_label="workflow-output")
def parse_workflow_output_bindings_file(path: Path) -> list[InputBinding]:
"""Read an ordered canonical workflow-output 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_step_output_binding_flags(
values: list[str] | None,
) -> list[OutputBinding]:
+98 -19
View File
@@ -18,6 +18,9 @@ from wf_cli.commands.draft_options import (
parse_step_input_value_flags,
parse_step_output_binding_flags,
parse_step_output_bindings_file,
parse_workflow_output_binding_flags,
parse_workflow_output_bindings_file,
parse_workflow_output_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
@@ -607,47 +610,123 @@ def set_workflow_output(
typer.Option(
"--map",
help=(
"One output binding GRAPH_SOURCE=OUTPUT_FIELD, for example "
"state.markdown=markdown. Repeat in one command."
"GRAPH_SOURCE=OUTPUT_TARGET canonical path binding. Repeat to "
"replace the complete ordered workflow output binding list."
),
),
] = None,
literal_values: Annotated[
list[str] | None,
typer.Option(
"--value",
help=(
"OUTPUT_TARGET=JSON canonical literal binding. Repeat after "
"--map bindings in the replacement list."
),
),
] = None,
bindings_file: Annotated[
Path | None,
typer.Option(
"--bindings-file",
help=(
"Replace with an ordered canonical JSON array of path/value "
"workflow output bindings."
),
),
] = None,
clear: Annotated[
bool,
typer.Option(
"--clear",
help=(
"Remove explicit bindings and restore implicit same-name state "
"fallback from output_schema."
),
),
] = False,
merge: Annotated[
bool,
typer.Option(
"--merge",
help="Preserve existing workflow output bindings and add/update the passed --map entries.",
help=(
"Compatibility-only and potentially lossy: preserve existing "
"bindings and add/update map-only entries."
),
),
] = False,
) -> None:
"""Set the top-level workflow output projection without writing JSON Patch manually.
"""Replace the workflow's public output projection with canonical bindings.
Default behavior replaces the full workflow output map. 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, ``--map GRAPH_SOURCE=OUTPUT_TARGET`` and
``--value OUTPUT_TARGET=JSON`` replace the complete ordered workflow output
binding list. ``--bindings-file`` accepts an ordered canonical JSON array
and preserves exact path/value interleaving. ``--clear`` restores the
implicit same-name state fallback declared by ``output_schema``. Use
--merge only with --map for compatibility-only and potentially lossy edits.
This edits WorkflowDraft.output (top-level workflow output). Use
wf draft set-output for step-level output bindings.
For single-field input/state sources, missing output_schema fields are
projected automatically from the source schema.
Repeat --map for multiple mappings:
--map state.markdown=markdown --map state.title=title
To round-trip exact ordering, run wf draft inspect --include-draft and
export draft.output for a later --bindings-file replacement.
Run `wf draft validate <workspace_id>` after editing the projection.
"""
output_map = _parse_map_flags(mapping)
has_maps = bool(mapping)
has_values = bool(literal_values)
has_file = bindings_file is not None
has_convenience = has_maps or has_values
if not has_convenience and not has_file and not clear:
raise typer.BadParameter("provide --map, --value, --bindings-file, or --clear")
if merge and (has_values or has_file or clear):
raise typer.BadParameter(
"--merge is supported only for compatibility map-only edits"
)
if has_file and (has_convenience or clear):
raise typer.BadParameter(
"--bindings-file is mutually exclusive with --map, --value, and --clear"
)
if clear and has_convenience:
raise typer.BadParameter("--clear is mutually exclusive with --map and --value")
if merge:
output_map = _parse_map_flags(mapping)
bindings = None
else:
output_map = None
bindings = (
parse_workflow_output_bindings_file(bindings_file)
if bindings_file is not None
else []
if clear
else [
*parse_workflow_output_binding_flags(mapping),
*parse_workflow_output_value_flags(literal_values),
]
)
context = load_cli_context(ctx)
if merge:
assert output_map is not None
operation = context.handlers.set_workflow_output_map(
workspace_id=workspace_id,
revision=revision,
output_map=output_map,
merge=True,
)
else:
assert bindings is not None
operation = context.handlers.set_workflow_output_bindings(
workspace_id=workspace_id,
revision=revision,
bindings=bindings,
)
emit_json(
run_cli_operation(
context,
context.handlers.set_workflow_output_map(
workspace_id=workspace_id,
revision=revision,
output_map=output_map,
merge=merge,
),
operation,
)
)