All for smaller files

this is called a package? alright sure
This commit is contained in:
lda
2026-05-07 06:10:41 +07:00 Verified
parent 868709f99c
commit 06d25582c9
5 changed files with 153 additions and 108 deletions
+4
View File
@@ -0,0 +1,4 @@
from .core import WorkflowBuilder
from .refs import BranchRef, StepRef
__all__ = ["BranchRef", "StepRef", "WorkflowBuilder"]
@@ -2,8 +2,7 @@ from __future__ import annotations
from collections.abc import Mapping from collections.abc import Mapping
from dataclasses import dataclass, field from dataclasses import dataclass, field
import re from typing import Any, Literal, cast
from typing import Any, Literal, TypeAlias, TypeGuard, cast
import warnings import warnings
from wf_core import ( from wf_core import (
@@ -21,88 +20,20 @@ from wf_core import (
from wf_core.errors import WorkflowExecutionError from wf_core.errors import WorkflowExecutionError
from wf_core.model import Condition as CoreCondition from wf_core.model import Condition as CoreCondition
from .dsl import Expr, GraphPath, PathArg, compile_condition from ..dsl import Expr, PathArg, compile_condition
from .nodes.callables import SyncRegistryHandler from ..nodes.callables import SyncRegistryHandler
from .nodes.registry import build_registry from ..nodes.registry import build_registry
from .schemas import SchemaLike, StateSchemaLike, schema_ref_from, state_schema_from from ..schemas import SchemaLike, StateSchemaLike, schema_ref_from, state_schema_from
from .spec import NodeSpec from ..spec import NodeSpec
from .ids import next_step_id, slug_id
StepRef: TypeAlias = str | NodeUse | ConditionNode | ForeachNode | InterruptNode from .mapping import (
BranchRef: TypeAlias = StepRef | NodeSpec[Any, Any] MapArg,
MapArg: TypeAlias = Mapping[Any, Any] auto_input_map,
auto_output_map,
coerce_path,
def _coerce_path(value: object) -> str: normalize_mapping,
if isinstance(value, str): )
return value from .refs import BranchRef, StepRef, is_node_spec, step_id
if isinstance(value, GraphPath):
return value.value
raise TypeError(f"unsupported graph path value {value!r}")
def _normalize_mapping(
mapping: MapArg | None,
) -> dict[str, str]:
if mapping is None:
return {}
return {
_coerce_path(source): _coerce_path(destination)
for source, destination in mapping.items()
}
def _step_id(ref: StepRef) -> str:
if isinstance(ref, str):
return ref
return ref.id
def _is_node_spec(ref: object) -> TypeGuard[NodeSpec[Any, Any]]:
return isinstance(ref, NodeSpec)
def _slug_id(value: str) -> str:
slug = re.sub(r"[^0-9A-Za-z_]+", "_", value).strip("_").lower()
return slug or "step"
def _auto_input_map(
spec: NodeSpec[Any, Any],
*,
input_schema: SchemaRef,
state_schema: StateSchema,
) -> dict[str, str]:
return {
_auto_source_path(
field, input_schema=input_schema, state_schema=state_schema
): field
for field in spec.input_model.model_json_schema().get("properties", {})
}
def _auto_output_map(
spec: NodeSpec[Any, Any],
*,
state_schema: StateSchema,
) -> dict[str, str]:
return {
field: f"state.{field}"
for field in spec.output_model.model_json_schema().get("properties", {})
if field in state_schema.fields
}
def _auto_source_path(
field: str,
*,
input_schema: SchemaRef,
state_schema: StateSchema,
) -> str:
if field in state_schema.fields:
return f"state.{field}"
if field in input_schema.properties:
return f"input.{field}"
return f"state.{field}"
@dataclass(slots=True) @dataclass(slots=True)
@@ -135,23 +66,23 @@ class WorkflowBuilder:
normalized_input_schema = cast(SchemaRef, self.input_schema) normalized_input_schema = cast(SchemaRef, self.input_schema)
normalized_state_schema = cast(StateSchema, self.state_schema) normalized_state_schema = cast(StateSchema, self.state_schema)
node = NodeUse( node = NodeUse(
id=id or self._next_step_id(_slug_id(spec.name)), id=id or self._next_step_id(slug_id(spec.name)),
type="node", type="node",
node=spec.name, node=spec.name,
desc=desc or spec.description, desc=desc or spec.description,
in_map=( in_map=(
_auto_input_map( auto_input_map(
spec, spec,
input_schema=normalized_input_schema, input_schema=normalized_input_schema,
state_schema=normalized_state_schema, state_schema=normalized_state_schema,
) )
if in_map is None if in_map is None
else _normalize_mapping(in_map) else normalize_mapping(in_map)
), ),
out_map=( out_map=(
_auto_output_map(spec, state_schema=normalized_state_schema) auto_output_map(spec, state_schema=normalized_state_schema)
if out_map is None if out_map is None
else _normalize_mapping(out_map) else normalize_mapping(out_map)
), ),
) )
self.nodes.append(node) self.nodes.append(node)
@@ -159,17 +90,11 @@ class WorkflowBuilder:
def _next_step_id(self, base: str) -> str: def _next_step_id(self, base: str) -> str:
"""Return a stable unused step id based on the requested base name.""" """Return a stable unused step id based on the requested base name."""
used_ids = {_step_id(node) for node in self.nodes} return next_step_id(base, cast(list[StepRef], self.nodes))
if base not in used_ids:
return base
suffix = 2
while f"{base}_{suffix}" in used_ids:
suffix += 1
return f"{base}_{suffix}"
def set_entry_point(self, step: StepRef) -> None: def set_entry_point(self, step: StepRef) -> None:
"""Set the workflow start node explicitly.""" """Set the workflow start node explicitly."""
self.start = _step_id(step) self.start = step_id(step)
def registry(self) -> dict[str, SyncRegistryHandler]: def registry(self) -> dict[str, SyncRegistryHandler]:
"""Export handlers for all node specs used by this builder.""" """Export handlers for all node specs used by this builder."""
@@ -206,9 +131,9 @@ class WorkflowBuilder:
) -> ForeachNode: ) -> ForeachNode:
node = ForeachNode.model_validate( node = ForeachNode.model_validate(
{ {
"id": id or self._next_step_id(f"foreach_{_slug_id(as_)}"), "id": id or self._next_step_id(f"foreach_{slug_id(as_)}"),
"type": "foreach", "type": "foreach",
"over": _coerce_path(over), "over": coerce_path(over),
"as": as_, "as": as_,
"mode": mode, "mode": mode,
"on_item_error": on_item_error, "on_item_error": on_item_error,
@@ -227,11 +152,11 @@ class WorkflowBuilder:
outcomes: list[str] | None = None, outcomes: list[str] | None = None,
) -> InterruptNode: ) -> InterruptNode:
node = InterruptNode( node = InterruptNode(
id=id or self._next_step_id(f"interrupt_{_slug_id(kind)}"), id=id or self._next_step_id(f"interrupt_{slug_id(kind)}"),
type="interrupt", type="interrupt",
kind=kind, kind=kind,
request_map=_normalize_mapping(request_map), request_map=normalize_mapping(request_map),
out_map=_normalize_mapping(out_map), out_map=normalize_mapping(out_map),
outcomes=outcomes or ["submitted"], outcomes=outcomes or ["submitted"],
) )
self.nodes.append(node) self.nodes.append(node)
@@ -244,14 +169,14 @@ class WorkflowBuilder:
to: BranchRef, to: BranchRef,
) -> tuple[StepRef, StepRef]: ) -> tuple[StepRef, StepRef]:
"""Connect one outcome, auto-using NodeSpec endpoints as fresh node uses.""" """Connect one outcome, auto-using NodeSpec endpoints as fresh node uses."""
source = self.use(from_) if _is_node_spec(from_) else from_ source = self.use(from_) if is_node_spec(from_) else from_
target = self.use(to) if _is_node_spec(to) else to target = self.use(to) if is_node_spec(to) else to
self.edges.append( self.edges.append(
Edge.model_validate( Edge.model_validate(
{ {
"from": _step_id(cast(StepRef, source)), "from": step_id(cast(StepRef, source)),
"outcome": outcome, "outcome": outcome,
"to": _step_id(cast(StepRef, target)), "to": step_id(cast(StepRef, target)),
} }
) )
) )
@@ -277,10 +202,10 @@ class WorkflowBuilder:
) )
return {} return {}
source = self.use(from_) if _is_node_spec(from_) else from_ source = self.use(from_) if is_node_spec(from_) else from_
resolved_targets: dict[str, StepRef] = {} resolved_targets: dict[str, StepRef] = {}
for outcome, target in branches.items(): for outcome, target in branches.items():
resolved = self.use(target) if _is_node_spec(target) else target resolved = self.use(target) if is_node_spec(target) else target
self.connect(cast(StepRef, source), outcome, cast(StepRef, resolved)) self.connect(cast(StepRef, source), outcome, cast(StepRef, resolved))
resolved_targets[outcome] = cast(StepRef, resolved) resolved_targets[outcome] = cast(StepRef, resolved)
return resolved_targets return resolved_targets
+23
View File
@@ -0,0 +1,23 @@
from __future__ import annotations
import re
from collections.abc import Iterable
from .refs import StepRef, step_id
def slug_id(value: str) -> str:
"""Convert a display name into a stable, readable workflow step id."""
slug = re.sub(r"[^0-9A-Za-z_]+", "_", value).strip("_").lower()
return slug or "step"
def next_step_id(base: str, nodes: Iterable[StepRef]) -> str:
"""Return a stable unused step id based on the requested base name."""
used_ids = {step_id(node) for node in nodes}
if base not in used_ids:
return base
suffix = 2
while f"{base}_{suffix}" in used_ids:
suffix += 1
return f"{base}_{suffix}"
+71
View File
@@ -0,0 +1,71 @@
from __future__ import annotations
from collections.abc import Mapping
from typing import Any, TypeAlias
from wf_core import SchemaRef, StateSchema
from ..dsl import GraphPath
from ..spec import NodeSpec
MapArg: TypeAlias = Mapping[Any, Any]
def coerce_path(value: object) -> str:
"""Normalize an authoring path object or string into a core path string."""
if isinstance(value, str):
return value
if isinstance(value, GraphPath):
return value.value
raise TypeError(f"unsupported graph path value {value!r}")
def normalize_mapping(mapping: MapArg | None) -> dict[str, str]:
"""Normalize authoring map declarations into core string-to-string maps."""
if mapping is None:
return {}
return {
coerce_path(source): coerce_path(destination)
for source, destination in mapping.items()
}
def auto_input_map(
spec: NodeSpec[Any, Any],
*,
input_schema: SchemaRef,
state_schema: StateSchema,
) -> dict[str, str]:
"""Map node input fields from state first, then workflow input."""
return {
_auto_source_path(
field, input_schema=input_schema, state_schema=state_schema
): field
for field in spec.input_model.model_json_schema().get("properties", {})
}
def auto_output_map(
spec: NodeSpec[Any, Any],
*,
state_schema: StateSchema,
) -> dict[str, str]:
"""Map node output fields back into matching state fields."""
return {
field: f"state.{field}"
for field in spec.output_model.model_json_schema().get("properties", {})
if field in state_schema.fields
}
def _auto_source_path(
field: str,
*,
input_schema: SchemaRef,
state_schema: StateSchema,
) -> str:
if field in state_schema.fields:
return f"state.{field}"
if field in input_schema.properties:
return f"input.{field}"
return f"state.{field}"
+22
View File
@@ -0,0 +1,22 @@
from __future__ import annotations
from typing import Any, TypeAlias, TypeGuard
from wf_core import ConditionNode, ForeachNode, InterruptNode, NodeUse
from ..spec import NodeSpec
StepRef: TypeAlias = str | NodeUse | ConditionNode | ForeachNode | InterruptNode
BranchRef: TypeAlias = StepRef | NodeSpec[Any, Any]
def step_id(ref: StepRef) -> str:
"""Return the core step id for either a step object or an id string."""
if isinstance(ref, str):
return ref
return ref.id
def is_node_spec(ref: object) -> TypeGuard[NodeSpec[Any, Any]]:
"""Narrow an endpoint ref to a NodeSpec that should be auto-used."""
return isinstance(ref, NodeSpec)