stageflow-framework 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stageflow/__init__.py +25 -0
- stageflow/builtins/__init__.py +34 -0
- stageflow/builtins/dicts.py +59 -0
- stageflow/builtins/lists.py +71 -0
- stageflow/builtins/lists_extra.py +110 -0
- stageflow/builtins/logic.py +84 -0
- stageflow/builtins/strings.py +81 -0
- stageflow/builtins/vars.py +118 -0
- stageflow/core/__init__.py +7 -0
- stageflow/core/context.py +86 -0
- stageflow/core/event.py +71 -0
- stageflow/core/jsonlogic.py +43 -0
- stageflow/core/node.py +256 -0
- stageflow/core/pipeline.py +108 -0
- stageflow/core/session.py +398 -0
- stageflow/core/stage.py +152 -0
- stageflow/core/utils.py +62 -0
- stageflow/docs/__init__.py +4 -0
- stageflow/docs/html.py +479 -0
- stageflow/docs/schema.py +38 -0
- stageflow/docs/schemas/pipeline.json +134 -0
- stageflow/py.typed +1 -0
- stageflow/testing.py +59 -0
- stageflow_framework-0.1.0.dist-info/METADATA +167 -0
- stageflow_framework-0.1.0.dist-info/RECORD +27 -0
- stageflow_framework-0.1.0.dist-info/WHEEL +5 -0
- stageflow_framework-0.1.0.dist-info/top_level.txt +1 -0
stageflow/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .core import (
|
|
2
|
+
Event, EventSpec, InputSpec,
|
|
3
|
+
JsonLogic,
|
|
4
|
+
Node, ConditionNode, Condition, ParallelNode, TerminalNode, StageNode,
|
|
5
|
+
Session, SessionResult,
|
|
6
|
+
BaseStage, get_stage, register_stage, get_stages, get_stages_by_category,
|
|
7
|
+
Context, DotDict,
|
|
8
|
+
Pipeline,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from .docs import generate_stages_yaml, generate_stages_json
|
|
12
|
+
from . import builtins
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"Event", "EventSpec", "InputSpec",
|
|
17
|
+
"JsonLogic",
|
|
18
|
+
"Node", "ConditionNode", "Condition", "ParallelNode", "TerminalNode", "StageNode",
|
|
19
|
+
"Session", "SessionResult",
|
|
20
|
+
"BaseStage", "get_stage", "register_stage", "get_stages", "get_stages_by_category",
|
|
21
|
+
"Context", "DotDict",
|
|
22
|
+
"Pipeline",
|
|
23
|
+
"generate_stages_yaml", "generate_stages_json",
|
|
24
|
+
"builtins",
|
|
25
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from .vars import (
|
|
2
|
+
SetValueStage,
|
|
3
|
+
CopyValueStage,
|
|
4
|
+
IncrementStage,
|
|
5
|
+
MergeDictStage,
|
|
6
|
+
)
|
|
7
|
+
from .lists import (
|
|
8
|
+
AppendListStage,
|
|
9
|
+
ExtendListStage,
|
|
10
|
+
)
|
|
11
|
+
from .logic import AssertStage, FailStage, LogStage, SleepStage
|
|
12
|
+
from .strings import ConcatStage, TemplateStage
|
|
13
|
+
from .dicts import PickKeysStage, DropKeysStage
|
|
14
|
+
from .lists_extra import FilterListStage, UniqueListStage, PopListStage
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"SetValueStage",
|
|
18
|
+
"CopyValueStage",
|
|
19
|
+
"IncrementStage",
|
|
20
|
+
"MergeDictStage",
|
|
21
|
+
"AppendListStage",
|
|
22
|
+
"ExtendListStage",
|
|
23
|
+
"AssertStage",
|
|
24
|
+
"FailStage",
|
|
25
|
+
"LogStage",
|
|
26
|
+
"SleepStage",
|
|
27
|
+
"ConcatStage",
|
|
28
|
+
"TemplateStage",
|
|
29
|
+
"PickKeysStage",
|
|
30
|
+
"DropKeysStage",
|
|
31
|
+
"FilterListStage",
|
|
32
|
+
"UniqueListStage",
|
|
33
|
+
"PopListStage",
|
|
34
|
+
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from stageflow.core.stage import BaseStage, register_stage
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@register_stage("PickKeysStage")
|
|
5
|
+
class PickKeysStage(BaseStage):
|
|
6
|
+
"""
|
|
7
|
+
description: "Pick only specified keys from a dict and return new object"
|
|
8
|
+
arguments:
|
|
9
|
+
src:
|
|
10
|
+
type: object
|
|
11
|
+
description: "Source dict"
|
|
12
|
+
config:
|
|
13
|
+
keys:
|
|
14
|
+
type: list
|
|
15
|
+
description: "Keys to keep in the result"
|
|
16
|
+
outputs:
|
|
17
|
+
result:
|
|
18
|
+
type: object
|
|
19
|
+
description: "Dict containing only picked keys"
|
|
20
|
+
"""
|
|
21
|
+
category = "builtin.dicts"
|
|
22
|
+
|
|
23
|
+
async def run(self):
|
|
24
|
+
args = self.get_arguments()
|
|
25
|
+
src_val = args.get("src") or {}
|
|
26
|
+
keys = self.config.get("keys", [])
|
|
27
|
+
if not isinstance(src_val, dict):
|
|
28
|
+
raise ValueError("PickKeysStage expects dict in arguments.src")
|
|
29
|
+
picked = {k: src_val[k] for k in keys if k in src_val}
|
|
30
|
+
self.set_outputs({"result": picked})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@register_stage("DropKeysStage")
|
|
34
|
+
class DropKeysStage(BaseStage):
|
|
35
|
+
"""
|
|
36
|
+
description: "Remove specified keys from dict and return cleaned object"
|
|
37
|
+
arguments:
|
|
38
|
+
src:
|
|
39
|
+
type: object
|
|
40
|
+
description: "Source dict"
|
|
41
|
+
config:
|
|
42
|
+
keys:
|
|
43
|
+
type: list
|
|
44
|
+
description: "Keys to remove from the dict"
|
|
45
|
+
outputs:
|
|
46
|
+
result:
|
|
47
|
+
type: object
|
|
48
|
+
description: "Dict without removed keys"
|
|
49
|
+
"""
|
|
50
|
+
category = "builtin.dicts"
|
|
51
|
+
|
|
52
|
+
async def run(self):
|
|
53
|
+
args = self.get_arguments()
|
|
54
|
+
keys = set(self.config.get("keys", []))
|
|
55
|
+
src_val = args.get("src") or {}
|
|
56
|
+
if not isinstance(src_val, dict):
|
|
57
|
+
raise ValueError("DropKeysStage expects dict in arguments.src")
|
|
58
|
+
cleaned = {k: v for k, v in src_val.items() if k not in keys}
|
|
59
|
+
self.set_outputs({"result": cleaned})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from stageflow.core.stage import BaseStage, register_stage
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@register_stage("AppendListStage")
|
|
5
|
+
class AppendListStage(BaseStage):
|
|
6
|
+
"""
|
|
7
|
+
description: "Append value from args/config to list (creates list if missing)"
|
|
8
|
+
arguments:
|
|
9
|
+
list:
|
|
10
|
+
type: list
|
|
11
|
+
description: "List to append to (from context)"
|
|
12
|
+
value:
|
|
13
|
+
type: any
|
|
14
|
+
description: "Value to append (overrides config)"
|
|
15
|
+
config:
|
|
16
|
+
value:
|
|
17
|
+
type: any
|
|
18
|
+
description: "Fallback value when argument is missing"
|
|
19
|
+
outputs:
|
|
20
|
+
list:
|
|
21
|
+
type: list
|
|
22
|
+
description: "Resulting list after append"
|
|
23
|
+
"""
|
|
24
|
+
category = "builtin.lists"
|
|
25
|
+
|
|
26
|
+
async def run(self):
|
|
27
|
+
args = self.get_arguments()
|
|
28
|
+
value = args.get("value", self.config.get("value"))
|
|
29
|
+
lst = args.get("list", None)
|
|
30
|
+
if lst is None:
|
|
31
|
+
lst = []
|
|
32
|
+
if not isinstance(lst, list):
|
|
33
|
+
raise ValueError("AppendListStage expects list")
|
|
34
|
+
lst.append(value)
|
|
35
|
+
self.set_outputs({"list": lst})
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@register_stage("ExtendListStage")
|
|
39
|
+
class ExtendListStage(BaseStage):
|
|
40
|
+
"""
|
|
41
|
+
description: "Extend list with items from arguments (or default list)"
|
|
42
|
+
arguments:
|
|
43
|
+
list:
|
|
44
|
+
type: list
|
|
45
|
+
description: "Base list to extend"
|
|
46
|
+
items:
|
|
47
|
+
type: list
|
|
48
|
+
description: "Items to extend the list with"
|
|
49
|
+
config:
|
|
50
|
+
default_list:
|
|
51
|
+
type: list
|
|
52
|
+
description: "Fallback list when argument is missing"
|
|
53
|
+
outputs:
|
|
54
|
+
list:
|
|
55
|
+
type: list
|
|
56
|
+
description: "Resulting list after extend"
|
|
57
|
+
"""
|
|
58
|
+
category = "builtin.lists"
|
|
59
|
+
|
|
60
|
+
async def run(self):
|
|
61
|
+
args = self.get_arguments()
|
|
62
|
+
lst = args.get("list", self.config.get("default_list", []))
|
|
63
|
+
if lst is None:
|
|
64
|
+
lst = []
|
|
65
|
+
if not isinstance(lst, list):
|
|
66
|
+
raise ValueError("ExtendListStage expects list")
|
|
67
|
+
src_val = args.get("items", [])
|
|
68
|
+
if not isinstance(src_val, (list, tuple)):
|
|
69
|
+
raise ValueError("Source for extend is not a list/tuple")
|
|
70
|
+
lst.extend(src_val)
|
|
71
|
+
self.set_outputs({"list": lst})
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from stageflow.core.stage import BaseStage, register_stage
|
|
2
|
+
from stageflow.core.jsonlogic import JsonLogic
|
|
3
|
+
from stageflow.core.context import Context
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@register_stage("FilterListStage")
|
|
7
|
+
class FilterListStage(BaseStage):
|
|
8
|
+
"""
|
|
9
|
+
description: "Filter list items by JsonLogic condition using item_path binding"
|
|
10
|
+
arguments:
|
|
11
|
+
items:
|
|
12
|
+
type: list
|
|
13
|
+
description: "List to filter"
|
|
14
|
+
config:
|
|
15
|
+
condition:
|
|
16
|
+
type: object
|
|
17
|
+
description: "JsonLogic condition evaluated for each item"
|
|
18
|
+
item_path:
|
|
19
|
+
type: string
|
|
20
|
+
description: "Context key to bind current item during evaluation"
|
|
21
|
+
outputs:
|
|
22
|
+
list:
|
|
23
|
+
type: list
|
|
24
|
+
description: "Filtered list"
|
|
25
|
+
"""
|
|
26
|
+
category = "builtin.lists"
|
|
27
|
+
|
|
28
|
+
async def run(self):
|
|
29
|
+
args = self.get_arguments()
|
|
30
|
+
condition = self.config.get("condition")
|
|
31
|
+
item_path = self.config.get("item_path", "item")
|
|
32
|
+
if condition is None:
|
|
33
|
+
raise ValueError("FilterListStage requires config.condition")
|
|
34
|
+
items = args.get("items", [])
|
|
35
|
+
if not isinstance(items, list):
|
|
36
|
+
raise ValueError("FilterListStage expects list in arguments.items")
|
|
37
|
+
base_payload = self.session.context.payload
|
|
38
|
+
result = []
|
|
39
|
+
for item in items:
|
|
40
|
+
temp_ctx = Context(payload=dict(base_payload))
|
|
41
|
+
temp_ctx.set(item_path, item)
|
|
42
|
+
if JsonLogic(condition).evaluate(temp_ctx):
|
|
43
|
+
result.append(item)
|
|
44
|
+
self.set_outputs({"list": result})
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@register_stage("UniqueListStage")
|
|
48
|
+
class UniqueListStage(BaseStage):
|
|
49
|
+
"""
|
|
50
|
+
description: "Deduplicate list while preserving original order"
|
|
51
|
+
arguments:
|
|
52
|
+
items:
|
|
53
|
+
type: list
|
|
54
|
+
description: "List to deduplicate"
|
|
55
|
+
outputs:
|
|
56
|
+
list:
|
|
57
|
+
type: list
|
|
58
|
+
description: "List with unique items"
|
|
59
|
+
"""
|
|
60
|
+
category = "builtin.lists"
|
|
61
|
+
|
|
62
|
+
async def run(self):
|
|
63
|
+
args = self.get_arguments()
|
|
64
|
+
items = args.get("items", [])
|
|
65
|
+
if not isinstance(items, list):
|
|
66
|
+
raise ValueError("UniqueListStage expects list in arguments.items")
|
|
67
|
+
seen = set()
|
|
68
|
+
out = []
|
|
69
|
+
for item in items:
|
|
70
|
+
if item not in seen:
|
|
71
|
+
seen.add(item)
|
|
72
|
+
out.append(item)
|
|
73
|
+
self.set_outputs({"list": out})
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@register_stage("PopListStage")
|
|
77
|
+
class PopListStage(BaseStage):
|
|
78
|
+
"""
|
|
79
|
+
description: "Pop element from list (default last) and return list+popped value"
|
|
80
|
+
arguments:
|
|
81
|
+
items:
|
|
82
|
+
type: list
|
|
83
|
+
description: "List to pop from"
|
|
84
|
+
index:
|
|
85
|
+
type: int
|
|
86
|
+
description: "Index to pop (overrides config)"
|
|
87
|
+
config:
|
|
88
|
+
index:
|
|
89
|
+
type: int
|
|
90
|
+
description: "Default index to pop, -1 means last element"
|
|
91
|
+
outputs:
|
|
92
|
+
list:
|
|
93
|
+
type: list
|
|
94
|
+
description: "List after pop"
|
|
95
|
+
popped:
|
|
96
|
+
type: any
|
|
97
|
+
description: "Popped value"
|
|
98
|
+
"""
|
|
99
|
+
category = "builtin.lists"
|
|
100
|
+
|
|
101
|
+
async def run(self):
|
|
102
|
+
args = self.get_arguments()
|
|
103
|
+
index = args.get("index", self.config.get("index", -1))
|
|
104
|
+
lst = args.get("items", [])
|
|
105
|
+
if not isinstance(lst, list):
|
|
106
|
+
raise ValueError("PopListStage expects list in arguments.items")
|
|
107
|
+
if not lst:
|
|
108
|
+
return
|
|
109
|
+
value = lst.pop(index)
|
|
110
|
+
self.set_outputs({"list": lst, "popped": value})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from stageflow.core.stage import BaseStage, register_stage
|
|
3
|
+
from stageflow.core.jsonlogic import JsonLogic
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@register_stage("AssertStage")
|
|
7
|
+
class AssertStage(BaseStage):
|
|
8
|
+
"""
|
|
9
|
+
description: "Validate JsonLogic condition against context, raise on failure"
|
|
10
|
+
config:
|
|
11
|
+
condition:
|
|
12
|
+
type: object
|
|
13
|
+
description: "JsonLogic condition to check"
|
|
14
|
+
message:
|
|
15
|
+
type: string
|
|
16
|
+
description: "Error message when condition fails"
|
|
17
|
+
outputs: {}
|
|
18
|
+
"""
|
|
19
|
+
category = "builtin.logic"
|
|
20
|
+
|
|
21
|
+
async def run(self):
|
|
22
|
+
condition = self.config.get("condition")
|
|
23
|
+
if not condition:
|
|
24
|
+
raise ValueError("AssertStage requires config.condition")
|
|
25
|
+
ok = JsonLogic(condition).evaluate(self.session.context)
|
|
26
|
+
if not ok:
|
|
27
|
+
raise AssertionError(self.config.get("message", "assertion failed"))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@register_stage("FailStage")
|
|
31
|
+
class FailStage(BaseStage):
|
|
32
|
+
"""
|
|
33
|
+
description: "Always raise a runtime error with provided message"
|
|
34
|
+
config:
|
|
35
|
+
message:
|
|
36
|
+
type: string
|
|
37
|
+
description: "Message for raised error"
|
|
38
|
+
outputs: {}
|
|
39
|
+
"""
|
|
40
|
+
category = "builtin.logic"
|
|
41
|
+
|
|
42
|
+
async def run(self):
|
|
43
|
+
msg = self.config.get("message", "fail")
|
|
44
|
+
raise RuntimeError(msg)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@register_stage("LogStage")
|
|
48
|
+
class LogStage(BaseStage):
|
|
49
|
+
"""
|
|
50
|
+
description: "Emit log event with message and payload resolved from context paths"
|
|
51
|
+
config:
|
|
52
|
+
message:
|
|
53
|
+
type: string
|
|
54
|
+
description: "Log message"
|
|
55
|
+
paths:
|
|
56
|
+
type: object
|
|
57
|
+
description: "Mapping of payload fields to context paths"
|
|
58
|
+
outputs: {}
|
|
59
|
+
"""
|
|
60
|
+
category = "builtin.logic"
|
|
61
|
+
|
|
62
|
+
async def run(self):
|
|
63
|
+
payload = {"message": self.config.get("message", "")}
|
|
64
|
+
paths = self.config.get("paths", {})
|
|
65
|
+
for key, path in paths.items():
|
|
66
|
+
payload[key] = self.session.context.get(path)
|
|
67
|
+
self.emit("log", payload)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@register_stage("SleepStage")
|
|
71
|
+
class SleepStage(BaseStage):
|
|
72
|
+
"""
|
|
73
|
+
description: "Async sleep for configured number of seconds"
|
|
74
|
+
config:
|
|
75
|
+
seconds:
|
|
76
|
+
type: number
|
|
77
|
+
description: "Duration to sleep in seconds"
|
|
78
|
+
outputs: {}
|
|
79
|
+
"""
|
|
80
|
+
category = "builtin.logic"
|
|
81
|
+
|
|
82
|
+
async def run(self):
|
|
83
|
+
sec = self.config.get("seconds", 0)
|
|
84
|
+
await asyncio.sleep(sec)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from stageflow.core.stage import BaseStage, register_stage
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@register_stage("ConcatStage")
|
|
5
|
+
class ConcatStage(BaseStage):
|
|
6
|
+
"""
|
|
7
|
+
description: "Concatenate parts (values or context paths) with separator"
|
|
8
|
+
arguments:
|
|
9
|
+
parts:
|
|
10
|
+
type: list
|
|
11
|
+
description: "List of values or context paths to concatenate"
|
|
12
|
+
separator:
|
|
13
|
+
type: string
|
|
14
|
+
description: "Separator overriding config"
|
|
15
|
+
config:
|
|
16
|
+
parts:
|
|
17
|
+
type: list
|
|
18
|
+
description: "Default parts when argument is missing"
|
|
19
|
+
separator:
|
|
20
|
+
type: string
|
|
21
|
+
description: "Default separator"
|
|
22
|
+
output_key:
|
|
23
|
+
type: string
|
|
24
|
+
description: "Context key for resulting string"
|
|
25
|
+
outputs:
|
|
26
|
+
value:
|
|
27
|
+
type: string
|
|
28
|
+
description: "Concatenated string"
|
|
29
|
+
"""
|
|
30
|
+
category = "builtin.strings"
|
|
31
|
+
|
|
32
|
+
async def run(self):
|
|
33
|
+
args = self.get_arguments()
|
|
34
|
+
parts = args.get("parts", self.config.get("parts", []))
|
|
35
|
+
sep = args.get("separator", self.config.get("separator", ""))
|
|
36
|
+
out_key = self.config.get("output_key", "value")
|
|
37
|
+
values = []
|
|
38
|
+
for p in parts:
|
|
39
|
+
if isinstance(p, str):
|
|
40
|
+
values.append(str(self.session.context.get(p, p)))
|
|
41
|
+
else:
|
|
42
|
+
values.append(str(p))
|
|
43
|
+
self.set_outputs({out_key: sep.join(values)})
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@register_stage("TemplateStage")
|
|
47
|
+
class TemplateStage(BaseStage):
|
|
48
|
+
"""
|
|
49
|
+
description: "Format template string with values pulled from context paths"
|
|
50
|
+
arguments:
|
|
51
|
+
template:
|
|
52
|
+
type: string
|
|
53
|
+
description: "Template overriding config"
|
|
54
|
+
config:
|
|
55
|
+
template:
|
|
56
|
+
type: string
|
|
57
|
+
description: "Default template string"
|
|
58
|
+
output_key:
|
|
59
|
+
type: string
|
|
60
|
+
description: "Context key for rendered value"
|
|
61
|
+
values:
|
|
62
|
+
type: object
|
|
63
|
+
description: "Mapping placeholder -> context path"
|
|
64
|
+
outputs:
|
|
65
|
+
value:
|
|
66
|
+
type: string
|
|
67
|
+
description: "Rendered string"
|
|
68
|
+
"""
|
|
69
|
+
category = "builtin.strings"
|
|
70
|
+
|
|
71
|
+
async def run(self):
|
|
72
|
+
args = self.get_arguments()
|
|
73
|
+
template = self.config.get("template") or args.get("template")
|
|
74
|
+
out_key = self.config.get("output_key", "value")
|
|
75
|
+
values_cfg = self.config.get("values", {})
|
|
76
|
+
if template is None:
|
|
77
|
+
raise ValueError("TemplateStage requires template")
|
|
78
|
+
values = {}
|
|
79
|
+
for key, path in values_cfg.items():
|
|
80
|
+
values[key] = self.session.context.get(path)
|
|
81
|
+
self.set_outputs({out_key: template.format(**values)})
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from stageflow.core.stage import BaseStage, register_stage
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@register_stage("SetValueStage")
|
|
5
|
+
class SetValueStage(BaseStage):
|
|
6
|
+
"""
|
|
7
|
+
description: "Set value from arguments or config to the target path"
|
|
8
|
+
arguments:
|
|
9
|
+
value:
|
|
10
|
+
type: any
|
|
11
|
+
description: "Value to set (overrides config)"
|
|
12
|
+
config:
|
|
13
|
+
value:
|
|
14
|
+
type: any
|
|
15
|
+
description: "Fallback value when argument is missing"
|
|
16
|
+
outputs:
|
|
17
|
+
value:
|
|
18
|
+
type: any
|
|
19
|
+
description: "Value that was written"
|
|
20
|
+
"""
|
|
21
|
+
category = "builtin.vars"
|
|
22
|
+
|
|
23
|
+
async def run(self):
|
|
24
|
+
args = self.get_arguments()
|
|
25
|
+
value = args.get("value", self.config.get("value"))
|
|
26
|
+
if value is None and "value" not in args and "value" not in self.config:
|
|
27
|
+
raise ValueError("SetValueStage requires value via arguments or config")
|
|
28
|
+
self.set_outputs({"value": value})
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@register_stage("CopyValueStage")
|
|
32
|
+
class CopyValueStage(BaseStage):
|
|
33
|
+
"""
|
|
34
|
+
description: "Copy value from arguments (or default) to output path"
|
|
35
|
+
arguments:
|
|
36
|
+
value:
|
|
37
|
+
type: any
|
|
38
|
+
description: "Value to copy"
|
|
39
|
+
config:
|
|
40
|
+
default:
|
|
41
|
+
type: any
|
|
42
|
+
description: "Default value when argument is missing"
|
|
43
|
+
outputs:
|
|
44
|
+
value:
|
|
45
|
+
type: any
|
|
46
|
+
description: "Copied value"
|
|
47
|
+
"""
|
|
48
|
+
category = "builtin.vars"
|
|
49
|
+
|
|
50
|
+
async def run(self):
|
|
51
|
+
args = self.get_arguments()
|
|
52
|
+
value = args.get("value", None)
|
|
53
|
+
if value is None:
|
|
54
|
+
value = self.config.get("default")
|
|
55
|
+
self.set_outputs({"value": value})
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@register_stage("IncrementStage")
|
|
59
|
+
class IncrementStage(BaseStage):
|
|
60
|
+
"""
|
|
61
|
+
description: "Increment numeric value by delta (from args or config)"
|
|
62
|
+
arguments:
|
|
63
|
+
current:
|
|
64
|
+
type: number
|
|
65
|
+
description: "Current numeric value"
|
|
66
|
+
delta:
|
|
67
|
+
type: number
|
|
68
|
+
description: "Delta overriding config"
|
|
69
|
+
config:
|
|
70
|
+
delta:
|
|
71
|
+
type: number
|
|
72
|
+
description: "Default delta (1 if missing)"
|
|
73
|
+
outputs:
|
|
74
|
+
value:
|
|
75
|
+
type: number
|
|
76
|
+
description: "Result after increment"
|
|
77
|
+
"""
|
|
78
|
+
category = "builtin.vars"
|
|
79
|
+
|
|
80
|
+
async def run(self):
|
|
81
|
+
args = self.get_arguments()
|
|
82
|
+
current = args.get("current", 0)
|
|
83
|
+
delta = args.get("delta", self.config.get("delta", 1))
|
|
84
|
+
if not isinstance(current, (int, float)):
|
|
85
|
+
raise ValueError("IncrementStage current is not numeric")
|
|
86
|
+
self.set_outputs({"value": current + delta})
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@register_stage("MergeDictStage")
|
|
90
|
+
class MergeDictStage(BaseStage):
|
|
91
|
+
"""
|
|
92
|
+
description: "Shallow merge src dict into dst (config default if missing)"
|
|
93
|
+
arguments:
|
|
94
|
+
src:
|
|
95
|
+
type: object
|
|
96
|
+
description: "Dict with overrides"
|
|
97
|
+
dst:
|
|
98
|
+
type: object
|
|
99
|
+
description: "Base dict to merge into"
|
|
100
|
+
config:
|
|
101
|
+
default_dst:
|
|
102
|
+
type: object
|
|
103
|
+
description: "Fallback base dict if dst is missing"
|
|
104
|
+
outputs:
|
|
105
|
+
merged:
|
|
106
|
+
type: object
|
|
107
|
+
description: "Merged dict result"
|
|
108
|
+
"""
|
|
109
|
+
category = "builtin.vars"
|
|
110
|
+
|
|
111
|
+
async def run(self):
|
|
112
|
+
args = self.get_arguments()
|
|
113
|
+
src_val = args.get("src") or {}
|
|
114
|
+
dst_val = args.get("dst") or self.config.get("default_dst", {})
|
|
115
|
+
if not isinstance(src_val, dict) or not isinstance(dst_val, dict):
|
|
116
|
+
raise ValueError("MergeDictStage expects dict values")
|
|
117
|
+
merged = {**dst_val, **src_val}
|
|
118
|
+
self.set_outputs({"merged": merged})
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from .event import Event, EventSpec, InputSpec # noqa: F401
|
|
2
|
+
from .jsonlogic import JsonLogic # noqa: F401
|
|
3
|
+
from .node import Node, ConditionNode, Condition, ParallelNode, TerminalNode, StageNode # noqa: F401
|
|
4
|
+
from .session import Session, SessionResult # noqa: F401
|
|
5
|
+
from .stage import BaseStage, get_stage, register_stage, get_stages, get_stages_by_category # noqa: F401
|
|
6
|
+
from .context import Context, DotDict # noqa: F401
|
|
7
|
+
from .pipeline import Pipeline # noqa: F401
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DotDict(dict):
|
|
5
|
+
def __getattr__(self, item):
|
|
6
|
+
try:
|
|
7
|
+
value = self[item]
|
|
8
|
+
if isinstance(value, dict) and not isinstance(value, DotDict):
|
|
9
|
+
value = DotDict(value)
|
|
10
|
+
self[item] = value
|
|
11
|
+
return value
|
|
12
|
+
except KeyError:
|
|
13
|
+
raise AttributeError(item)
|
|
14
|
+
|
|
15
|
+
def __setattr__(self, key, value):
|
|
16
|
+
self[key] = value
|
|
17
|
+
|
|
18
|
+
def __delattr__(self, key):
|
|
19
|
+
try:
|
|
20
|
+
del self[key]
|
|
21
|
+
except KeyError:
|
|
22
|
+
raise AttributeError(key)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Context:
|
|
26
|
+
def __init__(self, payload: dict[str, Any] | None = None):
|
|
27
|
+
self.payload: DotDict = DotDict(payload or {})
|
|
28
|
+
|
|
29
|
+
def to_dict(self) -> dict[str, Any]:
|
|
30
|
+
return dict(self._deep_copy(self.payload))
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def from_dict(cls, data: dict[str, Any]) -> "Context":
|
|
34
|
+
return cls(payload=data)
|
|
35
|
+
|
|
36
|
+
def get(self, path: str, default=None):
|
|
37
|
+
parts = path.split(".")
|
|
38
|
+
if parts and parts[0] == "payload":
|
|
39
|
+
parts = parts[1:]
|
|
40
|
+
cur: Any = self.payload
|
|
41
|
+
for p in parts:
|
|
42
|
+
if isinstance(cur, dict):
|
|
43
|
+
cur = cur.get(p, default)
|
|
44
|
+
elif isinstance(cur, list):
|
|
45
|
+
try:
|
|
46
|
+
idx = int(p)
|
|
47
|
+
cur = cur[idx]
|
|
48
|
+
except (ValueError, IndexError):
|
|
49
|
+
return default
|
|
50
|
+
else:
|
|
51
|
+
return default
|
|
52
|
+
return cur
|
|
53
|
+
|
|
54
|
+
def set(self, path: str, value: Any):
|
|
55
|
+
parts = path.split(".")
|
|
56
|
+
if parts and parts[0] == "payload":
|
|
57
|
+
parts = parts[1:]
|
|
58
|
+
cur = self.payload
|
|
59
|
+
for p in parts[:-1]:
|
|
60
|
+
if isinstance(cur, dict):
|
|
61
|
+
cur = cur.setdefault(p, DotDict())
|
|
62
|
+
elif isinstance(cur, list):
|
|
63
|
+
idx = int(p)
|
|
64
|
+
while len(cur) <= idx:
|
|
65
|
+
cur.append(DotDict())
|
|
66
|
+
cur = cur[idx]
|
|
67
|
+
else:
|
|
68
|
+
raise ValueError(f"Can't traverse into {type(cur)}")
|
|
69
|
+
|
|
70
|
+
last = parts[-1]
|
|
71
|
+
if isinstance(cur, dict):
|
|
72
|
+
cur[last] = value
|
|
73
|
+
elif isinstance(cur, list):
|
|
74
|
+
idx = int(last)
|
|
75
|
+
while len(cur) <= idx:
|
|
76
|
+
cur.append(None)
|
|
77
|
+
cur[idx] = value
|
|
78
|
+
else:
|
|
79
|
+
raise ValueError(f"Can't set into {type(cur)}")
|
|
80
|
+
|
|
81
|
+
def _deep_copy(self, obj: Any) -> Any:
|
|
82
|
+
if isinstance(obj, dict):
|
|
83
|
+
return {k: self._deep_copy(v) for k, v in obj.items()}
|
|
84
|
+
if isinstance(obj, list):
|
|
85
|
+
return [self._deep_copy(v) for v in obj]
|
|
86
|
+
return obj
|