darabos commited on
Commit
47c8ab4
·
1 Parent(s): 01d25c5

Show docstrings on UI.

Browse files
examples/fake_data.py CHANGED
@@ -7,6 +7,11 @@ faker = Faker()
7
 
8
  @op("LynxKite Graph Analytics", "Fake data")
9
  def fake(*, n=10):
 
 
 
 
 
10
  df = pd.DataFrame(
11
  {
12
  "name": [faker.name() for _ in range(n)],
 
7
 
8
  @op("LynxKite Graph Analytics", "Fake data")
9
  def fake(*, n=10):
10
+ """Creates a DataFrame with random-generated names and postal addresses.
11
+
12
+ Parameters:
13
+ n: Number of rows to create.
14
+ """
15
  df = pd.DataFrame(
16
  {
17
  "name": [faker.name() for _ in range(n)],
lynxkite-app/web/src/index.css CHANGED
@@ -91,10 +91,19 @@ body {
91
  display: flex;
92
  flex-direction: column;
93
  }
 
 
 
 
 
 
 
 
 
 
94
  }
95
 
96
  .expanded .lynxkite-node {
97
- overflow-y: auto;
98
  height: 100%;
99
  }
100
 
 
91
  display: flex;
92
  flex-direction: column;
93
  }
94
+
95
+ .node-documentation {
96
+ padding: 8px;
97
+ border-radius: 4px;
98
+ background: #fffa;
99
+ box-shadow: 0px 5px 50px 0px rgba(0, 0, 0, 0.1);
100
+ backdrop-filter: blur(10px);
101
+ font-size: 12px;
102
+ font-weight: initial;
103
+ }
104
  }
105
 
106
  .expanded .lynxkite-node {
 
107
  height: 100%;
108
  }
109
 
lynxkite-app/web/src/workspace/nodes/LynxKiteNode.tsx CHANGED
@@ -1,8 +1,16 @@
1
  import { Handle, NodeResizeControl, type Position, useReactFlow } from "@xyflow/react";
 
2
  import { ErrorBoundary } from "react-error-boundary";
 
 
 
3
  // @ts-ignore
4
  import ChevronDownRight from "~icons/tabler/chevron-down-right.jsx";
5
  // @ts-ignore
 
 
 
 
6
  import Skull from "~icons/tabler/skull.jsx";
7
 
8
  interface LynxKiteNodeProps {
@@ -82,8 +90,17 @@ function LynxKiteNodeComponent(props: LynxKiteNodeProps) {
82
  onClick={titleClicked}
83
  >
84
  {data.title}
85
- {data.error && <span className="title-icon">⚠️</span>}
86
- {expanded || <span className="title-icon">⋯</span>}
 
 
 
 
 
 
 
 
 
87
  </div>
88
  {expanded && (
89
  <>
@@ -137,3 +154,19 @@ export default function LynxKiteNode(Component: React.ComponentType<any>) {
137
  );
138
  };
139
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { Handle, NodeResizeControl, type Position, useReactFlow } from "@xyflow/react";
2
+ import type React from "react";
3
  import { ErrorBoundary } from "react-error-boundary";
4
+ import Markdown from "react-markdown";
5
+ // @ts-ignore
6
+ import AlertTriangle from "~icons/tabler/alert-triangle-filled.jsx";
7
  // @ts-ignore
8
  import ChevronDownRight from "~icons/tabler/chevron-down-right.jsx";
9
  // @ts-ignore
10
+ import Dots from "~icons/tabler/dots.jsx";
11
+ // @ts-ignore
12
+ import Help from "~icons/tabler/help.jsx";
13
+ // @ts-ignore
14
  import Skull from "~icons/tabler/skull.jsx";
15
 
16
  interface LynxKiteNodeProps {
 
90
  onClick={titleClicked}
91
  >
92
  {data.title}
93
+ {data.error && (
94
+ <span className="title-icon">
95
+ <AlertTriangle />
96
+ </span>
97
+ )}
98
+ {expanded || (
99
+ <span className="title-icon">
100
+ <Dots />
101
+ </span>
102
+ )}
103
+ <NodeDocumentation doc={data.meta?.value?.doc} width={props.width - 60} />
104
  </div>
105
  {expanded && (
106
  <>
 
154
  );
155
  };
156
  }
157
+
158
+ function NodeDocumentation(props: any) {
159
+ if (!props.doc) return null;
160
+ return (
161
+ <div className="dropdown dropdown-hover dropdown-top dropdown-end title-icon">
162
+ <button tabIndex={0}>
163
+ <Help />
164
+ </button>
165
+ <div className="node-documentation dropdown-content" style={{ width: props.width }}>
166
+ {props.doc.map(
167
+ (section: any) => section.kind === "text" && <Markdown>{section.value}</Markdown>,
168
+ )}
169
+ </div>
170
+ </div>
171
+ );
172
+ }
lynxkite-core/src/lynxkite/core/ops.py CHANGED
@@ -1,19 +1,23 @@
1
  """API for implementing LynxKite operations."""
2
 
3
  from __future__ import annotations
 
4
  import asyncio
5
  import enum
6
  import functools
 
 
7
  import importlib
8
  import inspect
9
  import pathlib
10
  import subprocess
11
  import traceback
12
- import joblib
13
  import types
14
- import pydantic
15
  import typing
16
  from dataclasses import dataclass
 
 
 
17
  from typing_extensions import Annotated
18
 
19
  if typing.TYPE_CHECKING:
@@ -180,6 +184,7 @@ class Op(BaseConfig):
180
  # TODO: Make type an enum with the possible values.
181
  type: str = "basic" # The UI to use for this operation.
182
  color: str = "orange" # The color of the operation in the UI.
 
183
 
184
  def __call__(self, *inputs, **params):
185
  # Convert parameters.
@@ -236,6 +241,7 @@ def op(
236
  """Decorator for defining an operation."""
237
 
238
  def decorator(func):
 
239
  sig = inspect.signature(func)
240
  _view = view
241
  if view == "matplotlib":
@@ -262,6 +268,7 @@ def op(
262
  _outputs = [Output(name="output", type=None)] if view == "basic" else []
263
  op = Op(
264
  func=func,
 
265
  name=name,
266
  params=_params,
267
  inputs=inputs,
@@ -279,10 +286,11 @@ def op(
279
 
280
  def matplotlib_to_image(func):
281
  """Decorator for converting a matplotlib figure to an image."""
282
- import matplotlib.pyplot as plt
283
  import base64
284
  import io
285
 
 
 
286
  @functools.wraps(func)
287
  def wrapper(*args, **kwargs):
288
  func(*args, **kwargs)
@@ -423,3 +431,10 @@ def run_user_script(script_path: pathlib.Path):
423
  spec = importlib.util.spec_from_file_location(script_path.stem, str(script_path))
424
  module = importlib.util.module_from_spec(spec)
425
  spec.loader.exec_module(module)
 
 
 
 
 
 
 
 
1
  """API for implementing LynxKite operations."""
2
 
3
  from __future__ import annotations
4
+
5
  import asyncio
6
  import enum
7
  import functools
8
+ import json
9
+ import griffe
10
  import importlib
11
  import inspect
12
  import pathlib
13
  import subprocess
14
  import traceback
 
15
  import types
 
16
  import typing
17
  from dataclasses import dataclass
18
+
19
+ import joblib
20
+ import pydantic
21
  from typing_extensions import Annotated
22
 
23
  if typing.TYPE_CHECKING:
 
184
  # TODO: Make type an enum with the possible values.
185
  type: str = "basic" # The UI to use for this operation.
186
  color: str = "orange" # The color of the operation in the UI.
187
+ doc: object = None
188
 
189
  def __call__(self, *inputs, **params):
190
  # Convert parameters.
 
241
  """Decorator for defining an operation."""
242
 
243
  def decorator(func):
244
+ doc = get_doc(func)
245
  sig = inspect.signature(func)
246
  _view = view
247
  if view == "matplotlib":
 
268
  _outputs = [Output(name="output", type=None)] if view == "basic" else []
269
  op = Op(
270
  func=func,
271
+ doc=doc,
272
  name=name,
273
  params=_params,
274
  inputs=inputs,
 
286
 
287
  def matplotlib_to_image(func):
288
  """Decorator for converting a matplotlib figure to an image."""
 
289
  import base64
290
  import io
291
 
292
+ import matplotlib.pyplot as plt
293
+
294
  @functools.wraps(func)
295
  def wrapper(*args, **kwargs):
296
  func(*args, **kwargs)
 
431
  spec = importlib.util.spec_from_file_location(script_path.stem, str(script_path))
432
  module = importlib.util.module_from_spec(spec)
433
  spec.loader.exec_module(module)
434
+
435
+
436
+ def get_doc(func):
437
+ if func.__doc__ is None:
438
+ return None
439
+ doc = griffe.Docstring(func.__doc__).parse("google")
440
+ return json.loads(json.dumps(doc, cls=griffe.JSONEncoder))
lynxkite-graph-analytics/src/lynxkite_graph_analytics/lynxkite_ops.py CHANGED
@@ -8,7 +8,6 @@ from collections import deque
8
 
9
  from . import core
10
  import grandcypher
11
- import joblib
12
  import matplotlib
13
  import networkx as nx
14
  import pandas as pd
@@ -16,7 +15,6 @@ import polars as pl
16
  import json
17
 
18
 
19
- mem = joblib.Memory(".joblib-cache")
20
  op = ops.op_registration(core.ENV)
21
 
22
 
@@ -82,8 +80,7 @@ def import_parquet(*, filename: str):
82
  return pd.read_parquet(filename)
83
 
84
 
85
- @op("Import CSV")
86
- @mem.cache
87
  def import_csv(*, filename: str, columns: str = "<from file>", separator: str = "<auto>"):
88
  """Imports a CSV file."""
89
  return pd.read_csv(
@@ -93,8 +90,7 @@ def import_csv(*, filename: str, columns: str = "<from file>", separator: str =
93
  )
94
 
95
 
96
- @op("Import GraphML")
97
- @mem.cache
98
  def import_graphml(*, filename: str):
99
  """Imports a GraphML file."""
100
  files = fsspec.open_files(filename, compression="infer")
@@ -105,8 +101,7 @@ def import_graphml(*, filename: str):
105
  raise ValueError(f"No .graphml file found at {filename}")
106
 
107
 
108
- @op("Graph from OSM")
109
- @mem.cache
110
  def import_osm(*, location: str):
111
  import osmnx as ox
112
 
 
8
 
9
  from . import core
10
  import grandcypher
 
11
  import matplotlib
12
  import networkx as nx
13
  import pandas as pd
 
15
  import json
16
 
17
 
 
18
  op = ops.op_registration(core.ENV)
19
 
20
 
 
80
  return pd.read_parquet(filename)
81
 
82
 
83
+ @op("Import CSV", slow=True)
 
84
  def import_csv(*, filename: str, columns: str = "<from file>", separator: str = "<auto>"):
85
  """Imports a CSV file."""
86
  return pd.read_csv(
 
90
  )
91
 
92
 
93
+ @op("Import GraphML", slow=True)
 
94
  def import_graphml(*, filename: str):
95
  """Imports a GraphML file."""
96
  files = fsspec.open_files(filename, compression="infer")
 
101
  raise ValueError(f"No .graphml file found at {filename}")
102
 
103
 
104
+ @op("Graph from OSM", slow=True)
 
105
  def import_osm(*, location: str):
106
  import osmnx as ox
107