some more ops
could we make subgraphs as ops or patterns like this?
This commit is contained in:
@@ -22,10 +22,15 @@ from .dsl import (
|
|||||||
from .ops import (
|
from .ops import (
|
||||||
BoolOutput,
|
BoolOutput,
|
||||||
CoalesceInput,
|
CoalesceInput,
|
||||||
|
ConcatInput,
|
||||||
ConstantInput,
|
ConstantInput,
|
||||||
CountOutput,
|
CountOutput,
|
||||||
|
ExtractFieldInput,
|
||||||
|
FilterItemsInput,
|
||||||
|
FilterItemsPresentInput,
|
||||||
ItemOutput,
|
ItemOutput,
|
||||||
MappingOutput,
|
MappingOutput,
|
||||||
|
MappingItemsOutput,
|
||||||
MaybeItemOutput,
|
MaybeItemOutput,
|
||||||
PickPathInput,
|
PickPathInput,
|
||||||
PickKeyInput,
|
PickKeyInput,
|
||||||
@@ -33,11 +38,16 @@ from .ops import (
|
|||||||
RenameFieldsInput,
|
RenameFieldsInput,
|
||||||
RuntimeErrorInput,
|
RuntimeErrorInput,
|
||||||
SequenceInput,
|
SequenceInput,
|
||||||
|
TextOutput,
|
||||||
TruthyInput,
|
TruthyInput,
|
||||||
ValueOutput,
|
ValueOutput,
|
||||||
coalesce,
|
coalesce,
|
||||||
|
concat,
|
||||||
constant,
|
constant,
|
||||||
default_if_none,
|
default_if_none,
|
||||||
|
extract_field,
|
||||||
|
filter_items,
|
||||||
|
filter_items_present,
|
||||||
first_item,
|
first_item,
|
||||||
first_item_maybe,
|
first_item_maybe,
|
||||||
first_item_or_none,
|
first_item_or_none,
|
||||||
@@ -75,11 +85,16 @@ __all__ = [
|
|||||||
"async_subgraph_node",
|
"async_subgraph_node",
|
||||||
"BoolOutput",
|
"BoolOutput",
|
||||||
"CoalesceInput",
|
"CoalesceInput",
|
||||||
|
"ConcatInput",
|
||||||
"ConstantInput",
|
"ConstantInput",
|
||||||
"CountOutput",
|
"CountOutput",
|
||||||
|
"ExtractFieldInput",
|
||||||
|
"FilterItemsInput",
|
||||||
|
"FilterItemsPresentInput",
|
||||||
"GraphPath",
|
"GraphPath",
|
||||||
"ItemOutput",
|
"ItemOutput",
|
||||||
"MappingOutput",
|
"MappingOutput",
|
||||||
|
"MappingItemsOutput",
|
||||||
"MaybeItemOutput",
|
"MaybeItemOutput",
|
||||||
"PickKeyInput",
|
"PickKeyInput",
|
||||||
"PickPathInput",
|
"PickPathInput",
|
||||||
@@ -95,6 +110,7 @@ __all__ = [
|
|||||||
"SyncRegistryHandler",
|
"SyncRegistryHandler",
|
||||||
"SequenceInput",
|
"SequenceInput",
|
||||||
"StateFieldMetadata",
|
"StateFieldMetadata",
|
||||||
|
"TextOutput",
|
||||||
"TruthyInput",
|
"TruthyInput",
|
||||||
"ValueOutput",
|
"ValueOutput",
|
||||||
"WorkflowBuilder",
|
"WorkflowBuilder",
|
||||||
@@ -103,8 +119,12 @@ __all__ = [
|
|||||||
"build_registry",
|
"build_registry",
|
||||||
"bind_state",
|
"bind_state",
|
||||||
"coalesce",
|
"coalesce",
|
||||||
|
"concat",
|
||||||
"constant",
|
"constant",
|
||||||
"default_if_none",
|
"default_if_none",
|
||||||
|
"extract_field",
|
||||||
|
"filter_items",
|
||||||
|
"filter_items_present",
|
||||||
"merge_maps",
|
"merge_maps",
|
||||||
"context",
|
"context",
|
||||||
"context_path",
|
"context_path",
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
from .sequences import (
|
from .sequences import (
|
||||||
BoolOutput,
|
BoolOutput,
|
||||||
CountOutput,
|
CountOutput,
|
||||||
|
ExtractFieldInput,
|
||||||
|
FilterItemsInput,
|
||||||
|
FilterItemsPresentInput,
|
||||||
ItemOutput,
|
ItemOutput,
|
||||||
|
MappingItemsOutput,
|
||||||
MaybeItemOutput,
|
MaybeItemOutput,
|
||||||
SequenceInput,
|
SequenceInput,
|
||||||
|
ValuesOutput,
|
||||||
|
extract_field,
|
||||||
|
filter_items,
|
||||||
|
filter_items_present,
|
||||||
first_item,
|
first_item,
|
||||||
first_item_maybe,
|
first_item_maybe,
|
||||||
first_item_or_none,
|
first_item_or_none,
|
||||||
@@ -14,6 +22,7 @@ from .sequences import (
|
|||||||
)
|
)
|
||||||
from .values import (
|
from .values import (
|
||||||
CoalesceInput,
|
CoalesceInput,
|
||||||
|
ConcatInput,
|
||||||
ConstantInput,
|
ConstantInput,
|
||||||
MappingOutput,
|
MappingOutput,
|
||||||
PickPathInput,
|
PickPathInput,
|
||||||
@@ -22,8 +31,10 @@ from .values import (
|
|||||||
RenameFieldsInput,
|
RenameFieldsInput,
|
||||||
RuntimeErrorInput,
|
RuntimeErrorInput,
|
||||||
TruthyInput,
|
TruthyInput,
|
||||||
|
TextOutput,
|
||||||
ValueOutput,
|
ValueOutput,
|
||||||
coalesce,
|
coalesce,
|
||||||
|
concat,
|
||||||
constant,
|
constant,
|
||||||
default_if_none,
|
default_if_none,
|
||||||
pick_path,
|
pick_path,
|
||||||
@@ -37,10 +48,15 @@ from .values import (
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
"BoolOutput",
|
"BoolOutput",
|
||||||
"CoalesceInput",
|
"CoalesceInput",
|
||||||
|
"ConcatInput",
|
||||||
"ConstantInput",
|
"ConstantInput",
|
||||||
"CountOutput",
|
"CountOutput",
|
||||||
|
"ExtractFieldInput",
|
||||||
|
"FilterItemsInput",
|
||||||
|
"FilterItemsPresentInput",
|
||||||
"ItemOutput",
|
"ItemOutput",
|
||||||
"MappingOutput",
|
"MappingOutput",
|
||||||
|
"MappingItemsOutput",
|
||||||
"MaybeItemOutput",
|
"MaybeItemOutput",
|
||||||
"PickPathInput",
|
"PickPathInput",
|
||||||
"PickKeyInput",
|
"PickKeyInput",
|
||||||
@@ -49,10 +65,16 @@ __all__ = [
|
|||||||
"RuntimeErrorInput",
|
"RuntimeErrorInput",
|
||||||
"SequenceInput",
|
"SequenceInput",
|
||||||
"TruthyInput",
|
"TruthyInput",
|
||||||
|
"TextOutput",
|
||||||
"ValueOutput",
|
"ValueOutput",
|
||||||
|
"ValuesOutput",
|
||||||
"coalesce",
|
"coalesce",
|
||||||
|
"concat",
|
||||||
"constant",
|
"constant",
|
||||||
"default_if_none",
|
"default_if_none",
|
||||||
|
"extract_field",
|
||||||
|
"filter_items",
|
||||||
|
"filter_items_present",
|
||||||
"first_item",
|
"first_item",
|
||||||
"first_item_maybe",
|
"first_item_maybe",
|
||||||
"first_item_or_none",
|
"first_item_or_none",
|
||||||
|
|||||||
@@ -37,6 +37,40 @@ class BoolOutput(BaseModel):
|
|||||||
value: bool
|
value: bool
|
||||||
|
|
||||||
|
|
||||||
|
class FilterItemsInput(BaseModel):
|
||||||
|
"""Input model for filtering mapping items by exact key/value match."""
|
||||||
|
|
||||||
|
items: list[dict[str, Any]]
|
||||||
|
key: str
|
||||||
|
value: Any
|
||||||
|
|
||||||
|
|
||||||
|
class FilterItemsPresentInput(BaseModel):
|
||||||
|
"""Input model for filtering mapping items that contain a key."""
|
||||||
|
|
||||||
|
items: list[dict[str, Any]]
|
||||||
|
key: str
|
||||||
|
|
||||||
|
|
||||||
|
class MappingItemsOutput(BaseModel):
|
||||||
|
"""Output model for ops that return mapping items."""
|
||||||
|
|
||||||
|
items: list[dict[str, Any]]
|
||||||
|
|
||||||
|
|
||||||
|
class ExtractFieldInput(BaseModel):
|
||||||
|
"""Input model for extracting one field from mapping items."""
|
||||||
|
|
||||||
|
items: list[dict[str, Any]]
|
||||||
|
field: str
|
||||||
|
|
||||||
|
|
||||||
|
class ValuesOutput(BaseModel):
|
||||||
|
"""Output model for ops that return arbitrary values."""
|
||||||
|
|
||||||
|
values: list[Any]
|
||||||
|
|
||||||
|
|
||||||
@node(
|
@node(
|
||||||
name="authoring.first_item",
|
name="authoring.first_item",
|
||||||
input_model=SequenceInput,
|
input_model=SequenceInput,
|
||||||
@@ -119,3 +153,40 @@ def length(input: SequenceInput) -> CountOutput:
|
|||||||
def is_empty(input: SequenceInput) -> BoolOutput:
|
def is_empty(input: SequenceInput) -> BoolOutput:
|
||||||
"""Return whether a sequence has no items."""
|
"""Return whether a sequence has no items."""
|
||||||
return BoolOutput(value=not input.items)
|
return BoolOutput(value=not input.items)
|
||||||
|
|
||||||
|
|
||||||
|
@node(
|
||||||
|
name="authoring.filter_items",
|
||||||
|
input_model=FilterItemsInput,
|
||||||
|
output_model=MappingItemsOutput,
|
||||||
|
description="Filter mapping items by exact key/value match.",
|
||||||
|
)
|
||||||
|
def filter_items(input: FilterItemsInput) -> MappingItemsOutput:
|
||||||
|
"""Return items where item[key] exactly equals value."""
|
||||||
|
return MappingItemsOutput(
|
||||||
|
items=[item for item in input.items if item.get(input.key) == input.value]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@node(
|
||||||
|
name="authoring.filter_items_present",
|
||||||
|
input_model=FilterItemsPresentInput,
|
||||||
|
output_model=MappingItemsOutput,
|
||||||
|
description="Filter mapping items to those containing the requested key.",
|
||||||
|
)
|
||||||
|
def filter_items_present(input: FilterItemsPresentInput) -> MappingItemsOutput:
|
||||||
|
"""Return items that contain key, regardless of the stored value."""
|
||||||
|
return MappingItemsOutput(items=[item for item in input.items if input.key in item])
|
||||||
|
|
||||||
|
|
||||||
|
@node(
|
||||||
|
name="authoring.extract_field",
|
||||||
|
input_model=ExtractFieldInput,
|
||||||
|
output_model=ValuesOutput,
|
||||||
|
description="Extract one field from each mapping item that contains it.",
|
||||||
|
)
|
||||||
|
def extract_field(input: ExtractFieldInput) -> ValuesOutput:
|
||||||
|
"""Return item[field] for each item containing field."""
|
||||||
|
return ValuesOutput(
|
||||||
|
values=[item[input.field] for item in input.items if input.field in item]
|
||||||
|
)
|
||||||
|
|||||||
@@ -73,6 +73,19 @@ class RuntimeErrorInput(BaseModel):
|
|||||||
details: dict[str, Any] = Field(default_factory=dict)
|
details: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class ConcatInput(BaseModel):
|
||||||
|
"""Input model for joining string values."""
|
||||||
|
|
||||||
|
items: list[str]
|
||||||
|
separator: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class TextOutput(BaseModel):
|
||||||
|
"""Output model for ops that emit text."""
|
||||||
|
|
||||||
|
text: str
|
||||||
|
|
||||||
|
|
||||||
@node(
|
@node(
|
||||||
name="authoring.coalesce",
|
name="authoring.coalesce",
|
||||||
input_model=CoalesceInput,
|
input_model=CoalesceInput,
|
||||||
@@ -180,6 +193,17 @@ def truthy(input: TruthyInput) -> NodeReturn[ValueOutput]:
|
|||||||
return NodeReturn(outcome=outcome, output=ValueOutput(value=value))
|
return NodeReturn(outcome=outcome, output=ValueOutput(value=value))
|
||||||
|
|
||||||
|
|
||||||
|
@node(
|
||||||
|
name="authoring.concat",
|
||||||
|
input_model=ConcatInput,
|
||||||
|
output_model=TextOutput,
|
||||||
|
description="Join string items using separator.",
|
||||||
|
)
|
||||||
|
def concat(input: ConcatInput) -> TextOutput:
|
||||||
|
"""Join string items using separator."""
|
||||||
|
return TextOutput(text=input.separator.join(input.items))
|
||||||
|
|
||||||
|
|
||||||
@node(
|
@node(
|
||||||
name="authoring.runtime_error",
|
name="authoring.runtime_error",
|
||||||
input_model=RuntimeErrorInput,
|
input_model=RuntimeErrorInput,
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from wf_authoring import NodeSpec, coalesce, constant, default_if_none, first_item
|
from wf_authoring import NodeSpec, coalesce, concat, constant, default_if_none
|
||||||
|
from wf_authoring import extract_field, filter_items, filter_items_present, first_item
|
||||||
from wf_authoring import first_item_maybe, first_item_or_none, is_empty, last_item
|
from wf_authoring import first_item_maybe, first_item_or_none, is_empty, last_item
|
||||||
from wf_authoring import last_item_or_none, length, node, pick_key, pick_path
|
from wf_authoring import last_item_or_none, length, node, pick_key, pick_path
|
||||||
from wf_authoring import project_fields, rename_fields, runtime_error, truthy
|
from wf_authoring import project_fields, rename_fields, runtime_error, truthy
|
||||||
@@ -43,6 +44,10 @@ AUTHORING_STD_SPECS: tuple[NodeSpec[Any, Any], ...] = (
|
|||||||
last_item_or_none,
|
last_item_or_none,
|
||||||
length,
|
length,
|
||||||
is_empty,
|
is_empty,
|
||||||
|
filter_items,
|
||||||
|
filter_items_present,
|
||||||
|
extract_field,
|
||||||
|
concat,
|
||||||
)
|
)
|
||||||
"""Existing authoring ops that are also exposed through the workflow stdlib."""
|
"""Existing authoring ops that are also exposed through the workflow stdlib."""
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ from wf_authoring import (
|
|||||||
coalesce,
|
coalesce,
|
||||||
constant,
|
constant,
|
||||||
default_if_none,
|
default_if_none,
|
||||||
|
concat,
|
||||||
|
extract_field,
|
||||||
|
filter_items,
|
||||||
|
filter_items_present,
|
||||||
first_item,
|
first_item,
|
||||||
first_item_maybe,
|
first_item_maybe,
|
||||||
first_item_or_none,
|
first_item_or_none,
|
||||||
@@ -303,6 +307,80 @@ def test_rename_fields_remaps_existing_keys() -> None:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_filter_items_selects_mapping_items_by_exact_match() -> None:
|
||||||
|
registry = build_registry(filter_items)
|
||||||
|
|
||||||
|
result = registry["authoring.filter_items"](
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{"type": "text", "text": "a"},
|
||||||
|
{"type": "image", "url": "img://1"},
|
||||||
|
{"type": "text", "text": "b"},
|
||||||
|
],
|
||||||
|
"key": "type",
|
||||||
|
"value": "text",
|
||||||
|
},
|
||||||
|
RuntimeContext(current_node_id="filter_items"),
|
||||||
|
)
|
||||||
|
|
||||||
|
output = result["output"]
|
||||||
|
assert result["outcome"] == "ok"
|
||||||
|
assert output["items"][0]["text"] == "a"
|
||||||
|
assert output["items"][1]["text"] == "b"
|
||||||
|
|
||||||
|
|
||||||
|
def test_filter_items_present_selects_mapping_items_containing_key() -> None:
|
||||||
|
registry = build_registry(filter_items_present)
|
||||||
|
|
||||||
|
result = registry["authoring.filter_items_present"](
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{"text": "a"},
|
||||||
|
{"url": "img://1"},
|
||||||
|
{"text": None},
|
||||||
|
],
|
||||||
|
"key": "text",
|
||||||
|
},
|
||||||
|
RuntimeContext(current_node_id="filter_items_present"),
|
||||||
|
)
|
||||||
|
|
||||||
|
output = result["output"]
|
||||||
|
assert result["outcome"] == "ok"
|
||||||
|
assert output["items"][0]["text"] == "a"
|
||||||
|
assert output["items"][1]["text"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_field_returns_existing_field_values() -> None:
|
||||||
|
registry = build_registry(extract_field)
|
||||||
|
|
||||||
|
result = registry["authoring.extract_field"](
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{"text": "a"},
|
||||||
|
{"url": "img://1"},
|
||||||
|
{"text": "b"},
|
||||||
|
],
|
||||||
|
"field": "text",
|
||||||
|
},
|
||||||
|
RuntimeContext(current_node_id="extract_field"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["outcome"] == "ok"
|
||||||
|
assert result["output"]["values"] == ["a", "b"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_concat_joins_strings_with_separator() -> None:
|
||||||
|
registry = build_registry(concat)
|
||||||
|
|
||||||
|
result = registry["authoring.concat"](
|
||||||
|
{"items": ["a", "b", "c"], "separator": "\n"},
|
||||||
|
RuntimeContext(current_node_id="concat"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["outcome"] == "ok"
|
||||||
|
assert result["output"]["text"] == "a\nb\nc"
|
||||||
|
|
||||||
|
|
||||||
def test_truthy_routes_truthy_and_falsey_outcomes() -> None:
|
def test_truthy_routes_truthy_and_falsey_outcomes() -> None:
|
||||||
registry = build_registry(truthy)
|
registry = build_registry(truthy)
|
||||||
ctx = RuntimeContext(current_node_id="truthy")
|
ctx = RuntimeContext(current_node_id="truthy")
|
||||||
|
|||||||
@@ -237,6 +237,10 @@ def test_wf_std_source_contains_authoring_ops() -> None:
|
|||||||
"wf.std.last_item_or_none",
|
"wf.std.last_item_or_none",
|
||||||
"wf.std.length",
|
"wf.std.length",
|
||||||
"wf.std.is_empty",
|
"wf.std.is_empty",
|
||||||
|
"wf.std.filter_items",
|
||||||
|
"wf.std.filter_items_present",
|
||||||
|
"wf.std.extract_field",
|
||||||
|
"wf.std.concat",
|
||||||
}
|
}
|
||||||
assert set(specs) == expected
|
assert set(specs) == expected
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user