p2: old style deprecated support + wf_authoring to use new stuff
holy moly file changes
This commit is contained in:
@@ -64,7 +64,7 @@ def resolve_operand(
|
||||
if isinstance(operand, LiteralOperand):
|
||||
return operand.value
|
||||
return safe_resolve_path(
|
||||
operand.path,
|
||||
str(operand.path),
|
||||
state=state,
|
||||
workflow_input=workflow_input,
|
||||
context={"prior_outcome": context_data},
|
||||
|
||||
@@ -49,7 +49,9 @@ def set_local_value(payload: dict[str, Any], path: str | LocalPath, value: Any)
|
||||
for part in parts[:-1]:
|
||||
next_value = current.setdefault(part, {})
|
||||
if not isinstance(next_value, dict):
|
||||
raise LocalPathError(f"local path {str(parsed)!r} overlaps an existing value")
|
||||
raise LocalPathError(
|
||||
f"local path {str(parsed)!r} overlaps an existing value"
|
||||
)
|
||||
current = next_value
|
||||
current[parts[-1]] = value
|
||||
|
||||
|
||||
@@ -4,11 +4,13 @@ from typing import Annotated, Any, Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from wf_core.paths import GraphSourcePath
|
||||
|
||||
|
||||
class PathOperand(BaseModel):
|
||||
"""Condition operand resolved from a workflow graph path."""
|
||||
|
||||
path: str
|
||||
path: GraphSourcePath
|
||||
|
||||
|
||||
class LiteralOperand(BaseModel):
|
||||
@@ -25,7 +27,7 @@ class ExistsCondition(BaseModel):
|
||||
"""Condition that is true when a graph path resolves to a present value."""
|
||||
|
||||
op: Literal["exists"]
|
||||
path: str
|
||||
path: GraphSourcePath
|
||||
|
||||
|
||||
class NotCondition(BaseModel):
|
||||
|
||||
@@ -1,22 +1,60 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
from jsonschema import Draft202012Validator, SchemaError, validators
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
field_serializer,
|
||||
field_validator,
|
||||
model_serializer,
|
||||
model_validator,
|
||||
)
|
||||
|
||||
from wf_core.models.reducers import ReducerRef
|
||||
from wf_core.paths import StatePath
|
||||
|
||||
|
||||
class SchemaRef(BaseModel):
|
||||
"""JSON-schema-like shape used at workflow boundaries."""
|
||||
"""JSON Schema object used at workflow and node boundaries."""
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
title: str | None = None
|
||||
type: str | None = None
|
||||
type: str | list[str] | None = None
|
||||
properties: dict[str, Any] = Field(default_factory=dict)
|
||||
required: list[str] = Field(default_factory=list)
|
||||
|
||||
@model_serializer(mode="wrap")
|
||||
def _serialize_without_none_fields(self, handler: Any) -> dict[str, Any]:
|
||||
"""Persist JSON Schema objects without null-valued optional keywords."""
|
||||
data = handler(self)
|
||||
return {key: value for key, value in data.items() if value is not None}
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _validate_json_schema_definition(cls, value: object) -> object:
|
||||
if isinstance(value, SchemaRef):
|
||||
schema = value.model_dump(mode="json", exclude_none=True)
|
||||
elif isinstance(value, Mapping):
|
||||
schema = dict(value)
|
||||
else:
|
||||
return value
|
||||
|
||||
validator_cls = (
|
||||
validators.validator_for(schema)
|
||||
if "$schema" in schema
|
||||
else Draft202012Validator
|
||||
)
|
||||
try:
|
||||
validator_cls.check_schema(schema)
|
||||
except SchemaError as exc:
|
||||
raise ValueError(f"invalid JSON Schema: {exc.message}") from exc
|
||||
return value
|
||||
|
||||
|
||||
class StateField(BaseModel):
|
||||
"""Declared state path plus its runtime merge behavior."""
|
||||
@@ -36,12 +74,123 @@ class StateField(BaseModel):
|
||||
return value
|
||||
|
||||
|
||||
class StateFieldDecl(BaseModel):
|
||||
"""One declared state path plus validation and reducer metadata."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True, serialize_by_alias=True)
|
||||
|
||||
path: StatePath
|
||||
validation_schema: SchemaRef = Field(
|
||||
default_factory=lambda: SchemaRef(type="object"),
|
||||
alias="schema",
|
||||
serialization_alias="schema",
|
||||
)
|
||||
reducer: ReducerRef = Field(
|
||||
default_factory=lambda: ReducerRef(name="wf.std.replace")
|
||||
)
|
||||
trace: bool = True
|
||||
default: Any = None
|
||||
|
||||
@property
|
||||
def type(self) -> str | None:
|
||||
"""Compatibility accessor for callers migrating from StateField.type."""
|
||||
schema_type = self.validation_schema.type
|
||||
return schema_type if isinstance(schema_type, str) else None
|
||||
|
||||
@field_serializer("path")
|
||||
def _serialize_path(self, path: StatePath) -> str:
|
||||
return str(path)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _coerce_legacy_type(cls, value: object) -> object:
|
||||
if not isinstance(value, Mapping):
|
||||
return value
|
||||
|
||||
data = dict(value)
|
||||
if "schema" not in data and "type" in data:
|
||||
data["schema"] = {"type": data.pop("type")}
|
||||
return data
|
||||
|
||||
@field_validator("reducer", mode="before")
|
||||
@classmethod
|
||||
def _coerce_reducer(cls, value: object) -> object:
|
||||
if isinstance(value, str):
|
||||
return {"name": value}
|
||||
return value
|
||||
|
||||
|
||||
class StateSchema(BaseModel):
|
||||
"""Workflow state schema keyed by declared exact state path."""
|
||||
"""Workflow state schema with canonical list fields.
|
||||
|
||||
Deprecated dict-shaped input is still accepted at parse time and normalized
|
||||
so runtime and serialization only deal with list-of-struct declarations.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
fields: dict[str, StateField] = Field(default_factory=dict)
|
||||
fields: list[StateFieldDecl] = Field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def from_field_map(cls, fields: Mapping[str, StateField]) -> StateSchema:
|
||||
"""Build from the deprecated dict shape at typed Python call sites."""
|
||||
return cls.model_validate({"fields": fields})
|
||||
|
||||
def field_map(self) -> dict[str, StateFieldDecl]:
|
||||
"""Return declarations keyed by rootless dotted path."""
|
||||
return {".".join(field.path.parts): field for field in self.fields}
|
||||
|
||||
def root_fields(self) -> set[str]:
|
||||
"""Return declared top-level state field names."""
|
||||
return {field.path.parts[0] for field in self.fields}
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _coerce_deprecated_field_map(cls, value: object) -> object:
|
||||
if not isinstance(value, Mapping):
|
||||
return value
|
||||
|
||||
data = dict(value)
|
||||
fields = data.get("fields")
|
||||
if not isinstance(fields, Mapping):
|
||||
return data
|
||||
|
||||
normalized_fields: list[object] = []
|
||||
for raw_path, raw_field in fields.items():
|
||||
path = str(raw_path)
|
||||
if not path.startswith("state."):
|
||||
path = f"state.{path}"
|
||||
|
||||
if isinstance(raw_field, BaseModel):
|
||||
field_data = raw_field.model_dump(mode="python")
|
||||
elif isinstance(raw_field, Mapping):
|
||||
field_data = dict(raw_field)
|
||||
if "schema" in field_data or "type" not in field_data:
|
||||
raise ValueError(
|
||||
"legacy state field map entries must include 'type'; "
|
||||
"use canonical list form for entries with 'schema'"
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
"legacy state field map entries must include 'type'; "
|
||||
"use canonical list form for non-legacy declarations"
|
||||
)
|
||||
|
||||
field_data["path"] = path
|
||||
normalized_fields.append(field_data)
|
||||
|
||||
data["fields"] = normalized_fields
|
||||
return data
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _reject_duplicate_field_paths(self) -> StateSchema:
|
||||
seen: set[str] = set()
|
||||
for field in self.fields:
|
||||
key = ".".join(field.path.parts)
|
||||
if key in seen:
|
||||
raise ValueError(f"duplicate state field path {key!r}")
|
||||
seen.add(key)
|
||||
return self
|
||||
|
||||
|
||||
class NodeDef(BaseModel):
|
||||
|
||||
@@ -78,14 +78,15 @@ class NodeUse(BaseModel):
|
||||
input_values = cls._deprecated_mapping(
|
||||
normalized.pop("input_values", {}), field_name="input_values"
|
||||
)
|
||||
in_map = cls._deprecated_mapping(normalized.pop("in_map", {}), field_name="in_map")
|
||||
in_map = cls._deprecated_mapping(
|
||||
normalized.pop("in_map", {}), field_name="in_map"
|
||||
)
|
||||
out_map = cls._deprecated_mapping(
|
||||
normalized.pop("out_map", {}), field_name="out_map"
|
||||
)
|
||||
|
||||
input_bindings.extend(
|
||||
{"target": target, "value": value}
|
||||
for target, value in input_values.items()
|
||||
{"target": target, "value": value} for target, value in input_values.items()
|
||||
)
|
||||
input_bindings.extend(
|
||||
{"target": target, "path": path} for path, target in in_map.items()
|
||||
|
||||
@@ -90,7 +90,7 @@ class LocalPath:
|
||||
return core_schema.no_info_plain_validator_function(
|
||||
validate,
|
||||
serialization=core_schema.plain_serializer_function_ser_schema(
|
||||
str, when_used="json"
|
||||
str,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -112,9 +112,7 @@ class GraphSourcePath:
|
||||
parts: tuple[str, ...] = ()
|
||||
|
||||
_ROOTS: ClassVar[set[str]] = {"input", "state", "context"}
|
||||
_JSON_PATTERN: ClassVar[str] = (
|
||||
r"^(input|state|context)(\.[A-Za-z_][A-Za-z0-9_]*)*$"
|
||||
)
|
||||
_JSON_PATTERN: ClassVar[str] = r"^(input|state|context)(\.[A-Za-z_][A-Za-z0-9_]*)*$"
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.root not in self._ROOTS:
|
||||
@@ -123,8 +121,7 @@ class GraphSourcePath:
|
||||
self,
|
||||
"parts",
|
||||
tuple(
|
||||
_validate_segment(part, path_kind="graph source")
|
||||
for part in self.parts
|
||||
_validate_segment(part, path_kind="graph source") for part in self.parts
|
||||
),
|
||||
)
|
||||
|
||||
@@ -167,7 +164,7 @@ class GraphSourcePath:
|
||||
return core_schema.no_info_plain_validator_function(
|
||||
validate,
|
||||
serialization=core_schema.plain_serializer_function_ser_schema(
|
||||
str, when_used="json"
|
||||
str,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -187,7 +184,9 @@ class StatePath:
|
||||
|
||||
parts: tuple[str, ...]
|
||||
|
||||
_JSON_PATTERN: ClassVar[str] = r"^state\.[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$"
|
||||
_JSON_PATTERN: ClassVar[str] = (
|
||||
r"^state\.[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$"
|
||||
)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
parts = tuple(_validate_segment(part, path_kind="state") for part in self.parts)
|
||||
@@ -226,7 +225,7 @@ class StatePath:
|
||||
return core_schema.no_info_plain_validator_function(
|
||||
validate,
|
||||
serialization=core_schema.plain_serializer_function_ser_schema(
|
||||
str, when_used="json"
|
||||
str,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -3,15 +3,15 @@ from __future__ import annotations
|
||||
from copy import deepcopy
|
||||
|
||||
from wf_core.models.workflow import Workflow
|
||||
from wf_core.paths import set_nested_value
|
||||
from wf_core.run_state import ExecutionFrame, FrameStatus, RunState, RunStatus
|
||||
|
||||
|
||||
def create_run_state(workflow: Workflow, workflow_input: dict[str, object]) -> RunState:
|
||||
state = {
|
||||
name: deepcopy(field.default)
|
||||
for name, field in workflow.state_schema.fields.items()
|
||||
if field.default is not None
|
||||
}
|
||||
state: dict[str, object] = {}
|
||||
for field in workflow.state_schema.fields:
|
||||
if field.default is not None:
|
||||
set_nested_value(state, list(field.path.parts), deepcopy(field.default))
|
||||
state.update(dict(workflow_input))
|
||||
run = RunState(
|
||||
workflow_name=workflow.name,
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Any
|
||||
from wf_core.errors import WorkflowExecutionError
|
||||
from wf_core.local_paths import LocalPathError, get_local_value, has_overlapping_paths
|
||||
from wf_core.models.reducers import ReducerRef
|
||||
from wf_core.models.schemas import StateFieldDecl
|
||||
from wf_core.models.steps import NodeUse, OutputBinding
|
||||
from wf_core.models.workflow import Workflow
|
||||
from wf_core.paths import (
|
||||
@@ -59,6 +60,7 @@ def apply_output_bindings(
|
||||
"mapped state patch has overlapping destination paths"
|
||||
)
|
||||
|
||||
state_fields = workflow.state_schema.field_map()
|
||||
resolved_patch: dict[StatePath, Any] = {}
|
||||
for binding in bindings:
|
||||
try:
|
||||
@@ -77,6 +79,7 @@ def apply_output_bindings(
|
||||
destination_path,
|
||||
value,
|
||||
reducers=reducers,
|
||||
state_fields=state_fields,
|
||||
)
|
||||
prepared_patch[destination_path] = (key_path, merged_value)
|
||||
|
||||
@@ -137,6 +140,7 @@ def prepare_state_value(
|
||||
value: Any,
|
||||
*,
|
||||
reducers: Mapping[str, ReducerDefinition] | None = None,
|
||||
state_fields: Mapping[str, StateFieldDecl] | None = None,
|
||||
) -> tuple[list[str], Any]:
|
||||
"""Resolve reducer output for a state write without mutating state."""
|
||||
try:
|
||||
@@ -150,7 +154,10 @@ def prepare_state_value(
|
||||
)
|
||||
|
||||
declared_path = ".".join(parts)
|
||||
declared_field = workflow.state_schema.fields.get(declared_path)
|
||||
fields = (
|
||||
state_fields if state_fields is not None else workflow.state_schema.field_map()
|
||||
)
|
||||
declared_field = fields.get(declared_path)
|
||||
reducer = (
|
||||
declared_field.reducer if declared_field else ReducerRef(name="wf.std.replace")
|
||||
)
|
||||
|
||||
@@ -54,7 +54,7 @@ def _validate_nodes(
|
||||
report: ValidationReport,
|
||||
) -> dict[str, Step]:
|
||||
nodes_by_id: dict[str, Step] = {}
|
||||
state_root_fields = set(workflow.state_schema.fields)
|
||||
state_root_fields = workflow.state_schema.root_fields()
|
||||
input_root_fields = set(workflow.input_schema.properties)
|
||||
|
||||
for index, node in enumerate(workflow.nodes):
|
||||
|
||||
@@ -47,8 +47,7 @@ def validate_node_use(
|
||||
|
||||
input_fields = set(node_def.input_schema.properties)
|
||||
output_fields = set(node_def.output_schema.properties)
|
||||
state_fields = set(workflow.state_schema.fields)
|
||||
state_root_fields = {field.split(".", maxsplit=1)[0] for field in state_fields}
|
||||
state_root_fields = workflow.state_schema.root_fields()
|
||||
input_root_fields = set(workflow.input_schema.properties)
|
||||
|
||||
input_targets = []
|
||||
|
||||
Reference in New Issue
Block a user