File size: 4,156 Bytes
338e6a4
 
bfaa386
dc0212e
338e6a4
bfaa386
 
 
 
b8b73b2
ca01fa3
c3ebd8b
b8b73b2
dc66cc7
145d037
 
c4e194f
 
9de79f2
dc66cc7
e1a2778
 
 
338e6a4
 
 
 
e1a2778
338e6a4
e1a2778
 
 
 
ca01fa3
2720e84
 
ca01fa3
892d010
ca01fa3
 
75c875f
 
892d010
 
ca01fa3
 
b600751
05acf81
b600751
05acf81
892d010
05acf81
9de79f2
 
b600751
05acf81
892d010
ca01fa3
eee9365
05acf81
eee9365
05acf81
 
 
892d010
dc0212e
 
9de79f2
 
 
 
dc0212e
 
 
 
05acf81
 
9de79f2
 
05acf81
b600751
 
b8b73b2
892d010
b8b73b2
 
 
 
 
892d010
b8b73b2
 
9de79f2
 
892d010
 
 
9de79f2
 
892d010
 
 
 
 
05acf81
 
 
9de79f2
 
b3ecf8d
05acf81
c3ebd8b
892d010
dc0212e
 
9de79f2
 
dc0212e
 
 
dcedc35
 
892d010
e1a2778
dcedc35
 
 
 
 
 
e1a2778
dcedc35
145d037
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338e6a4
145d037
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
"""The FastAPI server for serving the LynxKite application."""

import os
import shutil

if os.environ.get("NX_CUGRAPH_AUTOCONFIG", "").strip().lower() == "true":
    import cudf.pandas

    cudf.pandas.install()
import dataclasses
import fastapi
import importlib
import pathlib
import pkgutil
from fastapi.staticfiles import StaticFiles
import starlette
from lynxkite.core import ops
from lynxkite.core import workspace
from . import crdt, config


def detect_plugins():
    plugins = {}
    for _, name, _ in pkgutil.iter_modules():
        if name.startswith("lynxkite_"):
            print(f"Importing {name}")
            plugins[name] = importlib.import_module(name)
    if not plugins:
        print("No LynxKite plugins found. Be sure to install some!")
    return plugins


lynxkite_plugins = detect_plugins()

app = fastapi.FastAPI(lifespan=crdt.lifespan)
app.include_router(crdt.router)


@app.get("/api/catalog")
def get_catalog():
    return {
        k: {op.name: op.model_dump() for op in v.values()}
        for k, v in ops.CATALOGS.items()
    }


class SaveRequest(workspace.BaseConfig):
    path: str
    ws: workspace.Workspace


def save(req: SaveRequest):
    path = config.DATA_PATH / req.path
    assert path.is_relative_to(config.DATA_PATH)
    workspace.save(req.ws, path)


@app.post("/api/save")
async def save_and_execute(req: SaveRequest):
    save(req)
    await workspace.execute(req.ws)
    save(req)
    return req.ws


@app.post("/api/delete")
async def delete_workspace(req: dict):
    json_path: pathlib.Path = config.DATA_PATH / req["path"]
    crdt_path: pathlib.Path = config.CRDT_PATH / f"{req['path']}.crdt"
    assert json_path.is_relative_to(config.DATA_PATH)
    assert crdt_path.is_relative_to(config.CRDT_PATH)
    json_path.unlink()
    crdt_path.unlink()


@app.get("/api/load")
def load(path: str):
    path = config.DATA_PATH / path
    assert path.is_relative_to(config.DATA_PATH)
    if not path.exists():
        return workspace.Workspace()
    return workspace.load(path)


@dataclasses.dataclass(order=True)
class DirectoryEntry:
    name: str
    type: str


@app.get("/api/dir/list")
def list_dir(path: str):
    path = config.DATA_PATH / path
    assert path.is_relative_to(config.DATA_PATH)
    return sorted(
        [
            DirectoryEntry(
                p.relative_to(config.DATA_PATH),
                "directory" if p.is_dir() else "workspace",
            )
            for p in path.iterdir()
        ]
    )


@app.post("/api/dir/mkdir")
def make_dir(req: dict):
    path = config.DATA_PATH / req["path"]
    assert path.is_relative_to(config.DATA_PATH)
    assert not path.exists(), f"{path} already exists"
    path.mkdir()


@app.post("/api/dir/delete")
def delete_dir(req: dict):
    path: pathlib.Path = config.DATA_PATH / req["path"]
    assert all([path.is_relative_to(config.DATA_PATH), path.exists(), path.is_dir()])
    shutil.rmtree(path)


@app.get("/api/service/{module_path:path}")
async def service_get(req: fastapi.Request, module_path: str):
    """Executors can provide extra HTTP APIs through the /api/service endpoint."""
    module = lynxkite_plugins[module_path.split("/")[0]]
    return await module.api_service_get(req)


@app.post("/api/service/{module_path:path}")
async def service_post(req: fastapi.Request, module_path: str):
    """Executors can provide extra HTTP APIs through the /api/service endpoint."""
    module = lynxkite_plugins[module_path.split("/")[0]]
    return await module.api_service_post(req)


class SPAStaticFiles(StaticFiles):
    """Route everything to index.html. https://stackoverflow.com/a/73552966/3318517"""

    async def get_response(self, path: str, scope):
        try:
            return await super().get_response(path, scope)
        except (
            fastapi.HTTPException,
            starlette.exceptions.HTTPException,
        ) as ex:
            if ex.status_code == 404:
                return await super().get_response(".", scope)
            else:
                raise ex


static_dir = SPAStaticFiles(packages=[("lynxkite_app", "web_assets")], html=True)
app.mount("/", static_dir, name="web_assets")