sched: extract shared input traversal behind SourceResolver seam (T03)
This commit is contained in:
@@ -7,17 +7,14 @@ from wf_core.conditions import safe_resolve_path
|
||||
from wf_core.errors import WorkflowExecutionError
|
||||
from wf_core.local_paths import LocalPathError, set_local_value
|
||||
from wf_core.models.input_bindings import (
|
||||
ArrayExpression,
|
||||
InputExpression,
|
||||
InputExpressionBinding,
|
||||
InputPathBinding,
|
||||
InputValueBinding,
|
||||
LiteralExpression,
|
||||
ObjectExpression,
|
||||
PathExpression,
|
||||
StepInputBinding,
|
||||
)
|
||||
from wf_core.models.json_values import JsonValue, validate_strict_json_value
|
||||
from wf_core.models.json_values import JsonValue
|
||||
from wf_core.runtime.input_sources import SourceResolver
|
||||
|
||||
|
||||
def resolve_input_expression(
|
||||
@@ -30,45 +27,32 @@ def resolve_input_expression(
|
||||
location: str,
|
||||
) -> JsonValue:
|
||||
"""Resolve one composite expression while preserving its payload location."""
|
||||
from wf_core.runtime.input_sources import (
|
||||
GraphSourceResolver,
|
||||
resolve_composed_expression,
|
||||
)
|
||||
|
||||
if isinstance(expression, LiteralExpression):
|
||||
return expression.value
|
||||
if isinstance(expression, PathExpression):
|
||||
try:
|
||||
value = safe_resolve_path(
|
||||
str(expression.path),
|
||||
state=state,
|
||||
workflow_input=workflow_input,
|
||||
context=context,
|
||||
)
|
||||
return validate_strict_json_value(value)
|
||||
except (ValueError, WorkflowExecutionError) as exc:
|
||||
raise WorkflowExecutionError(f"{label} {location}: {exc}") from exc
|
||||
if isinstance(expression, ArrayExpression):
|
||||
return [
|
||||
resolve_input_expression(
|
||||
item,
|
||||
state=state,
|
||||
workflow_input=workflow_input,
|
||||
context=context,
|
||||
label=label,
|
||||
location=f"{location}[{index}]",
|
||||
)
|
||||
for index, item in enumerate(expression.items)
|
||||
]
|
||||
if isinstance(expression, ObjectExpression):
|
||||
return {
|
||||
field: resolve_input_expression(
|
||||
value,
|
||||
state=state,
|
||||
workflow_input=workflow_input,
|
||||
context=context,
|
||||
label=label,
|
||||
location=f"{location}.{field}",
|
||||
)
|
||||
for field, value in expression.fields.items()
|
||||
}
|
||||
raise WorkflowExecutionError(f"unsupported input expression for {label} {location}")
|
||||
resolver = GraphSourceResolver(
|
||||
state=state, workflow_input=workflow_input, context=context
|
||||
)
|
||||
return resolve_composed_expression(
|
||||
expression, resolver=resolver, label=label, location=location
|
||||
)
|
||||
|
||||
|
||||
def resolve_input_expression_with_resolver(
|
||||
expression: InputExpression,
|
||||
*,
|
||||
resolver: SourceResolver,
|
||||
label: str,
|
||||
location: str,
|
||||
) -> JsonValue:
|
||||
"""Resolve one expression through an explicit typed source resolver."""
|
||||
from wf_core.runtime.input_sources import resolve_composed_expression
|
||||
|
||||
return resolve_composed_expression(
|
||||
expression, resolver=resolver, label=label, location=location
|
||||
)
|
||||
|
||||
|
||||
def resolve_step_input_bindings(
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
"""Typed source-resolver seam for composed input expressions.
|
||||
|
||||
Graph and schedule paths share one composition traversal (literal / array /
|
||||
object recursion plus budget enforcement) but keep distinct source models:
|
||||
graph leaves resolve ``input`` / ``state`` / ``context`` paths, while
|
||||
schedule leaves resolve typed occurrence fields. Do not extend
|
||||
``GraphSourcePath`` with schedule-only roots and do not smuggle occurrence
|
||||
values through a faked graph ``context`` mapping.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator, Mapping
|
||||
from typing import Any, Protocol
|
||||
|
||||
from wf_core.errors import WorkflowExecutionError
|
||||
from wf_core.models.input_bindings import (
|
||||
ArrayExpression,
|
||||
InputExpression,
|
||||
LiteralExpression,
|
||||
ObjectExpression,
|
||||
PathExpression,
|
||||
)
|
||||
from wf_core.models.json_values import JsonValue, validate_strict_json_value
|
||||
from wf_core.paths import GraphSourcePath, resolve_graph_path
|
||||
|
||||
|
||||
class SourceResolver(Protocol):
|
||||
"""Resolve one leaf source path to a JSON-compatible value."""
|
||||
|
||||
def resolve_path(self, path: GraphSourcePath) -> Any:
|
||||
"""Return the value for one graph source path."""
|
||||
...
|
||||
|
||||
|
||||
class GraphSourceResolver:
|
||||
"""Resolve ``input`` / ``state`` / ``context`` paths from live mappings."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
state: Mapping[str, Any],
|
||||
workflow_input: Mapping[str, Any],
|
||||
context: Mapping[str, Any],
|
||||
) -> None:
|
||||
self._state = state
|
||||
self._workflow_input = workflow_input
|
||||
self._context = context
|
||||
|
||||
def resolve_path(self, path: GraphSourcePath) -> Any:
|
||||
"""Resolve one graph path against the captured mappings."""
|
||||
return resolve_graph_path(
|
||||
path,
|
||||
state=self._state,
|
||||
workflow_input=self._workflow_input,
|
||||
context=self._context,
|
||||
)
|
||||
|
||||
|
||||
class MappingSourceResolver:
|
||||
"""Resolve occurrence-style fields from a flat mapping (schedule side).
|
||||
|
||||
This resolver exists so schedule composition reuses the same traversal
|
||||
without faking a graph ``context``. It only understands occurrence field
|
||||
names; graph paths are invalid here.
|
||||
"""
|
||||
|
||||
def __init__(self, values: Mapping[str, Any]) -> None:
|
||||
self._values = dict(values)
|
||||
|
||||
def resolve_field(self, field: str) -> Any:
|
||||
"""Return one occurrence field value."""
|
||||
if field not in self._values:
|
||||
raise WorkflowExecutionError(f"unknown occurrence field {field!r}")
|
||||
return self._values[field]
|
||||
|
||||
|
||||
def resolve_composed_expression(
|
||||
expression: InputExpression,
|
||||
*,
|
||||
resolver: SourceResolver,
|
||||
label: str,
|
||||
location: str,
|
||||
) -> JsonValue:
|
||||
"""Resolve one composite expression through a typed source resolver."""
|
||||
if isinstance(expression, LiteralExpression):
|
||||
return expression.value
|
||||
if isinstance(expression, PathExpression):
|
||||
try:
|
||||
value = resolver.resolve_path(expression.path)
|
||||
return validate_strict_json_value(value)
|
||||
except (ValueError, WorkflowExecutionError) as exc:
|
||||
raise WorkflowExecutionError(f"{label} {location}: {exc}") from exc
|
||||
if isinstance(expression, ArrayExpression):
|
||||
return [
|
||||
resolve_composed_expression(
|
||||
item,
|
||||
resolver=resolver,
|
||||
label=label,
|
||||
location=f"{location}[{index}]",
|
||||
)
|
||||
for index, item in enumerate(expression.items)
|
||||
]
|
||||
if isinstance(expression, ObjectExpression):
|
||||
return {
|
||||
field: resolve_composed_expression(
|
||||
value,
|
||||
resolver=resolver,
|
||||
label=label,
|
||||
location=f"{location}.{field}",
|
||||
)
|
||||
for field, value in expression.fields.items()
|
||||
}
|
||||
raise WorkflowExecutionError(f"unsupported input expression for {label} {location}")
|
||||
|
||||
|
||||
def walk_expression_paths(
|
||||
expression: InputExpression,
|
||||
) -> Iterator[tuple[GraphSourcePath, str]]:
|
||||
"""Yield ``(graph path, location)`` leaves in one shared traversal order.
|
||||
|
||||
Locations use the same ``items[i]`` / ``fields.name`` / ``path`` suffix
|
||||
shape as :func:`wf_core.validation.steps._validate_expression_sources`,
|
||||
so validation and runtime traverse leaves in the same order.
|
||||
"""
|
||||
if isinstance(expression, PathExpression):
|
||||
yield expression.path, "path"
|
||||
elif isinstance(expression, ArrayExpression):
|
||||
for index, item in enumerate(expression.items):
|
||||
for path, suffix in walk_expression_paths(item):
|
||||
yield path, f"items[{index}].{suffix}"
|
||||
elif isinstance(expression, ObjectExpression):
|
||||
for field, item in expression.fields.items():
|
||||
for path, suffix in walk_expression_paths(item):
|
||||
yield path, f"fields.{field}.{suffix}"
|
||||
@@ -10,13 +10,7 @@ from wf_core.models.conditions import (
|
||||
PathOperand,
|
||||
VariadicCondition,
|
||||
)
|
||||
from wf_core.models.input_bindings import (
|
||||
ArrayExpression,
|
||||
InputExpression,
|
||||
InputExpressionBinding,
|
||||
ObjectExpression,
|
||||
PathExpression,
|
||||
)
|
||||
from wf_core.models.input_bindings import InputExpression, InputExpressionBinding
|
||||
from wf_core.models.schemas import NodeDef
|
||||
from wf_core.models.steps import (
|
||||
ConditionNode,
|
||||
@@ -249,39 +243,23 @@ def _validate_expression_sources(
|
||||
error_code: ValidationIssueCode = ValidationIssueCode.INVALID_SOURCE_PATH,
|
||||
message: str = "source path must start with input., state., or context. and reference a declared root field when applicable",
|
||||
) -> None:
|
||||
"""Validate every graph path leaf while keeping one top-level target atomic."""
|
||||
if isinstance(expression, PathExpression):
|
||||
"""Validate every graph path leaf while keeping one top-level target atomic.
|
||||
|
||||
Leaf traversal order is shared with
|
||||
:func:`wf_core.runtime.input_sources.walk_expression_paths`.
|
||||
"""
|
||||
from wf_core.runtime.input_sources import walk_expression_paths
|
||||
|
||||
for path, suffix in walk_expression_paths(expression):
|
||||
_validate_source_path(
|
||||
expression.path,
|
||||
f"{input_path}.path",
|
||||
path,
|
||||
f"{input_path}.{suffix}",
|
||||
report,
|
||||
state_root_fields,
|
||||
input_root_fields,
|
||||
error_code=error_code,
|
||||
message=message,
|
||||
)
|
||||
elif isinstance(expression, ArrayExpression):
|
||||
for index, item in enumerate(expression.items):
|
||||
_validate_expression_sources(
|
||||
item,
|
||||
input_path=f"{input_path}.items[{index}]",
|
||||
report=report,
|
||||
state_root_fields=state_root_fields,
|
||||
input_root_fields=input_root_fields,
|
||||
error_code=error_code,
|
||||
message=message,
|
||||
)
|
||||
elif isinstance(expression, ObjectExpression):
|
||||
for field, item in expression.fields.items():
|
||||
_validate_expression_sources(
|
||||
item,
|
||||
input_path=f"{input_path}.fields.{field}",
|
||||
report=report,
|
||||
state_root_fields=state_root_fields,
|
||||
input_root_fields=input_root_fields,
|
||||
error_code=error_code,
|
||||
message=message,
|
||||
)
|
||||
|
||||
|
||||
def _local_root(path: str | LocalPath) -> str | None:
|
||||
|
||||
Reference in New Issue
Block a user