Spaces:
Running
Running
File size: 7,463 Bytes
ca01fa3 d4a220c ca01fa3 f951d3a 0c44583 ca01fa3 a18645a d4a220c 3885cb0 d4a220c ca01fa3 75c875f 3833905 d4a220c 35abaee 7d647f8 d4a220c 35abaee d4a220c 3885cb0 1dc03e3 d4a220c 3885cb0 f951d3a 3833905 d4a220c 3833905 35abaee ca01fa3 d4a220c 28c40a9 d4a220c 1dc03e3 ca01fa3 3885cb0 28c40a9 d4a220c ca01fa3 9e91869 47eb7cc 9e91869 47eb7cc 9e91869 47eb7cc b6d30cb a18645a 28c40a9 3885cb0 28c40a9 a18645a ca01fa3 3010d5b ca01fa3 b6d30cb 3010d5b ca01fa3 3010d5b d4a220c 3885cb0 d4a220c ca01fa3 a18645a 0c44583 b6d30cb a18645a b6d30cb a18645a 0c44583 75c875f ca01fa3 28c40a9 ca01fa3 3885cb0 d4a220c 28c40a9 d4a220c 3010d5b 75c875f ef25918 ca01fa3 3885cb0 75c875f 3885cb0 28c40a9 75c875f 801415b 75c875f |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 |
'''API for implementing LynxKite operations.'''
from __future__ import annotations
import dataclasses
import enum
import functools
import inspect
import networkx as nx
import pandas as pd
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'}
]
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 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.
sub_nodes: list[Op] = None # If set, these nodes can be placed inside the operation's node.
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])
# Convert inputs.
inputs = list(inputs)
for i, (x, p) in enumerate(zip(inputs, self.inputs.values())):
if p.type == nx.Graph and isinstance(x, Bundle):
inputs[i] = x.to_nx()
elif p.type == Bundle and isinstance(x, nx.Graph):
inputs[i] = Bundle.from_nx(x)
res = self.func(*inputs, **params)
return res
@dataclasses.dataclass
class RelationDefinition:
'''Defines a set of edges.'''
df: str # The DataFrame that contains the edges.
source_column: str # The column in the edge DataFrame that contains the source node ID.
target_column: str # The column in the edge DataFrame that contains the target node ID.
source_table: str # The DataFrame that contains the source nodes.
target_table: str # The DataFrame that contains the target nodes.
source_key: str # The column in the source table that contains the node ID.
target_key: str # The column in the target table that contains the node ID.
@dataclasses.dataclass
class Bundle:
'''A collection of DataFrames and other data.
Can efficiently represent a knowledge graph (homogeneous or heterogeneous) or tabular data.
It can also carry other data, such as a trained model.
'''
dfs: dict[str, pd.DataFrame] = dataclasses.field(default_factory=dict)
relations: list[RelationDefinition] = dataclasses.field(default_factory=list)
other: dict[str, typing.Any] = None
@classmethod
def from_nx(cls, graph: nx.Graph):
edges = nx.to_pandas_edgelist(graph)
d = dict(graph.nodes(data=True))
nodes = pd.DataFrame(d.values(), index=d.keys())
nodes['id'] = nodes.index
return cls(
dfs={'edges': edges, 'nodes': nodes},
relations=[
RelationDefinition(
df='edges',
source_column='source',
target_column='target',
source_table='nodes',
target_table='nodes',
source_key='id',
target_key='id',
)
]
)
def to_nx(self):
graph = nx.from_pandas_edgelist(self.dfs['edges'])
nx.set_node_attributes(graph, self.dfs['nodes'].set_index('id').to_dict('index'))
return graph
def nx_node_attribute_func(name):
'''Decorator for wrapping a function that adds a NetworkX node attribute.'''
def decorator(func):
@functools.wraps(func)
def wrapper(graph: nx.Graph, **kwargs):
graph = graph.copy()
attr = func(graph, **kwargs)
nx.set_node_attributes(graph, attr, name)
return graph
return wrapper
return decorator
def op(env: str, name: str, *, view='basic', sub_nodes=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:
params[n] = Parameter.basic(n, param.default, param.annotation)
outputs = {'output': Output(name='output', type=None)} if view == 'basic' else {} # Maybe more fancy later.
op = Op(func=func, name=name, params=params, inputs=inputs, outputs=outputs, type=view)
if sub_nodes is not None:
op.sub_nodes = sub_nodes
op.type = 'sub_flow'
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 Bundle()
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)
|