lynxkite / server /ops.py
darabos's picture
Remove sub_nodes/sub_flow/parentId.
15bcc0e
raw
history blame
6.05 kB
"""API for implementing LynxKite operations."""
from __future__ import annotations
import enum
import functools
import inspect
import pydantic
import typing
from typing_extensions import Annotated
CATALOGS = {}
EXECUTORS = {}
typeof = type # We have some arguments called "type".
def type_to_json(t):
if isinstance(t, type) and issubclass(t, enum.Enum):
return {"enum": list(t.__members__.keys())}
if getattr(t, "__metadata__", None):
return t.__metadata__[-1]
return {"type": str(t)}
Type = Annotated[typing.Any, pydantic.PlainSerializer(type_to_json, return_type=dict)]
LongStr = Annotated[str, {"format": "textarea"}]
PathStr = Annotated[str, {"format": "path"}]
CollapsedStr = Annotated[str, {"format": "collapsed"}]
NodeAttribute = Annotated[str, {"format": "node attribute"}]
EdgeAttribute = Annotated[str, {"format": "edge attribute"}]
class BaseConfig(pydantic.BaseModel):
model_config = pydantic.ConfigDict(
arbitrary_types_allowed=True,
)
class Parameter(BaseConfig):
"""Defines a parameter for an operation."""
name: str
default: typing.Any
type: Type = None
@staticmethod
def options(name, options, default=None):
e = enum.Enum(f"OptionsFor_{name}", options)
return Parameter.basic(name, e[default or options[0]], e)
@staticmethod
def collapsed(name, default, type=None):
return Parameter.basic(name, default, CollapsedStr)
@staticmethod
def basic(name, default=None, type=None):
if default is inspect._empty:
default = None
if type is None or type is inspect._empty:
type = typeof(default) if default is not None else None
return Parameter(name=name, default=default, type=type)
class Input(BaseConfig):
name: str
type: Type
position: str = "left"
class Output(BaseConfig):
name: str
type: Type
position: str = "right"
MULTI_INPUT = Input(name="multi", type="*")
def basic_inputs(*names):
return {name: Input(name=name, type=None) for name in names}
def basic_outputs(*names):
return {name: Output(name=name, type=None) for name in names}
class Op(BaseConfig):
func: typing.Callable = pydantic.Field(exclude=True)
name: str
params: dict[str, Parameter]
inputs: dict[str, Input]
outputs: dict[str, Output]
type: str = "basic" # The UI to use for this operation.
def __call__(self, *inputs, **params):
# Convert parameters.
for p in params:
if p in self.params:
if self.params[p].type == int:
params[p] = int(params[p])
elif self.params[p].type == float:
params[p] = float(params[p])
elif isinstance(self.params[p].type, enum.EnumMeta):
params[p] = self.params[p].type[params[p]]
res = self.func(*inputs, **params)
return res
def op(env: str, name: str, *, view="basic", outputs=None):
"""Decorator for defining an operation."""
def decorator(func):
sig = inspect.signature(func)
# Positional arguments are inputs.
inputs = {
name: Input(name=name, type=param.annotation)
for name, param in sig.parameters.items()
if param.kind != param.KEYWORD_ONLY
}
params = {}
for n, param in sig.parameters.items():
if param.kind == param.KEYWORD_ONLY and not n.startswith("_"):
params[n] = Parameter.basic(n, param.default, param.annotation)
if outputs:
_outputs = {name: Output(name=name, type=None) for name in outputs}
else:
_outputs = (
{"output": Output(name="output", type=None)} if view == "basic" else {}
)
op = Op(
func=func,
name=name,
params=params,
inputs=inputs,
outputs=_outputs,
type=view,
)
CATALOGS.setdefault(env, {})
CATALOGS[env][name] = op
func.__op__ = op
return func
return decorator
def input_position(**kwargs):
"""Decorator for specifying unusual positions for the inputs."""
def decorator(func):
op = func.__op__
for k, v in kwargs.items():
op.inputs[k].position = v
return func
return decorator
def output_position(**kwargs):
"""Decorator for specifying unusual positions for the outputs."""
def decorator(func):
op = func.__op__
for k, v in kwargs.items():
op.outputs[k].position = v
return func
return decorator
def no_op(*args, **kwargs):
if args:
return args[0]
return None
def register_passive_op(env: str, name: str, inputs=[], outputs=["output"], params=[]):
"""A passive operation has no associated code."""
op = Op(
func=no_op,
name=name,
params={p.name: p for p in params},
inputs=dict(
(i, Input(name=i, type=None)) if isinstance(i, str) else (i.name, i)
for i in inputs
),
outputs=dict(
(o, Output(name=o, type=None)) if isinstance(o, str) else (o.name, o)
for o in outputs
),
)
CATALOGS.setdefault(env, {})
CATALOGS[env][name] = op
return op
def register_executor(env: str):
"""Decorator for registering an executor."""
def decorator(func):
EXECUTORS[env] = func
return func
return decorator
def op_registration(env: str):
return functools.partial(op, env)
def passive_op_registration(env: str):
return functools.partial(register_passive_op, env)
def register_area(env, name, params=[]):
"""A node that represents an area. It can contain other nodes, but does not restrict movement in any way."""
op = Op(
func=no_op,
name=name,
params={p.name: p for p in params},
inputs={},
outputs={},
type="area",
)
CATALOGS[env][name] = op