Spaces:
Running
Running
Support optional inputs in simple executor.
Browse files
lynxkite-core/pyproject.toml
CHANGED
@@ -11,3 +11,6 @@ dependencies = [
|
|
11 |
dev = [
|
12 |
"pytest",
|
13 |
]
|
|
|
|
|
|
|
|
11 |
dev = [
|
12 |
"pytest",
|
13 |
]
|
14 |
+
|
15 |
+
[tool.pytest.ini_options]
|
16 |
+
asyncio_mode = "auto"
|
lynxkite-core/src/lynxkite/core/executors/simple.py
CHANGED
@@ -42,7 +42,11 @@ async def execute(ws: workspace.Workspace, catalog: ops.Catalog):
|
|
42 |
if i.name in edges and edges[i.name] in outputs:
|
43 |
inputs.append(outputs[edges[i.name]])
|
44 |
else:
|
45 |
-
|
|
|
|
|
|
|
|
|
46 |
if missing:
|
47 |
node.publish_error(f"Missing input: {', '.join(missing)}")
|
48 |
continue
|
|
|
42 |
if i.name in edges and edges[i.name] in outputs:
|
43 |
inputs.append(outputs[edges[i.name]])
|
44 |
else:
|
45 |
+
opt_type = ops.get_optional_type(i.type)
|
46 |
+
if opt_type is not None:
|
47 |
+
inputs.append(None)
|
48 |
+
else:
|
49 |
+
missing.append(i.name)
|
50 |
if missing:
|
51 |
node.publish_error(f"Missing input: {', '.join(missing)}")
|
52 |
continue
|
lynxkite-core/src/lynxkite/core/ops.py
CHANGED
@@ -149,6 +149,16 @@ def basic_outputs(*names):
|
|
149 |
return {name: Output(name=name, type=None) for name in names}
|
150 |
|
151 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
152 |
def _param_to_type(name, value, type):
|
153 |
value = value or ""
|
154 |
if type is int:
|
@@ -160,12 +170,9 @@ def _param_to_type(name, value, type):
|
|
160 |
if isinstance(type, enum.EnumMeta):
|
161 |
assert value in type.__members__, f"{value} is not an option for {name}."
|
162 |
return type[value]
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
return None if value == "" else _param_to_type(name, value, type)
|
167 |
-
case (type, types.NoneType):
|
168 |
-
return None if value == "" else _param_to_type(name, value, type)
|
169 |
if isinstance(type, typeof) and issubclass(type, pydantic.BaseModel):
|
170 |
try:
|
171 |
return type.model_validate_json(value)
|
|
|
149 |
return {name: Output(name=name, type=None) for name in names}
|
150 |
|
151 |
|
152 |
+
def get_optional_type(type):
|
153 |
+
"""For a type like `int | None`, returns `int`. Returns `None` otherwise."""
|
154 |
+
if isinstance(type, types.UnionType):
|
155 |
+
match type.__args__:
|
156 |
+
case (types.NoneType, type):
|
157 |
+
return type
|
158 |
+
case (type, types.NoneType):
|
159 |
+
return type
|
160 |
+
|
161 |
+
|
162 |
def _param_to_type(name, value, type):
|
163 |
value = value or ""
|
164 |
if type is int:
|
|
|
170 |
if isinstance(type, enum.EnumMeta):
|
171 |
assert value in type.__members__, f"{value} is not an option for {name}."
|
172 |
return type[value]
|
173 |
+
opt_type = get_optional_type(type)
|
174 |
+
if opt_type:
|
175 |
+
return None if value == "" else _param_to_type(name, value, opt_type)
|
|
|
|
|
|
|
176 |
if isinstance(type, typeof) and issubclass(type, pydantic.BaseModel):
|
177 |
try:
|
178 |
return type.model_validate_json(value)
|
lynxkite-core/src/lynxkite/core/workspace.py
CHANGED
@@ -125,7 +125,7 @@ class Workspace(BaseConfig):
|
|
125 |
return self.env in ops.EXECUTORS
|
126 |
|
127 |
async def execute(self):
|
128 |
-
await ops.EXECUTORS[self.env](self)
|
129 |
|
130 |
def save(self, path: str):
|
131 |
"""Persist the workspace to a local file in JSON format."""
|
@@ -201,3 +201,35 @@ class Workspace(BaseConfig):
|
|
201 |
if "data" not in nc:
|
202 |
nc["data"] = pycrdt.Map()
|
203 |
np._crdt = nc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
125 |
return self.env in ops.EXECUTORS
|
126 |
|
127 |
async def execute(self):
|
128 |
+
return await ops.EXECUTORS[self.env](self)
|
129 |
|
130 |
def save(self, path: str):
|
131 |
"""Persist the workspace to a local file in JSON format."""
|
|
|
201 |
if "data" not in nc:
|
202 |
nc["data"] = pycrdt.Map()
|
203 |
np._crdt = nc
|
204 |
+
|
205 |
+
def add_node(self, func):
|
206 |
+
"""For convenience in e.g. tests."""
|
207 |
+
node = WorkspaceNode(
|
208 |
+
id=func.__name__,
|
209 |
+
type=func.__op__.type,
|
210 |
+
data=WorkspaceNodeData(
|
211 |
+
title=func.__op__.name,
|
212 |
+
params={},
|
213 |
+
display=None,
|
214 |
+
input_metadata=None,
|
215 |
+
error=None,
|
216 |
+
status=NodeStatus.planned,
|
217 |
+
),
|
218 |
+
position=Position(x=0, y=0),
|
219 |
+
)
|
220 |
+
self.nodes.append(node)
|
221 |
+
return node
|
222 |
+
|
223 |
+
def add_edge(
|
224 |
+
self, source: WorkspaceNode, sourceHandle: str, target: WorkspaceNode, targetHandle: str
|
225 |
+
):
|
226 |
+
"""For convenience in e.g. tests."""
|
227 |
+
edge = WorkspaceEdge(
|
228 |
+
id=f"{source.id} {sourceHandle} to {target.id} {targetHandle}",
|
229 |
+
source=source.id,
|
230 |
+
target=target.id,
|
231 |
+
sourceHandle=sourceHandle,
|
232 |
+
targetHandle=targetHandle,
|
233 |
+
)
|
234 |
+
self.edges.append(edge)
|
235 |
+
return edge
|
lynxkite-core/tests/test_simple.py
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from lynxkite.core import ops, workspace
|
2 |
+
from lynxkite.core.executors import simple
|
3 |
+
|
4 |
+
|
5 |
+
async def test_optional_inputs():
|
6 |
+
@ops.op("test", "one")
|
7 |
+
def one():
|
8 |
+
return 1
|
9 |
+
|
10 |
+
@ops.op("test", "maybe add")
|
11 |
+
def maybe_add(a: int, b: int | None = None):
|
12 |
+
return a + (b or 0)
|
13 |
+
|
14 |
+
assert maybe_add.__op__.inputs == [
|
15 |
+
ops.Input(name="a", type=int, position="left"),
|
16 |
+
ops.Input(name="b", type=int | None, position="left"),
|
17 |
+
]
|
18 |
+
simple.register("test")
|
19 |
+
ws = workspace.Workspace(env="test", nodes=[], edges=[])
|
20 |
+
a = ws.add_node(one)
|
21 |
+
b = ws.add_node(maybe_add)
|
22 |
+
outputs = await ws.execute()
|
23 |
+
assert b.data.error == "Missing input: a"
|
24 |
+
ws.add_edge(a, "output", b, "a")
|
25 |
+
outputs = await ws.execute()
|
26 |
+
assert outputs[b.id, "output"] == 1
|