darabos commited on
Commit
ca01fa3
·
1 Parent(s): 3d534f4

Some backend execution. Errors.

Browse files
.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: -300, y: 0 },
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
- const boxes = [
110
- {
111
- type: 'basic',
112
- data: { title: 'Import Parquet', params: { filename: '/tmp/x.parquet' } },
113
- sourcePosition: Position.Right,
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
- g.nodes[2].data.graph = j.graph;
156
- backendWorkspace = JSON.stringify(g);
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
- const fuse = new Fuse(boxes, {
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() {