File size: 4,497 Bytes
fc43558
 
58bf1b1
d161f2f
ca01fa3
b34d742
b8b73b2
121923b
d7ccb5f
758e8e3
d7ccb5f
a083285
 
e4ff751
121923b
83b1026
 
 
fc43558
 
 
 
83b1026
fc43558
83b1026
 
 
 
ca01fa3
a180fd2
 
758e8e3
ca01fa3
d8f90d7
ca01fa3
 
e7fa7ee
 
d8f90d7
 
ca01fa3
 
bc2b550
05acf81
bc2b550
05acf81
d8f90d7
e4ff751
 
 
05acf81
e4ff751
 
bc2b550
05acf81
d8f90d7
ca01fa3
a0194e7
05acf81
a0194e7
05acf81
 
 
d8f90d7
58bf1b1
 
e4ff751
 
 
58bf1b1
 
 
 
05acf81
 
e4ff751
 
05acf81
bc2b550
 
b8b73b2
d8f90d7
d161f2f
b8b73b2
 
 
d8f90d7
b8b73b2
 
e4ff751
 
d8f90d7
 
 
e4ff751
4a2c869
d8f90d7
 
e4ff751
4a2c869
 
d8f90d7
 
05acf81
 
 
e4ff751
 
0724669
05acf81
b34d742
d8f90d7
58bf1b1
 
e4ff751
 
58bf1b1
 
 
03b7855
 
d8f90d7
83b1026
03b7855
 
 
 
 
 
83b1026
03b7855
d7ccb5f
 
289f6da
 
 
 
 
e4ff751
 
289f6da
 
 
 
 
d7ccb5f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fc43558
d7ccb5f
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
"""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()

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


@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


data_path = pathlib.Path()


def save(req: SaveRequest):
    path = data_path / req.path
    assert path.is_relative_to(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 = data_path / req["path"]
    crdt_path: pathlib.Path = data_path / ".crdt" / f"{req['path']}.crdt"
    assert json_path.is_relative_to(data_path)
    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)
    if not path.exists():
        return workspace.Workspace()
    return workspace.load(path)


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


@app.get("/api/dir/list")
def list_dir(path: str):
    path = data_path / path
    assert path.is_relative_to(data_path)
    return sorted(
        [
            DirectoryEntry(
                name=str(p.relative_to(data_path)),
                type="directory" if p.is_dir() else "workspace",
            )
            for p in path.iterdir()
            if not p.name.startswith(".")
        ],
        key=lambda x: x.name,
    )


@app.post("/api/dir/mkdir")
def make_dir(req: dict):
    path = data_path / req["path"]
    assert path.is_relative_to(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 = data_path / req["path"]
    assert all([path.is_relative_to(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)


@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), "Invalid file path"
        with file_path.open("wb") as buffer:
            shutil.copyfileobj(file.file, buffer)
    return {"status": "ok"}


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