Spaces:
Running
Running
Update the LynxScribe ops with OpenAI API change.
Browse files
lynxkite-app/.gitignore
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
/src/lynxkite/app/web_assets
|
2 |
+
!/src/lynxkite/app/web_assets/__init__.py
|
3 |
+
!/src/lynxkite/app/web_assets/assets/__init__.py
|
lynxkite-app/src/lynxkite/app/main.py
CHANGED
@@ -20,7 +20,6 @@ def detect_plugins():
|
|
20 |
print("No modules found in lynxkite_plugins. Be sure to install some plugins.")
|
21 |
return {}
|
22 |
|
23 |
-
print(list(pkgutil.iter_modules(lynxkite_plugins.__path__)))
|
24 |
plugins = {}
|
25 |
for _, name, _ in pkgutil.iter_modules(lynxkite_plugins.__path__):
|
26 |
name = f"lynxkite_plugins.{name}"
|
|
|
20 |
print("No modules found in lynxkite_plugins. Be sure to install some plugins.")
|
21 |
return {}
|
22 |
|
|
|
23 |
plugins = {}
|
24 |
for _, name, _ in pkgutil.iter_modules(lynxkite_plugins.__path__):
|
25 |
name = f"lynxkite_plugins.{name}"
|
lynxkite-core/build/lib/lynxkite/executors/one_by_one.py
DELETED
@@ -1,175 +0,0 @@
|
|
1 |
-
"""A LynxKite executor that assumes most operations operate on their input one by one."""
|
2 |
-
|
3 |
-
from .. import ops
|
4 |
-
from .. import workspace
|
5 |
-
import orjson
|
6 |
-
import pandas as pd
|
7 |
-
import pydantic
|
8 |
-
import traceback
|
9 |
-
import inspect
|
10 |
-
import typing
|
11 |
-
|
12 |
-
|
13 |
-
class Context(ops.BaseConfig):
|
14 |
-
"""Passed to operation functions as "_ctx" if they have such a parameter."""
|
15 |
-
|
16 |
-
node: workspace.WorkspaceNode
|
17 |
-
last_result: typing.Any = None
|
18 |
-
|
19 |
-
|
20 |
-
class Output(ops.BaseConfig):
|
21 |
-
"""Return this to send values to specific outputs of a node."""
|
22 |
-
|
23 |
-
output_handle: str
|
24 |
-
value: dict
|
25 |
-
|
26 |
-
|
27 |
-
def df_to_list(df):
|
28 |
-
return df.to_dict(orient="records")
|
29 |
-
|
30 |
-
|
31 |
-
def has_ctx(op):
|
32 |
-
sig = inspect.signature(op.func)
|
33 |
-
return "_ctx" in sig.parameters
|
34 |
-
|
35 |
-
|
36 |
-
CACHES = {}
|
37 |
-
|
38 |
-
|
39 |
-
def register(env: str, cache: bool = True):
|
40 |
-
"""Registers the one-by-one executor."""
|
41 |
-
if cache:
|
42 |
-
CACHES[env] = {}
|
43 |
-
cache = CACHES[env]
|
44 |
-
else:
|
45 |
-
cache = None
|
46 |
-
ops.EXECUTORS[env] = lambda ws: execute(ws, ops.CATALOGS[env], cache=cache)
|
47 |
-
|
48 |
-
|
49 |
-
def get_stages(ws, catalog):
|
50 |
-
"""Inputs on top/bottom are batch inputs. We decompose the graph into a DAG of components along these edges."""
|
51 |
-
nodes = {n.id: n for n in ws.nodes}
|
52 |
-
batch_inputs = {}
|
53 |
-
inputs = {}
|
54 |
-
for edge in ws.edges:
|
55 |
-
inputs.setdefault(edge.target, []).append(edge.source)
|
56 |
-
node = nodes[edge.target]
|
57 |
-
op = catalog[node.data.title]
|
58 |
-
i = op.inputs[edge.targetHandle]
|
59 |
-
if i.position in "top or bottom":
|
60 |
-
batch_inputs.setdefault(edge.target, []).append(edge.source)
|
61 |
-
stages = []
|
62 |
-
for bt, bss in batch_inputs.items():
|
63 |
-
upstream = set(bss)
|
64 |
-
new = set(bss)
|
65 |
-
while new:
|
66 |
-
n = new.pop()
|
67 |
-
for i in inputs.get(n, []):
|
68 |
-
if i not in upstream:
|
69 |
-
upstream.add(i)
|
70 |
-
new.add(i)
|
71 |
-
stages.append(upstream)
|
72 |
-
stages.sort(key=lambda s: len(s))
|
73 |
-
stages.append(set(nodes))
|
74 |
-
return stages
|
75 |
-
|
76 |
-
|
77 |
-
def _default_serializer(obj):
|
78 |
-
if isinstance(obj, pydantic.BaseModel):
|
79 |
-
return obj.dict()
|
80 |
-
return {"__nonserializable__": id(obj)}
|
81 |
-
|
82 |
-
|
83 |
-
def make_cache_key(obj):
|
84 |
-
return orjson.dumps(obj, default=_default_serializer)
|
85 |
-
|
86 |
-
|
87 |
-
EXECUTOR_OUTPUT_CACHE = {}
|
88 |
-
|
89 |
-
|
90 |
-
async def await_if_needed(obj):
|
91 |
-
if inspect.isawaitable(obj):
|
92 |
-
return await obj
|
93 |
-
return obj
|
94 |
-
|
95 |
-
|
96 |
-
async def execute(ws, catalog, cache=None):
|
97 |
-
nodes = {n.id: n for n in ws.nodes}
|
98 |
-
contexts = {n.id: Context(node=n) for n in ws.nodes}
|
99 |
-
edges = {n.id: [] for n in ws.nodes}
|
100 |
-
for e in ws.edges:
|
101 |
-
edges[e.source].append(e)
|
102 |
-
tasks = {}
|
103 |
-
NO_INPUT = object() # Marker for initial tasks.
|
104 |
-
for node in ws.nodes:
|
105 |
-
node.data.error = None
|
106 |
-
op = catalog.get(node.data.title)
|
107 |
-
if op is None:
|
108 |
-
node.data.error = f'Operation "{node.data.title}" not found.'
|
109 |
-
continue
|
110 |
-
# Start tasks for nodes that have no non-batch inputs.
|
111 |
-
if all([i.position in "top or bottom" for i in op.inputs.values()]):
|
112 |
-
tasks[node.id] = [NO_INPUT]
|
113 |
-
batch_inputs = {}
|
114 |
-
# Run the rest until we run out of tasks.
|
115 |
-
stages = get_stages(ws, catalog)
|
116 |
-
for stage in stages:
|
117 |
-
next_stage = {}
|
118 |
-
while tasks:
|
119 |
-
n, ts = tasks.popitem()
|
120 |
-
if n not in stage:
|
121 |
-
next_stage.setdefault(n, []).extend(ts)
|
122 |
-
continue
|
123 |
-
node = nodes[n]
|
124 |
-
data = node.data
|
125 |
-
op = catalog[data.title]
|
126 |
-
params = {**data.params}
|
127 |
-
if has_ctx(op):
|
128 |
-
params["_ctx"] = contexts[node.id]
|
129 |
-
results = []
|
130 |
-
for task in ts:
|
131 |
-
try:
|
132 |
-
inputs = []
|
133 |
-
for i in op.inputs.values():
|
134 |
-
if i.position in "top or bottom":
|
135 |
-
assert (n, i.name) in batch_inputs, f"{i.name} is missing"
|
136 |
-
inputs.append(batch_inputs[(n, i.name)])
|
137 |
-
else:
|
138 |
-
inputs.append(task)
|
139 |
-
if cache is not None:
|
140 |
-
key = make_cache_key((inputs, params))
|
141 |
-
if key not in cache:
|
142 |
-
cache[key] = await await_if_needed(op(*inputs, **params))
|
143 |
-
result = cache[key]
|
144 |
-
else:
|
145 |
-
result = await await_if_needed(op(*inputs, **params))
|
146 |
-
except Exception as e:
|
147 |
-
traceback.print_exc()
|
148 |
-
data.error = str(e)
|
149 |
-
break
|
150 |
-
contexts[node.id].last_result = result
|
151 |
-
# Returned lists and DataFrames are considered multiple tasks.
|
152 |
-
if isinstance(result, pd.DataFrame):
|
153 |
-
result = df_to_list(result)
|
154 |
-
elif not isinstance(result, list):
|
155 |
-
result = [result]
|
156 |
-
results.extend(result)
|
157 |
-
else: # Finished all tasks without errors.
|
158 |
-
if (
|
159 |
-
op.type == "visualization"
|
160 |
-
or op.type == "table_view"
|
161 |
-
or op.type == "image"
|
162 |
-
):
|
163 |
-
data.display = results[0]
|
164 |
-
for edge in edges[node.id]:
|
165 |
-
t = nodes[edge.target]
|
166 |
-
op = catalog[t.data.title]
|
167 |
-
i = op.inputs[edge.targetHandle]
|
168 |
-
if i.position in "top or bottom":
|
169 |
-
batch_inputs.setdefault(
|
170 |
-
(edge.target, edge.targetHandle), []
|
171 |
-
).extend(results)
|
172 |
-
else:
|
173 |
-
tasks.setdefault(edge.target, []).extend(results)
|
174 |
-
tasks = next_stage
|
175 |
-
return contexts
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-core/build/lib/lynxkite/ops.py
DELETED
@@ -1,224 +0,0 @@
|
|
1 |
-
"""API for implementing LynxKite operations."""
|
2 |
-
|
3 |
-
from __future__ import annotations
|
4 |
-
import enum
|
5 |
-
import functools
|
6 |
-
import inspect
|
7 |
-
import pydantic
|
8 |
-
import typing
|
9 |
-
from typing_extensions import Annotated
|
10 |
-
|
11 |
-
CATALOGS = {}
|
12 |
-
EXECUTORS = {}
|
13 |
-
|
14 |
-
typeof = type # We have some arguments called "type".
|
15 |
-
|
16 |
-
|
17 |
-
def type_to_json(t):
|
18 |
-
if isinstance(t, type) and issubclass(t, enum.Enum):
|
19 |
-
return {"enum": list(t.__members__.keys())}
|
20 |
-
if getattr(t, "__metadata__", None):
|
21 |
-
return t.__metadata__[-1]
|
22 |
-
return {"type": str(t)}
|
23 |
-
|
24 |
-
|
25 |
-
Type = Annotated[typing.Any, pydantic.PlainSerializer(type_to_json, return_type=dict)]
|
26 |
-
LongStr = Annotated[str, {"format": "textarea"}]
|
27 |
-
PathStr = Annotated[str, {"format": "path"}]
|
28 |
-
CollapsedStr = Annotated[str, {"format": "collapsed"}]
|
29 |
-
NodeAttribute = Annotated[str, {"format": "node attribute"}]
|
30 |
-
EdgeAttribute = Annotated[str, {"format": "edge attribute"}]
|
31 |
-
|
32 |
-
|
33 |
-
class BaseConfig(pydantic.BaseModel):
|
34 |
-
model_config = pydantic.ConfigDict(
|
35 |
-
arbitrary_types_allowed=True,
|
36 |
-
)
|
37 |
-
|
38 |
-
|
39 |
-
class Parameter(BaseConfig):
|
40 |
-
"""Defines a parameter for an operation."""
|
41 |
-
|
42 |
-
name: str
|
43 |
-
default: typing.Any
|
44 |
-
type: Type = None
|
45 |
-
|
46 |
-
@staticmethod
|
47 |
-
def options(name, options, default=None):
|
48 |
-
e = enum.Enum(f"OptionsFor_{name}", options)
|
49 |
-
return Parameter.basic(name, e[default or options[0]], e)
|
50 |
-
|
51 |
-
@staticmethod
|
52 |
-
def collapsed(name, default, type=None):
|
53 |
-
return Parameter.basic(name, default, CollapsedStr)
|
54 |
-
|
55 |
-
@staticmethod
|
56 |
-
def basic(name, default=None, type=None):
|
57 |
-
if default is inspect._empty:
|
58 |
-
default = None
|
59 |
-
if type is None or type is inspect._empty:
|
60 |
-
type = typeof(default) if default is not None else None
|
61 |
-
return Parameter(name=name, default=default, type=type)
|
62 |
-
|
63 |
-
|
64 |
-
class Input(BaseConfig):
|
65 |
-
name: str
|
66 |
-
type: Type
|
67 |
-
position: str = "left"
|
68 |
-
|
69 |
-
|
70 |
-
class Output(BaseConfig):
|
71 |
-
name: str
|
72 |
-
type: Type
|
73 |
-
position: str = "right"
|
74 |
-
|
75 |
-
|
76 |
-
MULTI_INPUT = Input(name="multi", type="*")
|
77 |
-
|
78 |
-
|
79 |
-
def basic_inputs(*names):
|
80 |
-
return {name: Input(name=name, type=None) for name in names}
|
81 |
-
|
82 |
-
|
83 |
-
def basic_outputs(*names):
|
84 |
-
return {name: Output(name=name, type=None) for name in names}
|
85 |
-
|
86 |
-
|
87 |
-
class Op(BaseConfig):
|
88 |
-
func: typing.Callable = pydantic.Field(exclude=True)
|
89 |
-
name: str
|
90 |
-
params: dict[str, Parameter]
|
91 |
-
inputs: dict[str, Input]
|
92 |
-
outputs: dict[str, Output]
|
93 |
-
type: str = "basic" # The UI to use for this operation.
|
94 |
-
|
95 |
-
def __call__(self, *inputs, **params):
|
96 |
-
# Convert parameters.
|
97 |
-
for p in params:
|
98 |
-
if p in self.params:
|
99 |
-
if self.params[p].type == int:
|
100 |
-
params[p] = int(params[p])
|
101 |
-
elif self.params[p].type == float:
|
102 |
-
params[p] = float(params[p])
|
103 |
-
elif isinstance(self.params[p].type, enum.EnumMeta):
|
104 |
-
params[p] = self.params[p].type[params[p]]
|
105 |
-
res = self.func(*inputs, **params)
|
106 |
-
return res
|
107 |
-
|
108 |
-
|
109 |
-
def op(env: str, name: str, *, view="basic", outputs=None):
|
110 |
-
"""Decorator for defining an operation."""
|
111 |
-
|
112 |
-
def decorator(func):
|
113 |
-
sig = inspect.signature(func)
|
114 |
-
# Positional arguments are inputs.
|
115 |
-
inputs = {
|
116 |
-
name: Input(name=name, type=param.annotation)
|
117 |
-
for name, param in sig.parameters.items()
|
118 |
-
if param.kind != param.KEYWORD_ONLY
|
119 |
-
}
|
120 |
-
params = {}
|
121 |
-
for n, param in sig.parameters.items():
|
122 |
-
if param.kind == param.KEYWORD_ONLY and not n.startswith("_"):
|
123 |
-
params[n] = Parameter.basic(n, param.default, param.annotation)
|
124 |
-
if outputs:
|
125 |
-
_outputs = {name: Output(name=name, type=None) for name in outputs}
|
126 |
-
else:
|
127 |
-
_outputs = (
|
128 |
-
{"output": Output(name="output", type=None)} if view == "basic" else {}
|
129 |
-
)
|
130 |
-
op = Op(
|
131 |
-
func=func,
|
132 |
-
name=name,
|
133 |
-
params=params,
|
134 |
-
inputs=inputs,
|
135 |
-
outputs=_outputs,
|
136 |
-
type=view,
|
137 |
-
)
|
138 |
-
CATALOGS.setdefault(env, {})
|
139 |
-
CATALOGS[env][name] = op
|
140 |
-
func.__op__ = op
|
141 |
-
return func
|
142 |
-
|
143 |
-
return decorator
|
144 |
-
|
145 |
-
|
146 |
-
def input_position(**kwargs):
|
147 |
-
"""Decorator for specifying unusual positions for the inputs."""
|
148 |
-
|
149 |
-
def decorator(func):
|
150 |
-
op = func.__op__
|
151 |
-
for k, v in kwargs.items():
|
152 |
-
op.inputs[k].position = v
|
153 |
-
return func
|
154 |
-
|
155 |
-
return decorator
|
156 |
-
|
157 |
-
|
158 |
-
def output_position(**kwargs):
|
159 |
-
"""Decorator for specifying unusual positions for the outputs."""
|
160 |
-
|
161 |
-
def decorator(func):
|
162 |
-
op = func.__op__
|
163 |
-
for k, v in kwargs.items():
|
164 |
-
op.outputs[k].position = v
|
165 |
-
return func
|
166 |
-
|
167 |
-
return decorator
|
168 |
-
|
169 |
-
|
170 |
-
def no_op(*args, **kwargs):
|
171 |
-
if args:
|
172 |
-
return args[0]
|
173 |
-
return None
|
174 |
-
|
175 |
-
|
176 |
-
def register_passive_op(env: str, name: str, inputs=[], outputs=["output"], params=[]):
|
177 |
-
"""A passive operation has no associated code."""
|
178 |
-
op = Op(
|
179 |
-
func=no_op,
|
180 |
-
name=name,
|
181 |
-
params={p.name: p for p in params},
|
182 |
-
inputs=dict(
|
183 |
-
(i, Input(name=i, type=None)) if isinstance(i, str) else (i.name, i)
|
184 |
-
for i in inputs
|
185 |
-
),
|
186 |
-
outputs=dict(
|
187 |
-
(o, Output(name=o, type=None)) if isinstance(o, str) else (o.name, o)
|
188 |
-
for o in outputs
|
189 |
-
),
|
190 |
-
)
|
191 |
-
CATALOGS.setdefault(env, {})
|
192 |
-
CATALOGS[env][name] = op
|
193 |
-
return op
|
194 |
-
|
195 |
-
|
196 |
-
def register_executor(env: str):
|
197 |
-
"""Decorator for registering an executor."""
|
198 |
-
|
199 |
-
def decorator(func):
|
200 |
-
EXECUTORS[env] = func
|
201 |
-
return func
|
202 |
-
|
203 |
-
return decorator
|
204 |
-
|
205 |
-
|
206 |
-
def op_registration(env: str):
|
207 |
-
return functools.partial(op, env)
|
208 |
-
|
209 |
-
|
210 |
-
def passive_op_registration(env: str):
|
211 |
-
return functools.partial(register_passive_op, env)
|
212 |
-
|
213 |
-
|
214 |
-
def register_area(env, name, params=[]):
|
215 |
-
"""A node that represents an area. It can contain other nodes, but does not restrict movement in any way."""
|
216 |
-
op = Op(
|
217 |
-
func=no_op,
|
218 |
-
name=name,
|
219 |
-
params={p.name: p for p in params},
|
220 |
-
inputs={},
|
221 |
-
outputs={},
|
222 |
-
type="area",
|
223 |
-
)
|
224 |
-
CATALOGS[env][name] = op
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lynxkite-core/build/lib/lynxkite/workspace.py
DELETED
@@ -1,96 +0,0 @@
|
|
1 |
-
"""For working with LynxKite workspaces."""
|
2 |
-
|
3 |
-
from typing import Optional
|
4 |
-
import dataclasses
|
5 |
-
import os
|
6 |
-
import pydantic
|
7 |
-
import tempfile
|
8 |
-
from . import ops
|
9 |
-
|
10 |
-
|
11 |
-
class BaseConfig(pydantic.BaseModel):
|
12 |
-
model_config = pydantic.ConfigDict(
|
13 |
-
extra="allow",
|
14 |
-
)
|
15 |
-
|
16 |
-
|
17 |
-
class Position(BaseConfig):
|
18 |
-
x: float
|
19 |
-
y: float
|
20 |
-
|
21 |
-
|
22 |
-
class WorkspaceNodeData(BaseConfig):
|
23 |
-
title: str
|
24 |
-
params: dict
|
25 |
-
display: Optional[object] = None
|
26 |
-
error: Optional[str] = None
|
27 |
-
# Also contains a "meta" field when going out.
|
28 |
-
# This is ignored when coming back from the frontend.
|
29 |
-
|
30 |
-
|
31 |
-
class WorkspaceNode(BaseConfig):
|
32 |
-
id: str
|
33 |
-
type: str
|
34 |
-
data: WorkspaceNodeData
|
35 |
-
position: Position
|
36 |
-
|
37 |
-
|
38 |
-
class WorkspaceEdge(BaseConfig):
|
39 |
-
id: str
|
40 |
-
source: str
|
41 |
-
target: str
|
42 |
-
sourceHandle: str
|
43 |
-
targetHandle: str
|
44 |
-
|
45 |
-
|
46 |
-
class Workspace(BaseConfig):
|
47 |
-
env: str = ""
|
48 |
-
nodes: list[WorkspaceNode] = dataclasses.field(default_factory=list)
|
49 |
-
edges: list[WorkspaceEdge] = dataclasses.field(default_factory=list)
|
50 |
-
|
51 |
-
|
52 |
-
async def execute(ws: Workspace):
|
53 |
-
if ws.env in ops.EXECUTORS:
|
54 |
-
await ops.EXECUTORS[ws.env](ws)
|
55 |
-
|
56 |
-
|
57 |
-
def save(ws: Workspace, path: str):
|
58 |
-
j = ws.model_dump_json(indent=2)
|
59 |
-
dirname, basename = os.path.split(path)
|
60 |
-
# Create temp file in the same directory to make sure it's on the same filesystem.
|
61 |
-
with tempfile.NamedTemporaryFile(
|
62 |
-
"w", prefix=f".{basename}.", dir=dirname, delete=False
|
63 |
-
) as f:
|
64 |
-
temp_name = f.name
|
65 |
-
f.write(j)
|
66 |
-
os.replace(temp_name, path)
|
67 |
-
|
68 |
-
|
69 |
-
def load(path: str):
|
70 |
-
with open(path) as f:
|
71 |
-
j = f.read()
|
72 |
-
ws = Workspace.model_validate_json(j)
|
73 |
-
# Metadata is added after loading. This way code changes take effect on old boxes too.
|
74 |
-
_update_metadata(ws)
|
75 |
-
return ws
|
76 |
-
|
77 |
-
|
78 |
-
def _update_metadata(ws):
|
79 |
-
catalog = ops.CATALOGS.get(ws.env, {})
|
80 |
-
nodes = {node.id: node for node in ws.nodes}
|
81 |
-
done = set()
|
82 |
-
while len(done) < len(nodes):
|
83 |
-
for node in ws.nodes:
|
84 |
-
if node.id in done:
|
85 |
-
continue
|
86 |
-
data = node.data
|
87 |
-
op = catalog.get(data.title)
|
88 |
-
if op:
|
89 |
-
data.meta = op
|
90 |
-
node.type = op.type
|
91 |
-
if data.error == "Unknown operation.":
|
92 |
-
data.error = None
|
93 |
-
else:
|
94 |
-
data.error = "Unknown operation."
|
95 |
-
done.add(node.id)
|
96 |
-
return ws
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|