File size: 5,322 Bytes
be37871
 
938d45b
d328dbf
ca01fa3
b34d742
b8b73b2
121923b
fdfb0b2
e0801c9
fdfb0b2
359ff3d
 
cac41bd
121923b
0daac8b
 
 
be37871
 
 
 
0daac8b
be37871
0daac8b
 
 
 
21e70fe
ca01fa3
a180fd2
 
e0801c9
ca01fa3
57ea732
ca01fa3
21e70fe
 
1270bff
ca01fa3
 
bc2b550
05acf81
bc2b550
05acf81
57ea732
cac41bd
 
 
05acf81
cac41bd
5376af4
01575eb
05acf81
57ea732
ca01fa3
c9fc495
05acf81
7f31eb6
 
 
05acf81
 
57ea732
938d45b
 
cac41bd
 
5376af4
938d45b
 
 
 
05acf81
 
cac41bd
5376af4
05acf81
bc2b550
01575eb
b8b73b2
57ea732
d328dbf
b8b73b2
 
 
57ea732
13ae1a3
 
 
 
 
 
 
 
 
b8b73b2
 
cac41bd
5376af4
57ea732
 
 
cac41bd
13ae1a3
57ea732
 
cac41bd
4df7999
5266fe9
57ea732
 
05acf81
 
 
cac41bd
5376af4
b4c0638
05acf81
b34d742
57ea732
938d45b
 
cac41bd
5376af4
 
 
938d45b
 
 
9f88800
 
57ea732
0daac8b
9f88800
 
 
 
 
 
0daac8b
9f88800
fdfb0b2
 
893e71a
 
 
 
 
cac41bd
5376af4
893e71a
 
 
 
 
825e272
 
 
 
 
 
 
 
fdfb0b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
be37871
fdfb0b2
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
"""The FastAPI server for serving the LynxKite application."""

import shutil
import pydantic
import fastapi
import importlib
import pathlib
import pkgutil
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.gzip import GZipMiddleware
import starlette
from lynxkite.core import ops
from lynxkite.core import workspace
from . import crdt


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()
ops.save_catalogs("plugins loaded")

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


@app.get("/api/catalog")
def get_catalog(workspace: str):
    ops.load_user_scripts(workspace)
    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


data_path = pathlib.Path()


def save(req: SaveRequest):
    path = data_path / req.path
    assert path.is_relative_to(data_path), f"Path '{path}' is invalid"
    req.ws.save(path)


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


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


@app.get("/api/load")
def load(path: str):
    path = data_path / path
    assert path.is_relative_to(data_path), f"Path '{path}' is invalid"
    if not path.exists():
        return workspace.Workspace()
    return workspace.Workspace.load(path)


class DirectoryEntry(pydantic.BaseModel):
    name: str
    type: str


def _get_path_type(path: pathlib.Path) -> str:
    if path.is_dir():
        return "directory"
    elif path.suffixes[-2:] == [".lynxkite", ".json"]:
        return "workspace"
    else:
        return "file"


@app.get("/api/dir/list")
def list_dir(path: str):
    path = data_path / path
    assert path.is_relative_to(data_path), f"Path '{path}' is invalid"
    return sorted(
        [
            DirectoryEntry(
                name=str(p.relative_to(data_path)),
                type=_get_path_type(p),
            )
            for p in path.iterdir()
            if not p.name.startswith(".")
        ],
        key=lambda x: (x.type != "directory", x.name.lower()),
    )


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


@app.post("/api/dir/delete")
def delete_dir(req: dict):
    path: pathlib.Path = data_path / req["path"]
    assert all([path.is_relative_to(data_path), path.exists(), path.is_dir()]), (
        f"Path '{path}' is invalid"
    )
    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)


@app.post("/api/upload")
async def upload(req: fastapi.Request):
    """Receives file uploads and stores them in DATA_PATH."""
    form = await req.form()
    for file in form.values():
        file_path = data_path / "uploads" / file.filename
        assert file_path.is_relative_to(data_path), f"Path '{file_path}' is invalid"
        with file_path.open("wb") as buffer:
            shutil.copyfileobj(file.file, buffer)
    return {"status": "ok"}


@app.post("/api/execute_workspace")
async def execute_workspace(name: str):
    """Trigger and await the execution of a workspace."""
    room = await crdt.ws_websocket_server.get_room(name)
    ws_pyd = workspace.Workspace.model_validate(room.ws.to_py())
    await crdt.execute(name, room.ws, ws_pyd)


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")