Spaces:
Running
Running
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")
|