darabos commited on
Commit
a9f42f4
·
unverified ·
2 Parent(s): 84c27bf 5bb4aa5

Merge branch 'main' into darabos-lint

Browse files
.github/workflows/test.yaml CHANGED
@@ -10,7 +10,7 @@ jobs:
10
  runs-on: ubuntu-latest
11
  steps:
12
  - uses: actions/checkout@v4
13
-
14
  - name: Install uv
15
  uses: astral-sh/setup-uv@v5
16
  with:
@@ -37,8 +37,8 @@ jobs:
37
  run: |
38
  cd lynxkite-app
39
  pytest
40
-
41
- - name: Run graph analytics tests
42
  run: |
43
  cd lynxkite-graph-analytics
44
  pytest
@@ -51,17 +51,17 @@ jobs:
51
  run: |
52
  cd lynxkite-app/web
53
  npm i
54
- npx playwright install --with-deps
55
 
56
  - name: Run Playwright tests
57
  run: |
58
  cd lynxkite-app/web
59
  npm run test
60
-
61
  - uses: actions/upload-artifact@v4
62
  name: Upload playwright report
63
  if: ${{ !cancelled() }}
64
  with:
65
  name: playwright-report
66
  path: lynxkite-app/web/playwright-report/
67
- retention-days: 30
 
10
  runs-on: ubuntu-latest
11
  steps:
12
  - uses: actions/checkout@v4
13
+
14
  - name: Install uv
15
  uses: astral-sh/setup-uv@v5
16
  with:
 
37
  run: |
38
  cd lynxkite-app
39
  pytest
40
+
41
+ - name: Run graph analytics tests
42
  run: |
43
  cd lynxkite-graph-analytics
44
  pytest
 
51
  run: |
52
  cd lynxkite-app/web
53
  npm i
54
+ npx playwright install --with-deps
55
 
56
  - name: Run Playwright tests
57
  run: |
58
  cd lynxkite-app/web
59
  npm run test
60
+
61
  - uses: actions/upload-artifact@v4
62
  name: Upload playwright report
63
  if: ${{ !cancelled() }}
64
  with:
65
  name: playwright-report
66
  path: lynxkite-app/web/playwright-report/
67
+ retention-days: 30
examples/Graph RAG CHANGED
@@ -7,7 +7,7 @@
7
  "data": {
8
  "title": "Input document",
9
  "params": {
10
- "filename": "data/example-pizza.md"
11
  },
12
  "display": null,
13
  "error": null,
 
7
  "data": {
8
  "title": "Input document",
9
  "params": {
10
+ "filename": "examples/example-pizza.md"
11
  },
12
  "display": null,
13
  "error": null,
lynxkite-app/src/lynxkite_app/config.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ """Some common configuration."""
2
+
3
+ import os
4
+ import pathlib
5
+
6
+
7
+ DATA_PATH = pathlib.Path(os.environ.get("LYNXKITE_DATA", "lynxkite_data"))
8
+ CRDT_PATH = pathlib.Path(os.environ.get("LYNXKITE_CRDT_DATA", "lynxkite_crdt_data"))
lynxkite-app/src/lynxkite_app/crdt.py CHANGED
@@ -12,10 +12,9 @@ import pycrdt_websocket.ystore
12
  import uvicorn
13
  import builtins
14
  from lynxkite.core import workspace, ops
 
15
 
16
  router = fastapi.APIRouter()
17
- DATA_PATH = pathlib.Path.cwd() / "data"
18
- CRDT_PATH = pathlib.Path.cwd() / "crdt_data"
19
 
20
 
21
  def ws_exception_handler(exception, log):
@@ -34,8 +33,8 @@ class WebsocketServer(pycrdt_websocket.WebsocketServer):
34
 
35
  The workspace is loaded from "crdt_data" if it exists there, or from "data", or a new workspace is created.
36
  """
37
- path = CRDT_PATH / f"{name}.crdt"
38
- assert path.is_relative_to(CRDT_PATH)
39
  ystore = pycrdt_websocket.ystore.FileYStore(path)
40
  ydoc = pycrdt.Doc()
41
  ydoc["workspace"] = ws = pycrdt.Map()
@@ -52,7 +51,7 @@ class WebsocketServer(pycrdt_websocket.WebsocketServer):
52
  if "edges" not in ws:
53
  ws["edges"] = pycrdt.Array()
54
  if "env" not in ws:
55
- ws["env"] = next(iter(ops.CATALOGS), 'unset')
56
  # We have two possible sources of truth for the workspaces, the YStore and the JSON files.
57
  # In case we didn't find the workspace in the YStore, we try to load it from the JSON files.
58
  try_to_load_workspace(ws, name)
@@ -162,7 +161,7 @@ def try_to_load_workspace(ws: pycrdt.Map, name: str):
162
  ws: CRDT object to udpate with the workspace contents.
163
  name: Name of the workspace to load.
164
  """
165
- json_path = f"data/{name}"
166
  if os.path.exists(json_path):
167
  ws_pyd = workspace.load(json_path)
168
  # We treat the display field as a black box, since it is a large
@@ -222,8 +221,8 @@ async def execute(
222
  await asyncio.sleep(delay)
223
  except asyncio.CancelledError:
224
  return
225
- path = DATA_PATH / name
226
- assert path.is_relative_to(DATA_PATH),"Provided workspace path is invalid"
227
  # Save user changes before executing, in case the execution fails.
228
  workspace.save(ws_pyd, path)
229
  await workspace.execute(ws_pyd)
 
12
  import uvicorn
13
  import builtins
14
  from lynxkite.core import workspace, ops
15
+ from . import config
16
 
17
  router = fastapi.APIRouter()
 
 
18
 
19
 
20
  def ws_exception_handler(exception, log):
 
33
 
34
  The workspace is loaded from "crdt_data" if it exists there, or from "data", or a new workspace is created.
35
  """
36
+ path = config.CRDT_PATH / f"{name}.crdt"
37
+ assert path.is_relative_to(config.CRDT_PATH)
38
  ystore = pycrdt_websocket.ystore.FileYStore(path)
39
  ydoc = pycrdt.Doc()
40
  ydoc["workspace"] = ws = pycrdt.Map()
 
51
  if "edges" not in ws:
52
  ws["edges"] = pycrdt.Array()
53
  if "env" not in ws:
54
+ ws["env"] = next(iter(ops.CATALOGS), "unset")
55
  # We have two possible sources of truth for the workspaces, the YStore and the JSON files.
56
  # In case we didn't find the workspace in the YStore, we try to load it from the JSON files.
57
  try_to_load_workspace(ws, name)
 
161
  ws: CRDT object to udpate with the workspace contents.
162
  name: Name of the workspace to load.
163
  """
164
+ json_path = f"{config.DATA_PATH}/{name}"
165
  if os.path.exists(json_path):
166
  ws_pyd = workspace.load(json_path)
167
  # We treat the display field as a black box, since it is a large
 
221
  await asyncio.sleep(delay)
222
  except asyncio.CancelledError:
223
  return
224
+ path = config.DATA_PATH / name
225
+ assert path.is_relative_to(config.DATA_PATH), "Provided workspace path is invalid"
226
  # Save user changes before executing, in case the execution fails.
227
  workspace.save(ws_pyd, path)
228
  await workspace.execute(ws_pyd)
lynxkite-app/src/lynxkite_app/main.py CHANGED
@@ -11,7 +11,7 @@ from fastapi.staticfiles import StaticFiles
11
  import starlette
12
  from lynxkite.core import ops
13
  from lynxkite.core import workspace
14
- from . import crdt
15
 
16
  if os.environ.get("NX_CUGRAPH_AUTOCONFIG", "").strip().lower() == "true":
17
  import cudf.pandas
@@ -50,8 +50,8 @@ class SaveRequest(workspace.BaseConfig):
50
 
51
 
52
  def save(req: SaveRequest):
53
- path = DATA_PATH / req.path
54
- assert path.is_relative_to(DATA_PATH)
55
  workspace.save(req.ws, path)
56
 
57
 
@@ -65,27 +65,23 @@ async def save_and_execute(req: SaveRequest):
65
 
66
  @app.post("/api/delete")
67
  async def delete_workspace(req: dict):
68
- json_path: pathlib.Path = DATA_PATH / req["path"]
69
- crdt_path: pathlib.Path = CRDT_PATH / f"{req['path']}.crdt"
70
- assert json_path.is_relative_to(DATA_PATH)
71
- assert crdt_path.is_relative_to(CRDT_PATH)
72
  json_path.unlink()
73
  crdt_path.unlink()
74
 
75
 
76
  @app.get("/api/load")
77
  def load(path: str):
78
- path = DATA_PATH / path
79
- assert path.is_relative_to(DATA_PATH)
80
  if not path.exists():
81
  return workspace.Workspace()
82
  return workspace.load(path)
83
 
84
 
85
- DATA_PATH = pathlib.Path(os.environ.get("LYNXKITE_DATA", "lynxkite_data"))
86
- CRDT_PATH = pathlib.Path(os.environ.get("LYNXKITE_CRDT_DATA", "lynxkite_crdt_data"))
87
-
88
-
89
  class DirectoryEntry(pydantic.BaseModel):
90
  name: str
91
  type: str
@@ -93,12 +89,13 @@ class DirectoryEntry(pydantic.BaseModel):
93
 
94
  @app.get("/api/dir/list")
95
  def list_dir(path: str):
96
- path = DATA_PATH / path
97
- assert path.is_relative_to(DATA_PATH)
98
  return sorted(
99
  [
100
  DirectoryEntry(
101
- p.relative_to(DATA_PATH), "directory" if p.is_dir() else "workspace"
 
102
  )
103
  for p in path.iterdir()
104
  ]
@@ -107,19 +104,17 @@ def list_dir(path: str):
107
 
108
  @app.post("/api/dir/mkdir")
109
  def make_dir(req: dict):
110
- path = DATA_PATH / req["path"]
111
- assert path.is_relative_to(DATA_PATH)
112
- assert not path.exists()
113
  path.mkdir()
114
- return list_dir(path.parent)
115
 
116
 
117
  @app.post("/api/dir/delete")
118
  def delete_dir(req: dict):
119
- path: pathlib.Path = DATA_PATH / req["path"]
120
- assert all([path.is_relative_to(DATA_PATH), path.exists(), path.is_dir()])
121
  shutil.rmtree(path)
122
- return list_dir(path.parent)
123
 
124
 
125
  @app.get("/api/service/{module_path:path}")
 
11
  import starlette
12
  from lynxkite.core import ops
13
  from lynxkite.core import workspace
14
+ from . import crdt, config
15
 
16
  if os.environ.get("NX_CUGRAPH_AUTOCONFIG", "").strip().lower() == "true":
17
  import cudf.pandas
 
50
 
51
 
52
  def save(req: SaveRequest):
53
+ path = config.DATA_PATH / req.path
54
+ assert path.is_relative_to(config.DATA_PATH)
55
  workspace.save(req.ws, path)
56
 
57
 
 
65
 
66
  @app.post("/api/delete")
67
  async def delete_workspace(req: dict):
68
+ json_path: pathlib.Path = config.DATA_PATH / req["path"]
69
+ crdt_path: pathlib.Path = config.CRDT_PATH / f"{req['path']}.crdt"
70
+ assert json_path.is_relative_to(config.DATA_PATH)
71
+ assert crdt_path.is_relative_to(config.CRDT_PATH)
72
  json_path.unlink()
73
  crdt_path.unlink()
74
 
75
 
76
  @app.get("/api/load")
77
  def load(path: str):
78
+ path = config.DATA_PATH / path
79
+ assert path.is_relative_to(config.DATA_PATH)
80
  if not path.exists():
81
  return workspace.Workspace()
82
  return workspace.load(path)
83
 
84
 
 
 
 
 
85
  class DirectoryEntry(pydantic.BaseModel):
86
  name: str
87
  type: str
 
89
 
90
  @app.get("/api/dir/list")
91
  def list_dir(path: str):
92
+ path = config.DATA_PATH / path
93
+ assert path.is_relative_to(config.DATA_PATH)
94
  return sorted(
95
  [
96
  DirectoryEntry(
97
+ p.relative_to(config.DATA_PATH),
98
+ "directory" if p.is_dir() else "workspace",
99
  )
100
  for p in path.iterdir()
101
  ]
 
104
 
105
  @app.post("/api/dir/mkdir")
106
  def make_dir(req: dict):
107
+ path = config.DATA_PATH / req["path"]
108
+ assert path.is_relative_to(config.DATA_PATH)
109
+ assert not path.exists(), f"{path} already exists"
110
  path.mkdir()
 
111
 
112
 
113
  @app.post("/api/dir/delete")
114
  def delete_dir(req: dict):
115
+ path: pathlib.Path = config.DATA_PATH / req["path"]
116
+ assert all([path.is_relative_to(config.DATA_PATH), path.exists(), path.is_dir()])
117
  shutil.rmtree(path)
 
118
 
119
 
120
  @app.get("/api/service/{module_path:path}")
lynxkite-app/src/lynxkite_app/web_assets/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """The build process (uv build) puts the frontend build artifacts here."""
lynxkite-app/src/lynxkite_app/web_assets/assets/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """More frontend build artifacts."""
lynxkite-app/tests/test_main.py CHANGED
@@ -1,6 +1,7 @@
1
  import uuid
2
  from fastapi.testclient import TestClient
3
- from lynxkite_app.main import app, detect_plugins, DATA_PATH
 
4
  import os
5
 
6
 
@@ -13,9 +14,9 @@ def test_detect_plugins_with_plugins():
13
  assert all(
14
  plugin in plugins.keys()
15
  for plugin in [
16
- "lynxkite_plugins.graph_analytics",
17
- "lynxkite_plugins.lynxscribe",
18
- "lynxkite_plugins.pillow_example",
19
  ]
20
  )
21
 
@@ -57,7 +58,7 @@ def test_save_and_load():
57
  def test_list_dir():
58
  test_dir = str(uuid.uuid4())
59
  test_dir_full_path = DATA_PATH / test_dir
60
- test_dir_full_path.mkdir(exist_ok=True)
61
  test_file = test_dir_full_path / "test_file.txt"
62
  test_file.touch()
63
  response = client.get(f"/api/dir/list?path={str(test_dir)}")
 
1
  import uuid
2
  from fastapi.testclient import TestClient
3
+ from lynxkite_app.main import app, detect_plugins
4
+ from lynxkite_app.config import DATA_PATH
5
  import os
6
 
7
 
 
14
  assert all(
15
  plugin in plugins.keys()
16
  for plugin in [
17
+ "lynxkite_graph_analytics",
18
+ "lynxkite_lynxscribe",
19
+ "lynxkite_pillow_example",
20
  ]
21
  )
22
 
 
58
  def test_list_dir():
59
  test_dir = str(uuid.uuid4())
60
  test_dir_full_path = DATA_PATH / test_dir
61
+ test_dir_full_path.mkdir(parents=True, exist_ok=True)
62
  test_file = test_dir_full_path / "test_file.txt"
63
  test_file.touch()
64
  response = client.get(f"/api/dir/list?path={str(test_dir)}")
lynxkite-app/web/playwright.config.ts CHANGED
@@ -23,7 +23,7 @@ export default defineConfig({
23
  },
24
  ],
25
  webServer: {
26
- command: "cd .. && lynxkite",
27
  url: "http://127.0.0.1:8000",
28
  reuseExistingServer: false,
29
  },
 
23
  },
24
  ],
25
  webServer: {
26
+ command: "cd ../.. && LYNXKITE_DATA=examples lynxkite",
27
  url: "http://127.0.0.1:8000",
28
  reuseExistingServer: false,
29
  },
lynxkite-core/src/lynxkite/core/workspace.py CHANGED
@@ -67,6 +67,7 @@ def save(ws: Workspace, path: str):
67
  """Persist a workspace to a local file in JSON format."""
68
  j = ws.model_dump_json(indent=2)
69
  dirname, basename = os.path.split(path)
 
70
  # Create temp file in the same directory to make sure it's on the same filesystem.
71
  with tempfile.NamedTemporaryFile(
72
  "w", prefix=f".{basename}.", dir=dirname, delete=False
 
67
  """Persist a workspace to a local file in JSON format."""
68
  j = ws.model_dump_json(indent=2)
69
  dirname, basename = os.path.split(path)
70
+ os.makedirs(dirname, exist_ok=True)
71
  # Create temp file in the same directory to make sure it's on the same filesystem.
72
  with tempfile.NamedTemporaryFile(
73
  "w", prefix=f".{basename}.", dir=dirname, delete=False