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 dataclasses import dataclass, field
import re
from typing import Any, Literal, TypeAlias, TypeGuard, cast
from typing import Any, Literal, cast
import warnings
from wf_core import (
@@ -21,88 +20,20 @@ from wf_core import (
from wf_core.errors import WorkflowExecutionError
from wf_core.model import Condition as CoreCondition
from .dsl import Expr, GraphPath, PathArg, compile_condition
from .nodes.callables import SyncRegistryHandler
from .nodes.registry import build_registry
from .schemas import SchemaLike, StateSchemaLike, schema_ref_from, state_schema_from
from .spec import NodeSpec
StepRef: TypeAlias = str | NodeUse | ConditionNode | ForeachNode | InterruptNode
BranchRef: TypeAlias = StepRef | NodeSpec[Any, Any]
MapArg: TypeAlias = Mapping[Any, Any]
def _coerce_path(value: object) -> str:
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]:
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}"
from ..dsl import Expr, PathArg, compile_condition
from ..nodes.callables import SyncRegistryHandler
from ..nodes.registry import build_registry
from ..schemas import SchemaLike, StateSchemaLike, schema_ref_from, state_schema_from
from ..spec import NodeSpec
from .ids import next_step_id, slug_id
from .mapping import (
MapArg,
auto_input_map,
auto_output_map,
coerce_path,
normalize_mapping,
)
from .refs import BranchRef, StepRef, is_node_spec, step_id
@dataclass(slots=True)
@@ -135,23 +66,23 @@ class WorkflowBuilder:
normalized_input_schema = cast(SchemaRef, self.input_schema)
normalized_state_schema = cast(StateSchema, self.state_schema)
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",
node=spec.name,
desc=desc or spec.description,
in_map=(
_auto_input_map(
auto_input_map(
spec,
input_schema=normalized_input_schema,
state_schema=normalized_state_schema,
)
if in_map is None
else _normalize_mapping(in_map)
else normalize_mapping(in_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
else _normalize_mapping(out_map)
else normalize_mapping(out_map)
),
)
self.nodes.append(node)
@@ -159,17 +90,11 @@ class WorkflowBuilder:
def _next_step_id(self, base: str) -> str:
"""Return a stable unused step id based on the requested base name."""
used_ids = {_step_id(node) for node in 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}"
return next_step_id(base, cast(list[StepRef], self.nodes))
def set_entry_point(self, step: StepRef) -> None:
"""Set the workflow start node explicitly."""
self.start = _step_id(step)
self.start = step_id(step)
def registry(self) -> dict[str, SyncRegistryHandler]:
"""Export handlers for all node specs used by this builder."""
@@ -206,9 +131,9 @@ class WorkflowBuilder:
) -> ForeachNode:
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",
"over": _coerce_path(over),
"over": coerce_path(over),
"as": as_,
"mode": mode,
"on_item_error": on_item_error,
@@ -227,11 +152,11 @@ class WorkflowBuilder:
outcomes: list[str] | None = None,
) -> 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",
kind=kind,
request_map=_normalize_mapping(request_map),
out_map=_normalize_mapping(out_map),
request_map=normalize_mapping(request_map),
out_map=normalize_mapping(out_map),
outcomes=outcomes or ["submitted"],
)
self.nodes.append(node)
@@ -244,14 +169,14 @@ class WorkflowBuilder:
to: BranchRef,
) -> tuple[StepRef, StepRef]:
"""Connect one outcome, auto-using NodeSpec endpoints as fresh node uses."""
source = self.use(from_) if _is_node_spec(from_) else from_
target = self.use(to) if _is_node_spec(to) else to
source = self.use(from_) if is_node_spec(from_) else from_
target = self.use(to) if is_node_spec(to) else to
self.edges.append(
Edge.model_validate(
{
"from": _step_id(cast(StepRef, source)),
"from": step_id(cast(StepRef, source)),
"outcome": outcome,
"to": _step_id(cast(StepRef, target)),
"to": step_id(cast(StepRef, target)),
}
)
)
@@ -277,10 +202,10 @@ class WorkflowBuilder:
)
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] = {}
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))
resolved_targets[outcome] = cast(StepRef, resolved)
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)