Spaces:
Running
Running
Some backend execution. Errors.
Browse files- .gitignore +11 -0
- main.py +0 -68
- run.sh +2 -0
- server/__init__.py +0 -0
- server/basic_ops.py +42 -0
- server/main.py +81 -0
- server/ops.py +53 -0
- web/.gitignore +0 -11
- web/src/LynxKiteFlow.svelte +15 -37
- web/src/LynxKiteNode.svelte +14 -2
- web/src/NodeSearch.svelte +1 -1
.gitignore
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Editor directories and files
|
2 |
+
.vscode/*
|
3 |
+
!.vscode/extensions.json
|
4 |
+
.idea
|
5 |
+
.DS_Store
|
6 |
+
*.suo
|
7 |
+
*.ntvs*
|
8 |
+
*.njsproj
|
9 |
+
*.sln
|
10 |
+
*.sw?
|
11 |
+
__pycache__
|
main.py
DELETED
@@ -1,68 +0,0 @@
|
|
1 |
-
from typing import Union
|
2 |
-
import fastapi
|
3 |
-
import pydantic
|
4 |
-
import networkx as nx
|
5 |
-
|
6 |
-
class Position(pydantic.BaseModel):
|
7 |
-
x: float
|
8 |
-
y: float
|
9 |
-
|
10 |
-
class WorkspaceNodeData(pydantic.BaseModel):
|
11 |
-
title: str
|
12 |
-
params: dict
|
13 |
-
|
14 |
-
class WorkspaceNode(pydantic.BaseModel):
|
15 |
-
id: str
|
16 |
-
type: str
|
17 |
-
data: WorkspaceNodeData
|
18 |
-
position: Position
|
19 |
-
|
20 |
-
class WorkspaceEdge(pydantic.BaseModel):
|
21 |
-
id: str
|
22 |
-
source: str
|
23 |
-
target: str
|
24 |
-
|
25 |
-
class Workspace(pydantic.BaseModel):
|
26 |
-
nodes: list[WorkspaceNode]
|
27 |
-
edges: list[WorkspaceEdge]
|
28 |
-
|
29 |
-
|
30 |
-
app = fastapi.FastAPI()
|
31 |
-
|
32 |
-
|
33 |
-
@app.get("/")
|
34 |
-
def read_root():
|
35 |
-
return {"Hello": "World"}
|
36 |
-
|
37 |
-
|
38 |
-
@app.get("/items/{item_id}")
|
39 |
-
def read_item(item_id: int, q: Union[str, None] = None):
|
40 |
-
return {"item_id": item_id, "q": q}
|
41 |
-
|
42 |
-
|
43 |
-
@app.post("/api/save")
|
44 |
-
def save(ws: Workspace):
|
45 |
-
print(ws)
|
46 |
-
G = nx.scale_free_graph(4)
|
47 |
-
return {'graph':{
|
48 |
-
'attributes': {
|
49 |
-
'name': 'My Graph'
|
50 |
-
},
|
51 |
-
'options': {
|
52 |
-
'allowSelfLoops': True,
|
53 |
-
'multi': False,
|
54 |
-
'type': 'mixed'
|
55 |
-
},
|
56 |
-
'nodes': [
|
57 |
-
{'key': 'Thomas'},
|
58 |
-
{'key': 'Eric'}
|
59 |
-
],
|
60 |
-
'edges': [
|
61 |
-
{
|
62 |
-
'key': 'T->E',
|
63 |
-
'source': 'Thomas',
|
64 |
-
'target': 'Eric',
|
65 |
-
}
|
66 |
-
]
|
67 |
-
}}
|
68 |
-
return {"graph": list(nx.to_edgelist(G))}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
run.sh
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
#!/bin/bash -xue
|
2 |
+
uvicorn server.main:app --reload
|
server/__init__.py
ADDED
File without changes
|
server/basic_ops.py
ADDED
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'''Some operations. To be split into separate files when we have more.'''
|
2 |
+
from . import ops
|
3 |
+
import pandas as pd
|
4 |
+
import networkx as nx
|
5 |
+
|
6 |
+
@ops.op("Import Parquet")
|
7 |
+
def import_parquet(*, filename: str):
|
8 |
+
'''Imports a parquet file.'''
|
9 |
+
return pd.read_parquet(filename)
|
10 |
+
|
11 |
+
@ops.op("Create scale-free graph")
|
12 |
+
def create_scale_free_graph(*, nodes: int):
|
13 |
+
'''Creates a scale-free graph with the given number of nodes.'''
|
14 |
+
return nx.scale_free_graph(nodes)
|
15 |
+
|
16 |
+
@ops.op("Compute PageRank")
|
17 |
+
def compute_pagerank(graph, *, damping: 0.85, iterations: 3):
|
18 |
+
return nx.pagerank(graph)
|
19 |
+
|
20 |
+
@ops.op("Visualize graph")
|
21 |
+
def visualize_graph(graph) -> 'graphviz':
|
22 |
+
return {'graph':{
|
23 |
+
'attributes': {
|
24 |
+
'name': 'My Graph'
|
25 |
+
},
|
26 |
+
'options': {
|
27 |
+
'allowSelfLoops': True,
|
28 |
+
'multi': False,
|
29 |
+
'type': 'mixed'
|
30 |
+
},
|
31 |
+
'nodes': [
|
32 |
+
{'key': 'Thomas'},
|
33 |
+
{'key': 'Eric'}
|
34 |
+
],
|
35 |
+
'edges': [
|
36 |
+
{
|
37 |
+
'key': 'T->E',
|
38 |
+
'source': 'Thomas',
|
39 |
+
'target': 'Eric',
|
40 |
+
}
|
41 |
+
]
|
42 |
+
}}
|
server/main.py
ADDED
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Optional
|
2 |
+
import fastapi
|
3 |
+
import pydantic
|
4 |
+
from . import ops
|
5 |
+
from . import basic_ops
|
6 |
+
|
7 |
+
class BaseConfig(pydantic.BaseModel):
|
8 |
+
model_config = pydantic.ConfigDict(
|
9 |
+
extra='allow',
|
10 |
+
)
|
11 |
+
|
12 |
+
class Position(BaseConfig):
|
13 |
+
x: float
|
14 |
+
y: float
|
15 |
+
|
16 |
+
class WorkspaceNodeData(BaseConfig):
|
17 |
+
title: str
|
18 |
+
params: dict
|
19 |
+
display: Optional[object] = None
|
20 |
+
error: Optional[str] = None
|
21 |
+
|
22 |
+
class WorkspaceNode(BaseConfig):
|
23 |
+
id: str
|
24 |
+
type: str
|
25 |
+
data: WorkspaceNodeData
|
26 |
+
position: Position
|
27 |
+
|
28 |
+
class WorkspaceEdge(BaseConfig):
|
29 |
+
id: str
|
30 |
+
source: str
|
31 |
+
target: str
|
32 |
+
|
33 |
+
class Workspace(BaseConfig):
|
34 |
+
nodes: list[WorkspaceNode]
|
35 |
+
edges: list[WorkspaceEdge]
|
36 |
+
|
37 |
+
|
38 |
+
app = fastapi.FastAPI()
|
39 |
+
|
40 |
+
|
41 |
+
@app.get("/api/catalog")
|
42 |
+
def get_catalog():
|
43 |
+
return [
|
44 |
+
{
|
45 |
+
'type': op.type,
|
46 |
+
'data': { 'title': op.name, 'params': op.params },
|
47 |
+
'targetPosition': 'left' if op.inputs else None,
|
48 |
+
'sourcePosition': 'right' if op.outputs else None,
|
49 |
+
}
|
50 |
+
for op in ops.ALL_OPS.values()]
|
51 |
+
|
52 |
+
def execute(ws):
|
53 |
+
nodes = ws.nodes
|
54 |
+
outputs = {}
|
55 |
+
failed = 0
|
56 |
+
while len(outputs) + failed < len(nodes):
|
57 |
+
for node in nodes:
|
58 |
+
if node.id in outputs:
|
59 |
+
continue
|
60 |
+
inputs = [edge.source for edge in ws.edges if edge.target == node.id]
|
61 |
+
if all(input in outputs for input in inputs):
|
62 |
+
inputs = [outputs[input] for input in inputs]
|
63 |
+
data = node.data
|
64 |
+
op = ops.ALL_OPS[data.title]
|
65 |
+
try:
|
66 |
+
output = op(*inputs, **data.params)
|
67 |
+
except Exception as e:
|
68 |
+
data.error = str(e)
|
69 |
+
failed += 1
|
70 |
+
continue
|
71 |
+
outputs[node.id] = output
|
72 |
+
if op.type == 'graphviz':
|
73 |
+
data.graph = output
|
74 |
+
|
75 |
+
|
76 |
+
@app.post("/api/save")
|
77 |
+
def save(ws: Workspace):
|
78 |
+
print(ws)
|
79 |
+
execute(ws)
|
80 |
+
print('exec done', ws)
|
81 |
+
return ws
|
server/ops.py
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
'''API for implementing LynxKite operations.'''
|
2 |
+
import dataclasses
|
3 |
+
import inspect
|
4 |
+
|
5 |
+
ALL_OPS = {}
|
6 |
+
|
7 |
+
@dataclasses.dataclass
|
8 |
+
class Op:
|
9 |
+
func: callable
|
10 |
+
name: str
|
11 |
+
params: dict
|
12 |
+
inputs: dict
|
13 |
+
outputs: dict
|
14 |
+
type: str
|
15 |
+
|
16 |
+
def __call__(self, *inputs, **params):
|
17 |
+
# Do conversions here.
|
18 |
+
res = self.func(*inputs, **params)
|
19 |
+
return res
|
20 |
+
|
21 |
+
@dataclasses.dataclass
|
22 |
+
class EdgeDefinition:
|
23 |
+
df: str
|
24 |
+
source_column: str
|
25 |
+
target_column: str
|
26 |
+
source_table: str
|
27 |
+
target_table: str
|
28 |
+
source_key: str
|
29 |
+
target_key: str
|
30 |
+
|
31 |
+
@dataclasses.dataclass
|
32 |
+
class Bundle:
|
33 |
+
dfs: dict
|
34 |
+
edges: list[EdgeDefinition]
|
35 |
+
|
36 |
+
def op(name):
|
37 |
+
'''Decorator for defining an operation.'''
|
38 |
+
def decorator(func):
|
39 |
+
type = func.__annotations__.get('return') or 'basic'
|
40 |
+
sig = inspect.signature(func)
|
41 |
+
# Positional arguments are inputs.
|
42 |
+
inputs = {
|
43 |
+
name: param.annotation
|
44 |
+
for name, param in sig.parameters.items()
|
45 |
+
if param.kind != param.KEYWORD_ONLY}
|
46 |
+
params = {
|
47 |
+
name: param.default if param.default is not inspect._empty else None
|
48 |
+
for name, param in sig.parameters.items()
|
49 |
+
if param.kind == param.KEYWORD_ONLY}
|
50 |
+
op = Op(func, name, params=params, inputs=inputs, outputs={'output': 'yes'}, type=type)
|
51 |
+
ALL_OPS[name] = op
|
52 |
+
return func
|
53 |
+
return decorator
|
web/.gitignore
CHANGED
@@ -11,14 +11,3 @@ node_modules
|
|
11 |
dist
|
12 |
dist-ssr
|
13 |
*.local
|
14 |
-
|
15 |
-
# Editor directories and files
|
16 |
-
.vscode/*
|
17 |
-
!.vscode/extensions.json
|
18 |
-
.idea
|
19 |
-
.DS_Store
|
20 |
-
*.suo
|
21 |
-
*.ntvs*
|
22 |
-
*.njsproj
|
23 |
-
*.sln
|
24 |
-
*.sw?
|
|
|
11 |
dist
|
12 |
dist-ssr
|
13 |
*.local
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web/src/LynxKiteFlow.svelte
CHANGED
@@ -37,7 +37,7 @@
|
|
37 |
id: '3',
|
38 |
type: 'basic',
|
39 |
data: { title: 'Import Parquet', params: { filename: '/tmp/x.parquet' } },
|
40 |
-
position: { x: -
|
41 |
sourcePosition: Position.Right,
|
42 |
},
|
43 |
{
|
@@ -54,17 +54,13 @@
|
|
54 |
id: '3-1',
|
55 |
source: '3',
|
56 |
target: '1',
|
57 |
-
markerEnd: {
|
58 |
-
type: MarkerType.ArrowClosed,
|
59 |
-
},
|
60 |
},
|
61 |
{
|
62 |
id: '3-4',
|
63 |
source: '1',
|
64 |
target: '4',
|
65 |
-
markerEnd: {
|
66 |
-
type: MarkerType.ArrowClosed,
|
67 |
-
},
|
68 |
},
|
69 |
]);
|
70 |
|
@@ -105,30 +101,13 @@
|
|
105 |
});
|
106 |
closeNodeSearch();
|
107 |
}
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
{
|
116 |
-
type: 'basic',
|
117 |
-
data: { title: 'Export Parquet', params: { filename: '/tmp/x.parquet' } },
|
118 |
-
targetPosition: Position.Left,
|
119 |
-
},
|
120 |
-
{
|
121 |
-
type: 'graphviz',
|
122 |
-
data: { title: 'Visualize graph', params: {} },
|
123 |
-
targetPosition: Position.Left,
|
124 |
-
},
|
125 |
-
{
|
126 |
-
type: 'basic',
|
127 |
-
data: { title: 'Compute PageRank', params: { damping: 0.85, iterations: 3 } },
|
128 |
-
sourcePosition: Position.Right,
|
129 |
-
targetPosition: Position.Left,
|
130 |
-
},
|
131 |
-
];
|
132 |
|
133 |
let nodeSearchPos: XYPosition | undefined = undefined;
|
134 |
|
@@ -143,6 +122,7 @@
|
|
143 |
}
|
144 |
const ws = JSON.stringify(g);
|
145 |
if (ws === backendWorkspace) return;
|
|
|
146 |
backendWorkspace = ws;
|
147 |
const res = await fetch('/api/save', {
|
148 |
method: 'POST',
|
@@ -152,9 +132,8 @@
|
|
152 |
body: JSON.stringify(g),
|
153 |
});
|
154 |
const j = await res.json();
|
155 |
-
|
156 |
-
|
157 |
-
nodes.set(g.nodes);
|
158 |
});
|
159 |
</script>
|
160 |
|
@@ -165,10 +144,9 @@
|
|
165 |
maxZoom={1.5}
|
166 |
minZoom={0.3}
|
167 |
>
|
168 |
-
<Background />
|
169 |
<Controls />
|
170 |
-
<Background />
|
171 |
<MiniMap />
|
172 |
-
{#if nodeSearchPos}<NodeSearch boxes={boxes} on:cancel={closeNodeSearch} on:add={addNode} pos={nodeSearchPos} />{/if}
|
173 |
</SvelteFlow>
|
174 |
</div>
|
|
|
37 |
id: '3',
|
38 |
type: 'basic',
|
39 |
data: { title: 'Import Parquet', params: { filename: '/tmp/x.parquet' } },
|
40 |
+
position: { x: -400, y: 0 },
|
41 |
sourcePosition: Position.Right,
|
42 |
},
|
43 |
{
|
|
|
54 |
id: '3-1',
|
55 |
source: '3',
|
56 |
target: '1',
|
57 |
+
// markerEnd: { type: MarkerType.ArrowClosed },
|
|
|
|
|
58 |
},
|
59 |
{
|
60 |
id: '3-4',
|
61 |
source: '1',
|
62 |
target: '4',
|
63 |
+
// markerEnd: { type: MarkerType.ArrowClosed },
|
|
|
|
|
64 |
},
|
65 |
]);
|
66 |
|
|
|
101 |
});
|
102 |
closeNodeSearch();
|
103 |
}
|
104 |
+
const boxes = writable([]);
|
105 |
+
async function getBoxes() {
|
106 |
+
const res = await fetch('/api/catalog');
|
107 |
+
const j = await res.json();
|
108 |
+
boxes.set(j);
|
109 |
+
}
|
110 |
+
getBoxes();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
111 |
|
112 |
let nodeSearchPos: XYPosition | undefined = undefined;
|
113 |
|
|
|
122 |
}
|
123 |
const ws = JSON.stringify(g);
|
124 |
if (ws === backendWorkspace) return;
|
125 |
+
console.log('current vs backend', '\n' + ws, '\n' + backendWorkspace);
|
126 |
backendWorkspace = ws;
|
127 |
const res = await fetch('/api/save', {
|
128 |
method: 'POST',
|
|
|
132 |
body: JSON.stringify(g),
|
133 |
});
|
134 |
const j = await res.json();
|
135 |
+
backendWorkspace = JSON.stringify(j);
|
136 |
+
nodes.set(j.nodes);
|
|
|
137 |
});
|
138 |
</script>
|
139 |
|
|
|
144 |
maxZoom={1.5}
|
145 |
minZoom={0.3}
|
146 |
>
|
147 |
+
<Background patternColor="#39bcf3" />
|
148 |
<Controls />
|
|
|
149 |
<MiniMap />
|
150 |
+
{#if nodeSearchPos}<NodeSearch boxes={$boxes} on:cancel={closeNodeSearch} on:add={addNode} pos={nodeSearchPos} />{/if}
|
151 |
</SvelteFlow>
|
152 |
</div>
|
web/src/LynxKiteNode.svelte
CHANGED
@@ -28,8 +28,12 @@
|
|
28 |
<div class="lynxkite-node">
|
29 |
<div class="title" on:click={titleClicked}>
|
30 |
{data.title}
|
|
|
31 |
</div>
|
32 |
{#if expanded}
|
|
|
|
|
|
|
33 |
<slot />
|
34 |
{/if}
|
35 |
{#if sourcePosition}
|
@@ -42,8 +46,18 @@
|
|
42 |
</div>
|
43 |
|
44 |
<style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
45 |
.node-container {
|
46 |
padding: 8px;
|
|
|
|
|
47 |
}
|
48 |
.lynxkite-node {
|
49 |
box-shadow: 0px 5px 50px 0px rgba(0, 0, 0, 0.3);
|
@@ -53,7 +67,5 @@
|
|
53 |
background: #ff8800;
|
54 |
font-weight: bold;
|
55 |
padding: 8px;
|
56 |
-
min-width: 170px;
|
57 |
-
max-width: 300px;
|
58 |
}
|
59 |
</style>
|
|
|
28 |
<div class="lynxkite-node">
|
29 |
<div class="title" on:click={titleClicked}>
|
30 |
{data.title}
|
31 |
+
{#if data.error}<span class="error-sign">⚠️</span>{/if}
|
32 |
</div>
|
33 |
{#if expanded}
|
34 |
+
{#if data.error}
|
35 |
+
<div class="error">{data.error}</div>
|
36 |
+
{/if}
|
37 |
<slot />
|
38 |
{/if}
|
39 |
{#if sourcePosition}
|
|
|
46 |
</div>
|
47 |
|
48 |
<style>
|
49 |
+
.error {
|
50 |
+
background: #ffdddd;
|
51 |
+
padding: 8px;
|
52 |
+
font-size: 12px;
|
53 |
+
}
|
54 |
+
.error-sign {
|
55 |
+
float: right;
|
56 |
+
}
|
57 |
.node-container {
|
58 |
padding: 8px;
|
59 |
+
min-width: 170px;
|
60 |
+
max-width: 300px;
|
61 |
}
|
62 |
.lynxkite-node {
|
63 |
box-shadow: 0px 5px 50px 0px rgba(0, 0, 0, 0.3);
|
|
|
67 |
background: #ff8800;
|
68 |
font-weight: bold;
|
69 |
padding: 8px;
|
|
|
|
|
70 |
}
|
71 |
</style>
|
web/src/NodeSearch.svelte
CHANGED
@@ -8,7 +8,7 @@
|
|
8 |
let hits = [];
|
9 |
let selectedIndex = 0;
|
10 |
onMount(() => searchBox.focus());
|
11 |
-
|
12 |
keys: ['data.title']
|
13 |
})
|
14 |
function onInput() {
|
|
|
8 |
let hits = [];
|
9 |
let selectedIndex = 0;
|
10 |
onMount(() => searchBox.focus());
|
11 |
+
$: fuse = new Fuse(boxes, {
|
12 |
keys: ['data.title']
|
13 |
})
|
14 |
function onInput() {
|