feat: add draft decision and subgraph commands
This commit is contained in:
@@ -133,7 +133,7 @@ def _canonical_draft_if_valid(
|
||||
"""
|
||||
if validation_status != "valid":
|
||||
return draft
|
||||
return WorkflowDraft.model_validate(draft).model_dump(mode="json")
|
||||
return WorkflowDraft.model_validate(draft).model_dump(mode="json", by_alias=True)
|
||||
|
||||
|
||||
def _revision_conflict_payload(
|
||||
|
||||
@@ -4,9 +4,12 @@ from pathlib import Path
|
||||
from typing import Annotated, Literal
|
||||
|
||||
import typer
|
||||
from pydantic import ValidationError
|
||||
from pydantic import TypeAdapter, ValidationError
|
||||
|
||||
from wf_artifacts.drafts.models import (
|
||||
DraftChooseClause,
|
||||
DraftChoosePayload,
|
||||
DraftChooseStep,
|
||||
DraftEndPayload,
|
||||
DraftEndStep,
|
||||
DraftForeachPayload,
|
||||
@@ -14,15 +17,24 @@ from wf_artifacts.drafts.models import (
|
||||
DraftInterruptPayload,
|
||||
DraftInterruptStep,
|
||||
DraftJoinStep,
|
||||
DraftMatchCase,
|
||||
DraftMatchPayload,
|
||||
DraftMatchStep,
|
||||
DraftStep,
|
||||
DraftSubgraphPayload,
|
||||
DraftSubgraphStep,
|
||||
DraftWhenPayload,
|
||||
DraftWhenStep,
|
||||
)
|
||||
from wf_cli.context import load_cli_context_from_typer as load_cli_context
|
||||
from wf_cli.io import emit_json
|
||||
from wf_cli.remote_errors import run_cli_operation
|
||||
from wf_core.models.conditions import Condition
|
||||
from wf_core.models.schemas import SchemaRef
|
||||
from wf_core.models.steps import (
|
||||
ForeachConcurrentPolicy,
|
||||
)
|
||||
from wf_core.models.workflow_refs import WorkflowRef
|
||||
|
||||
from .draft_options import (
|
||||
_parse_map_flags,
|
||||
@@ -39,6 +51,10 @@ app = typer.Typer(
|
||||
no_args_is_help=True,
|
||||
)
|
||||
|
||||
_condition_adapter = TypeAdapter(Condition)
|
||||
_choose_clauses_adapter = TypeAdapter(list[DraftChooseClause])
|
||||
_match_cases_adapter = TypeAdapter(list[DraftMatchCase])
|
||||
|
||||
|
||||
def _submit_step(
|
||||
ctx: typer.Context,
|
||||
@@ -422,19 +438,266 @@ def add_end_step(
|
||||
)
|
||||
|
||||
|
||||
# These subgroups establish the public boundary for the next command slice.
|
||||
# They intentionally have no bodies until their typed decision options are ready.
|
||||
for _name in (
|
||||
"when",
|
||||
"choose",
|
||||
"match",
|
||||
"subgraph",
|
||||
):
|
||||
app.add_typer(
|
||||
typer.Typer(
|
||||
name=_name,
|
||||
help=f"Add a {_name} step (available in a later CLI task).",
|
||||
no_args_is_help=True,
|
||||
),
|
||||
name=_name,
|
||||
@app.command("when")
|
||||
def add_when_step(
|
||||
ctx: typer.Context,
|
||||
workspace_id: Annotated[str, typer.Argument(help="Draft workspace id.")],
|
||||
revision: Annotated[
|
||||
int, typer.Option("--revision", min=1, help="Expected workspace revision.")
|
||||
],
|
||||
step_id: Annotated[str, typer.Option("--step", help="New draft step id.")],
|
||||
condition_file: Annotated[
|
||||
Path, typer.Option("--condition-file", help="JSON condition expression.")
|
||||
],
|
||||
then: Annotated[str, typer.Option("--then", help="Target when true.")],
|
||||
otherwise: Annotated[
|
||||
str, typer.Option("--otherwise", help="Target when false.")
|
||||
] = "__end__",
|
||||
from_step: Annotated[
|
||||
str | None, typer.Option("--from-step", help="Incoming step id.")
|
||||
] = None,
|
||||
from_outcome: Annotated[
|
||||
str | None,
|
||||
typer.Option("--from-outcome", help="Outcome on --from-step (default: ok)."),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Add a boolean decision whose targets are embedded in the step."""
|
||||
try:
|
||||
condition = _condition_adapter.validate_python(
|
||||
parse_json_file(condition_file, option_name="--condition-file")
|
||||
)
|
||||
step = DraftWhenStep(
|
||||
when=DraftWhenPayload.model_validate(
|
||||
{"if": condition, "then": then, "otherwise": otherwise}
|
||||
)
|
||||
)
|
||||
except ValidationError as exc:
|
||||
raise _as_bad_parameter(exc) from exc
|
||||
_submit_step(
|
||||
ctx,
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
step_id=step_id,
|
||||
step=step,
|
||||
from_step=from_step,
|
||||
from_outcome=from_outcome,
|
||||
routes=None,
|
||||
)
|
||||
|
||||
|
||||
@app.command("choose")
|
||||
def add_choose_step(
|
||||
ctx: typer.Context,
|
||||
workspace_id: Annotated[str, typer.Argument(help="Draft workspace id.")],
|
||||
revision: Annotated[
|
||||
int, typer.Option("--revision", min=1, help="Expected workspace revision.")
|
||||
],
|
||||
step_id: Annotated[str, typer.Option("--step", help="New draft step id.")],
|
||||
clauses_file: Annotated[
|
||||
Path,
|
||||
typer.Option("--clauses-file", help="JSON array of ordered if/then clauses."),
|
||||
],
|
||||
default: Annotated[
|
||||
str, typer.Option("--default", help="Target when no clause matches.")
|
||||
] = "__end__",
|
||||
from_step: Annotated[
|
||||
str | None, typer.Option("--from-step", help="Incoming step id.")
|
||||
] = None,
|
||||
from_outcome: Annotated[
|
||||
str | None,
|
||||
typer.Option("--from-outcome", help="Outcome on --from-step (default: ok)."),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Add an ordered first-true decision with embedded targets."""
|
||||
try:
|
||||
clauses = _choose_clauses_adapter.validate_python(
|
||||
parse_json_file(clauses_file, option_name="--clauses-file")
|
||||
)
|
||||
step = DraftChooseStep(
|
||||
choose=DraftChoosePayload(clauses=clauses, default=default)
|
||||
)
|
||||
except ValidationError as exc:
|
||||
raise _as_bad_parameter(exc) from exc
|
||||
_submit_step(
|
||||
ctx,
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
step_id=step_id,
|
||||
step=step,
|
||||
from_step=from_step,
|
||||
from_outcome=from_outcome,
|
||||
routes=None,
|
||||
)
|
||||
|
||||
|
||||
@app.command("match")
|
||||
def add_match_step(
|
||||
ctx: typer.Context,
|
||||
workspace_id: Annotated[str, typer.Argument(help="Draft workspace id.")],
|
||||
revision: Annotated[
|
||||
int, typer.Option("--revision", min=1, help="Expected workspace revision.")
|
||||
],
|
||||
step_id: Annotated[str, typer.Option("--step", help="New draft step id.")],
|
||||
value: Annotated[str, typer.Option("--value", help="Graph path to compare.")],
|
||||
cases_file: Annotated[
|
||||
Path,
|
||||
typer.Option("--cases-file", help="JSON array of ordered equals/then cases."),
|
||||
],
|
||||
default: Annotated[
|
||||
str, typer.Option("--default", help="Target when no case matches.")
|
||||
] = "__end__",
|
||||
from_step: Annotated[
|
||||
str | None, typer.Option("--from-step", help="Incoming step id.")
|
||||
] = None,
|
||||
from_outcome: Annotated[
|
||||
str | None,
|
||||
typer.Option("--from-outcome", help="Outcome on --from-step (default: ok)."),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Add an ordered scalar match decision with embedded targets."""
|
||||
try:
|
||||
cases = _match_cases_adapter.validate_python(
|
||||
parse_json_file(cases_file, option_name="--cases-file")
|
||||
)
|
||||
step = DraftMatchStep(
|
||||
match=DraftMatchPayload(value=value, cases=cases, default=default)
|
||||
)
|
||||
except ValidationError as exc:
|
||||
raise _as_bad_parameter(exc) from exc
|
||||
_submit_step(
|
||||
ctx,
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
step_id=step_id,
|
||||
step=step,
|
||||
from_step=from_step,
|
||||
from_outcome=from_outcome,
|
||||
routes=None,
|
||||
)
|
||||
|
||||
|
||||
@app.command("subgraph")
|
||||
def add_subgraph_step(
|
||||
ctx: typer.Context,
|
||||
workspace_id: Annotated[str, typer.Argument(help="Draft workspace id.")],
|
||||
revision: Annotated[
|
||||
int, typer.Option("--revision", min=1, help="Expected workspace revision.")
|
||||
],
|
||||
step_id: Annotated[str, typer.Option("--step", help="New draft step id.")],
|
||||
workflow_name: Annotated[
|
||||
str | None,
|
||||
typer.Option("--workflow-name", help="Local child workflow registry name."),
|
||||
] = None,
|
||||
artifact_id: Annotated[
|
||||
str | None,
|
||||
typer.Option("--artifact-id", help="Saved child workflow artifact id."),
|
||||
] = None,
|
||||
artifact_version: Annotated[
|
||||
int | None,
|
||||
typer.Option("--artifact-version", min=1, help="Saved artifact version."),
|
||||
] = None,
|
||||
description: Annotated[
|
||||
str | None, typer.Option("--description", help="Boundary description.")
|
||||
] = None,
|
||||
input_schema_file: Annotated[
|
||||
Path | None,
|
||||
typer.Option("--input-schema-file", help="Child input JSON Schema."),
|
||||
] = None,
|
||||
output_schema_file: Annotated[
|
||||
Path | None,
|
||||
typer.Option("--output-schema-file", help="Child output JSON Schema."),
|
||||
] = None,
|
||||
input_mapping: Annotated[
|
||||
list[str] | None,
|
||||
typer.Option(
|
||||
"--input",
|
||||
help="Input binding GRAPH_SOURCE=LOCAL_TARGET. Repeat as needed.",
|
||||
),
|
||||
] = None,
|
||||
output_mapping: Annotated[
|
||||
list[str] | None,
|
||||
typer.Option(
|
||||
"--bind-output",
|
||||
help="Output binding LOCAL_SOURCE=STATE_TARGET. Repeat as needed.",
|
||||
),
|
||||
] = None,
|
||||
outcome: Annotated[
|
||||
list[str] | None,
|
||||
typer.Option("--outcome", help="Declared child outcome. Repeat as needed."),
|
||||
] = None,
|
||||
from_step: Annotated[
|
||||
str | None, typer.Option("--from-step", help="Incoming step id.")
|
||||
] = None,
|
||||
from_outcome: Annotated[
|
||||
str | None,
|
||||
typer.Option("--from-outcome", help="Outcome on --from-step (default: ok)."),
|
||||
] = None,
|
||||
route: Annotated[
|
||||
list[str] | None,
|
||||
typer.Option("--route", help="Route mapping OUTCOME=TARGET. Repeat as needed."),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Add a child-workflow boundary with an explicit reference and contract."""
|
||||
if workflow_name is not None:
|
||||
if artifact_id is not None or artifact_version is not None:
|
||||
raise typer.BadParameter(
|
||||
"--workflow-name cannot be combined with "
|
||||
"--artifact-id/--artifact-version"
|
||||
)
|
||||
workflow_data: dict[str, str | int] = {"name": workflow_name}
|
||||
else:
|
||||
if artifact_id is None or artifact_version is None:
|
||||
raise typer.BadParameter(
|
||||
"use --workflow-name or both --artifact-id and --artifact-version"
|
||||
)
|
||||
workflow_data = {"artifact_id": artifact_id, "version": artifact_version}
|
||||
|
||||
input_map = _parse_step_input_map_flags(input_mapping, option_name="--input")
|
||||
output_map = _parse_output_map_flags(output_mapping)
|
||||
try:
|
||||
workflow = WorkflowRef.model_validate(workflow_data)
|
||||
input_schema = (
|
||||
SchemaRef.model_validate(
|
||||
parse_json_file(input_schema_file, option_name="--input-schema-file")
|
||||
)
|
||||
if input_schema_file is not None
|
||||
else SchemaRef(type="object")
|
||||
)
|
||||
output_schema = (
|
||||
SchemaRef.model_validate(
|
||||
parse_json_file(output_schema_file, option_name="--output-schema-file")
|
||||
)
|
||||
if output_schema_file is not None
|
||||
else SchemaRef(type="object")
|
||||
)
|
||||
step = DraftSubgraphStep(
|
||||
subgraph=DraftSubgraphPayload.model_validate(
|
||||
{
|
||||
"workflow": workflow,
|
||||
"desc": description,
|
||||
"input_schema": input_schema,
|
||||
"output_schema": output_schema,
|
||||
"input": [
|
||||
{"path": source, "target": target}
|
||||
for source, target in input_map.items()
|
||||
],
|
||||
"output": [
|
||||
{"source": source, "target": target}
|
||||
for source, target in output_map.items()
|
||||
],
|
||||
"outcomes": outcome or ["ok"],
|
||||
}
|
||||
)
|
||||
)
|
||||
except ValidationError as exc:
|
||||
raise _as_bad_parameter(exc) from exc
|
||||
_submit_step(
|
||||
ctx,
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
step_id=step_id,
|
||||
step=step,
|
||||
from_step=from_step,
|
||||
from_outcome=from_outcome,
|
||||
routes=_parse_route_flags(route) or None,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user