oxyde-admin 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oxyde_admin/__init__.py +40 -0
- oxyde_admin/_version.py +1 -0
- oxyde_admin/adapters/__init__.py +0 -0
- oxyde_admin/adapters/_fastapi.py +185 -0
- oxyde_admin/adapters/_litestar.py +280 -0
- oxyde_admin/adapters/_sanic.py +261 -0
- oxyde_admin/adapters/base.py +100 -0
- oxyde_admin/api/__init__.py +0 -0
- oxyde_admin/api/routes.py +229 -0
- oxyde_admin/config.py +55 -0
- oxyde_admin/schema.py +66 -0
- oxyde_admin/site.py +420 -0
- oxyde_admin/static/assets/Dashboard-lLXvWYMG.js +1 -0
- oxyde_admin/static/assets/Login-CW4VUq6N.css +1 -0
- oxyde_admin/static/assets/Login-C_CHPOS1.js +101 -0
- oxyde_admin/static/assets/ModelDetail-CytWpk63.js +389 -0
- oxyde_admin/static/assets/ModelList-BrniEYli.js +1246 -0
- oxyde_admin/static/assets/index-BoOIem2p.css +1 -0
- oxyde_admin/static/assets/index-Cy4fw_D3.js +806 -0
- oxyde_admin/static/assets/index-DJs38nb7.js +1348 -0
- oxyde_admin/static/assets/index-Dyk0SQwm.js +54 -0
- oxyde_admin/static/assets/index-WzCL-nzV.js +590 -0
- oxyde_admin/static/assets/primeicons-C6QP2o4f.woff2 +0 -0
- oxyde_admin/static/assets/primeicons-DMOk5skT.eot +0 -0
- oxyde_admin/static/assets/primeicons-Dr5RGzOO.svg +345 -0
- oxyde_admin/static/assets/primeicons-MpK4pl85.ttf +0 -0
- oxyde_admin/static/assets/primeicons-WjwUDZjB.woff +0 -0
- oxyde_admin/static/favicon.png +0 -0
- oxyde_admin/static/index.html +14 -0
- oxyde_admin-0.1.0.dist-info/METADATA +219 -0
- oxyde_admin-0.1.0.dist-info/RECORD +34 -0
- oxyde_admin-0.1.0.dist-info/WHEEL +5 -0
- oxyde_admin-0.1.0.dist-info/licenses/LICENSE +21 -0
- oxyde_admin-0.1.0.dist-info/top_level.txt +1 -0
oxyde_admin/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from oxyde_admin._version import __version__
|
|
2
|
+
from oxyde_admin.config import Preset, PrimaryColor, Surface
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _make_stub(name, package):
|
|
6
|
+
class _Stub:
|
|
7
|
+
def __init__(self, *args, **kwargs):
|
|
8
|
+
raise ImportError(
|
|
9
|
+
f"{name} requires '{package}'. Install it with: pip install {package}"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
_Stub.__name__ = _Stub.__qualname__ = name
|
|
13
|
+
return _Stub
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from oxyde_admin.adapters._fastapi import FastAPIAdmin
|
|
18
|
+
except ImportError:
|
|
19
|
+
FastAPIAdmin = _make_stub("FastAPIAdmin", "fastapi")
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
from oxyde_admin.adapters._litestar import LitestarAdmin
|
|
23
|
+
except ImportError:
|
|
24
|
+
LitestarAdmin = _make_stub("LitestarAdmin", "litestar")
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
from oxyde_admin.adapters._sanic import SanicAdmin
|
|
28
|
+
except ImportError:
|
|
29
|
+
SanicAdmin = _make_stub("SanicAdmin", "sanic")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"__version__",
|
|
34
|
+
"Preset",
|
|
35
|
+
"PrimaryColor",
|
|
36
|
+
"Surface",
|
|
37
|
+
"FastAPIAdmin",
|
|
38
|
+
"LitestarAdmin",
|
|
39
|
+
"SanicAdmin",
|
|
40
|
+
]
|
oxyde_admin/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
File without changes
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI, Query, Request
|
|
6
|
+
from fastapi.responses import (
|
|
7
|
+
JSONResponse,
|
|
8
|
+
HTMLResponse,
|
|
9
|
+
FileResponse,
|
|
10
|
+
StreamingResponse,
|
|
11
|
+
)
|
|
12
|
+
from fastapi.staticfiles import StaticFiles
|
|
13
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
14
|
+
|
|
15
|
+
from oxyde_admin.adapters.base import AbstractAdapter, STATIC_DIR
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FastAPIAdmin(AbstractAdapter):
|
|
19
|
+
"""FastAPI adapter for Oxyde Admin."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, prefix: str = "/admin", **kwargs) -> None:
|
|
22
|
+
super().__init__(**kwargs)
|
|
23
|
+
self.prefix = prefix
|
|
24
|
+
|
|
25
|
+
def _build_app(self) -> FastAPI:
|
|
26
|
+
app = FastAPI(title="Oxyde Admin", docs_url=None, redoc_url=None)
|
|
27
|
+
|
|
28
|
+
if self.auth_check is not None:
|
|
29
|
+
self._register_auth_middleware(app)
|
|
30
|
+
|
|
31
|
+
self._register_exception_handlers(app)
|
|
32
|
+
self._register_routes(app)
|
|
33
|
+
self._register_static(app)
|
|
34
|
+
|
|
35
|
+
return app
|
|
36
|
+
|
|
37
|
+
def _register_auth_middleware(self, app: FastAPI) -> None:
|
|
38
|
+
check = self.auth_check
|
|
39
|
+
|
|
40
|
+
async def auth_middleware(request: Request, call_next):
|
|
41
|
+
root = request.scope.get("root_path", "")
|
|
42
|
+
raw_path = request.scope.get("path", request.url.path)
|
|
43
|
+
path = (
|
|
44
|
+
raw_path[len(root) :]
|
|
45
|
+
if root and raw_path.startswith(root)
|
|
46
|
+
else raw_path
|
|
47
|
+
)
|
|
48
|
+
if not path.startswith("/api/") or path == "/api/config":
|
|
49
|
+
return await call_next(request)
|
|
50
|
+
if inspect.iscoroutinefunction(check):
|
|
51
|
+
allowed = await check(request)
|
|
52
|
+
else:
|
|
53
|
+
allowed = check(request)
|
|
54
|
+
if not allowed:
|
|
55
|
+
return JSONResponse({"detail": "Unauthorized"}, status_code=401)
|
|
56
|
+
return await call_next(request)
|
|
57
|
+
|
|
58
|
+
app.add_middleware(BaseHTTPMiddleware, dispatch=auth_middleware)
|
|
59
|
+
|
|
60
|
+
def _register_exception_handlers(self, app: FastAPI) -> None:
|
|
61
|
+
for exc_cls, (status_code, detail_fn) in self.EXCEPTION_MAP.items():
|
|
62
|
+
|
|
63
|
+
def _make_handler(_status=status_code, _fn=detail_fn):
|
|
64
|
+
async def handler(request: Request, exc) -> JSONResponse:
|
|
65
|
+
detail = _fn(exc)
|
|
66
|
+
return JSONResponse({"detail": detail}, status_code=_status)
|
|
67
|
+
|
|
68
|
+
return handler
|
|
69
|
+
|
|
70
|
+
app.add_exception_handler(exc_cls, _make_handler())
|
|
71
|
+
|
|
72
|
+
def _register_routes(self, app: FastAPI) -> None:
|
|
73
|
+
@app.get("/api/config")
|
|
74
|
+
async def admin_config() -> dict:
|
|
75
|
+
return self._build_config()
|
|
76
|
+
|
|
77
|
+
@app.get("/api/models")
|
|
78
|
+
async def models_list() -> list[dict]:
|
|
79
|
+
return self._build_models_list()
|
|
80
|
+
|
|
81
|
+
@app.get("/api/models/counts")
|
|
82
|
+
async def models_counts() -> dict[str, int]:
|
|
83
|
+
return await self._build_models_counts()
|
|
84
|
+
|
|
85
|
+
@app.get("/api/{model_name}/schema", response_model=None)
|
|
86
|
+
async def model_schema(model_name: str):
|
|
87
|
+
return await self._handle_schema(model_name)
|
|
88
|
+
|
|
89
|
+
@app.get("/api/{model_name}", response_model=None)
|
|
90
|
+
async def model_list(
|
|
91
|
+
request: Request,
|
|
92
|
+
model_name: str,
|
|
93
|
+
page: int = 1,
|
|
94
|
+
per_page: int = 25,
|
|
95
|
+
ordering: str | None = None,
|
|
96
|
+
search: str | None = None,
|
|
97
|
+
):
|
|
98
|
+
return await self._handle_list(
|
|
99
|
+
model_name,
|
|
100
|
+
request.query_params,
|
|
101
|
+
page,
|
|
102
|
+
per_page,
|
|
103
|
+
ordering,
|
|
104
|
+
search,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
@app.get("/api/{model_name}/options", response_model=None)
|
|
108
|
+
async def model_options(
|
|
109
|
+
model_name: str,
|
|
110
|
+
search: str | None = None,
|
|
111
|
+
limit: int = 25,
|
|
112
|
+
include: str | None = None,
|
|
113
|
+
):
|
|
114
|
+
include_list = include.split(",") if include else None
|
|
115
|
+
return await self._handle_options(
|
|
116
|
+
model_name, search=search, limit=limit, include=include_list
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@app.get("/api/{model_name}/export", response_model=None)
|
|
120
|
+
async def model_export(
|
|
121
|
+
request: Request,
|
|
122
|
+
model_name: str,
|
|
123
|
+
fmt: str = Query("csv", alias="format"),
|
|
124
|
+
ordering: str | None = None,
|
|
125
|
+
search: str | None = None,
|
|
126
|
+
ids: str | None = None,
|
|
127
|
+
):
|
|
128
|
+
id_list = ids.split(",") if ids else None
|
|
129
|
+
stream, media_type, filename = await self._handle_export(
|
|
130
|
+
model_name,
|
|
131
|
+
request.query_params,
|
|
132
|
+
fmt,
|
|
133
|
+
ordering,
|
|
134
|
+
search,
|
|
135
|
+
ids=id_list,
|
|
136
|
+
)
|
|
137
|
+
return StreamingResponse(
|
|
138
|
+
stream,
|
|
139
|
+
media_type=media_type,
|
|
140
|
+
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
@app.get("/api/{model_name}/{pk}", response_model=None)
|
|
144
|
+
async def model_get(model_name: str, pk: str):
|
|
145
|
+
return await self._handle_get(model_name, pk)
|
|
146
|
+
|
|
147
|
+
@app.post("/api/{model_name}", status_code=201, response_model=None)
|
|
148
|
+
async def model_create(model_name: str, request: Request):
|
|
149
|
+
data = await request.json()
|
|
150
|
+
return await self._handle_create(model_name, data)
|
|
151
|
+
|
|
152
|
+
@app.put("/api/{model_name}/{pk}", response_model=None)
|
|
153
|
+
async def model_update(model_name: str, pk: str, request: Request):
|
|
154
|
+
data = await request.json()
|
|
155
|
+
return await self._handle_update(model_name, pk, data)
|
|
156
|
+
|
|
157
|
+
@app.delete("/api/{model_name}/{pk}", response_model=None)
|
|
158
|
+
async def model_delete(model_name: str, pk: str):
|
|
159
|
+
return await self._handle_delete(model_name, pk)
|
|
160
|
+
|
|
161
|
+
@app.post("/api/{model_name}/bulk-delete", response_model=None)
|
|
162
|
+
async def model_bulk_delete(model_name: str, request: Request):
|
|
163
|
+
body = await request.json()
|
|
164
|
+
return await self._handle_bulk_delete(model_name, body["ids"])
|
|
165
|
+
|
|
166
|
+
@app.post("/api/{model_name}/bulk-update", response_model=None)
|
|
167
|
+
async def model_bulk_update(model_name: str, request: Request):
|
|
168
|
+
body = await request.json()
|
|
169
|
+
return await self._handle_bulk_update(model_name, body["ids"], body["data"])
|
|
170
|
+
|
|
171
|
+
def _register_static(self, app: FastAPI) -> None:
|
|
172
|
+
assets_dir = STATIC_DIR / "assets"
|
|
173
|
+
if assets_dir.is_dir():
|
|
174
|
+
app.mount("/assets", StaticFiles(directory=assets_dir), name="static")
|
|
175
|
+
|
|
176
|
+
@app.get("/{path:path}", response_model=None)
|
|
177
|
+
async def catch_all(request: Request, path: str):
|
|
178
|
+
static_file = self._resolve_static_file(path)
|
|
179
|
+
if static_file is not None:
|
|
180
|
+
return FileResponse(static_file)
|
|
181
|
+
root = request.scope.get("root_path", "")
|
|
182
|
+
html = self._render_index_html(root)
|
|
183
|
+
if html is not None:
|
|
184
|
+
return HTMLResponse(html)
|
|
185
|
+
return JSONResponse({"detail": "Frontend not built"}, status_code=404)
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
|
|
5
|
+
from litestar import Litestar, Request, get, post, put, delete
|
|
6
|
+
from litestar.params import Parameter
|
|
7
|
+
from litestar.static_files import StaticFilesConfig
|
|
8
|
+
from litestar.openapi import OpenAPIConfig
|
|
9
|
+
from litestar.response import Response, File, Stream
|
|
10
|
+
from litestar.types import ASGIApp, Receive, Scope, Send
|
|
11
|
+
|
|
12
|
+
from oxyde_admin.adapters.base import AbstractAdapter, STATIC_DIR
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LitestarAdmin(AbstractAdapter):
|
|
16
|
+
"""Litestar adapter for Oxyde Admin."""
|
|
17
|
+
|
|
18
|
+
def _build_app(self) -> Litestar:
|
|
19
|
+
handlers = self._build_route_handlers()
|
|
20
|
+
|
|
21
|
+
middleware = []
|
|
22
|
+
if self.auth_check is not None:
|
|
23
|
+
middleware.append(self._create_auth_middleware())
|
|
24
|
+
|
|
25
|
+
exception_handlers = self._build_exception_handlers()
|
|
26
|
+
|
|
27
|
+
static_configs = []
|
|
28
|
+
assets_dir = STATIC_DIR / "assets"
|
|
29
|
+
if assets_dir.is_dir():
|
|
30
|
+
static_configs.append(
|
|
31
|
+
StaticFilesConfig(directories=[assets_dir], path="/assets")
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
return Litestar(
|
|
35
|
+
route_handlers=handlers,
|
|
36
|
+
middleware=middleware,
|
|
37
|
+
exception_handlers=exception_handlers,
|
|
38
|
+
static_files_config=static_configs,
|
|
39
|
+
openapi_config=OpenAPIConfig(
|
|
40
|
+
title="Oxyde Admin", version=self.version, path=None
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def _register_auth_middleware(self, app) -> None:
|
|
45
|
+
# Litestar builds middleware at app creation time,
|
|
46
|
+
# so this is handled via _create_auth_middleware() in _build_app().
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
def _register_exception_handlers(self, app) -> None:
|
|
50
|
+
# Litestar registers exception handlers at app creation time,
|
|
51
|
+
# so this is handled via _build_exception_handlers() in _build_app().
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
def _register_routes(self, app) -> None:
|
|
55
|
+
# Litestar registers route handlers at app creation time,
|
|
56
|
+
# so this is handled via _build_route_handlers() in _build_app().
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
def _register_static(self, app) -> None:
|
|
60
|
+
# Litestar configures static files at app creation time,
|
|
61
|
+
# so this is handled in _build_app().
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
# ------------------------------------------------------------------
|
|
65
|
+
# Auth middleware
|
|
66
|
+
# ------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
def _create_auth_middleware(self):
|
|
69
|
+
check = self.auth_check
|
|
70
|
+
|
|
71
|
+
def middleware_factory(app: ASGIApp) -> ASGIApp:
|
|
72
|
+
async def middleware(scope: Scope, receive: Receive, send: Send) -> None:
|
|
73
|
+
if scope["type"] != "http":
|
|
74
|
+
await app(scope, receive, send)
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
path = scope.get("path", "")
|
|
78
|
+
if not path.startswith("/api/") or path.rstrip("/") == "/api/config":
|
|
79
|
+
await app(scope, receive, send)
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
request = Request(scope)
|
|
83
|
+
if inspect.iscoroutinefunction(check):
|
|
84
|
+
allowed = await check(request)
|
|
85
|
+
else:
|
|
86
|
+
allowed = check(request)
|
|
87
|
+
|
|
88
|
+
if not allowed:
|
|
89
|
+
body = b'{"detail":"Unauthorized"}'
|
|
90
|
+
await send(
|
|
91
|
+
{
|
|
92
|
+
"type": "http.response.start",
|
|
93
|
+
"status": 401,
|
|
94
|
+
"headers": [
|
|
95
|
+
(b"content-type", b"application/json"),
|
|
96
|
+
(b"content-length", str(len(body)).encode()),
|
|
97
|
+
],
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
await send({"type": "http.response.body", "body": body})
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
await app(scope, receive, send)
|
|
104
|
+
|
|
105
|
+
return middleware
|
|
106
|
+
|
|
107
|
+
return middleware_factory
|
|
108
|
+
|
|
109
|
+
# ------------------------------------------------------------------
|
|
110
|
+
# Exception handlers
|
|
111
|
+
# ------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
def _build_exception_handlers(self) -> dict:
|
|
114
|
+
handlers = {}
|
|
115
|
+
for exc_cls, (status_code, detail_fn) in self.EXCEPTION_MAP.items():
|
|
116
|
+
|
|
117
|
+
def _make_handler(_status=status_code, _fn=detail_fn):
|
|
118
|
+
async def handler(request: Request, exc) -> Response:
|
|
119
|
+
detail = _fn(exc)
|
|
120
|
+
return Response(content={"detail": detail}, status_code=_status)
|
|
121
|
+
|
|
122
|
+
return handler
|
|
123
|
+
|
|
124
|
+
handlers[exc_cls] = _make_handler()
|
|
125
|
+
return handlers
|
|
126
|
+
|
|
127
|
+
# ------------------------------------------------------------------
|
|
128
|
+
# Route handlers
|
|
129
|
+
# ------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
def _build_route_handlers(self) -> list:
|
|
132
|
+
admin = self
|
|
133
|
+
|
|
134
|
+
@get("/api/config")
|
|
135
|
+
async def admin_config() -> dict:
|
|
136
|
+
return admin._build_config()
|
|
137
|
+
|
|
138
|
+
@get("/api/models")
|
|
139
|
+
async def models_list() -> list[dict]:
|
|
140
|
+
return admin._build_models_list()
|
|
141
|
+
|
|
142
|
+
@get("/api/models/counts")
|
|
143
|
+
async def models_counts() -> dict[str, int]:
|
|
144
|
+
return await admin._build_models_counts()
|
|
145
|
+
|
|
146
|
+
@get("/api/{model_name:str}/schema")
|
|
147
|
+
async def model_schema(model_name: str) -> dict:
|
|
148
|
+
return await admin._handle_schema(model_name)
|
|
149
|
+
|
|
150
|
+
@get("/api/{model_name:str}")
|
|
151
|
+
async def model_list(
|
|
152
|
+
request: Request,
|
|
153
|
+
model_name: str,
|
|
154
|
+
page: int = 1,
|
|
155
|
+
per_page: int = 25,
|
|
156
|
+
ordering: str | None = None,
|
|
157
|
+
search: str | None = None,
|
|
158
|
+
) -> dict:
|
|
159
|
+
return await admin._handle_list(
|
|
160
|
+
model_name,
|
|
161
|
+
request.query_params,
|
|
162
|
+
page,
|
|
163
|
+
per_page,
|
|
164
|
+
ordering,
|
|
165
|
+
search,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
@get("/api/{model_name:str}/options")
|
|
169
|
+
async def model_options(
|
|
170
|
+
model_name: str,
|
|
171
|
+
search: str | None = None,
|
|
172
|
+
limit: int = 25,
|
|
173
|
+
include: str | None = None,
|
|
174
|
+
) -> list:
|
|
175
|
+
include_list = include.split(",") if include else None
|
|
176
|
+
return await admin._handle_options(
|
|
177
|
+
model_name, search=search, limit=limit, include=include_list
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
@get("/api/{model_name:str}/export")
|
|
181
|
+
async def model_export(
|
|
182
|
+
request: Request,
|
|
183
|
+
model_name: str,
|
|
184
|
+
fmt: str = Parameter(default="csv", query="format"),
|
|
185
|
+
ordering: str | None = None,
|
|
186
|
+
search: str | None = None,
|
|
187
|
+
ids: str | None = None,
|
|
188
|
+
) -> Stream:
|
|
189
|
+
id_list = ids.split(",") if ids else None
|
|
190
|
+
stream, media_type, filename = await admin._handle_export(
|
|
191
|
+
model_name,
|
|
192
|
+
request.query_params,
|
|
193
|
+
fmt,
|
|
194
|
+
ordering,
|
|
195
|
+
search,
|
|
196
|
+
ids=id_list,
|
|
197
|
+
)
|
|
198
|
+
return Stream(
|
|
199
|
+
stream,
|
|
200
|
+
media_type=media_type,
|
|
201
|
+
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
@get("/api/{model_name:str}/{pk:str}")
|
|
205
|
+
async def model_get(model_name: str, pk: str) -> dict:
|
|
206
|
+
return await admin._handle_get(model_name, pk)
|
|
207
|
+
|
|
208
|
+
@post("/api/{model_name:str}", status_code=201)
|
|
209
|
+
async def model_create(model_name: str, data: dict) -> dict:
|
|
210
|
+
return await admin._handle_create(model_name, data)
|
|
211
|
+
|
|
212
|
+
@put("/api/{model_name:str}/{pk:str}")
|
|
213
|
+
async def model_update(model_name: str, pk: str, data: dict) -> dict:
|
|
214
|
+
return await admin._handle_update(model_name, pk, data)
|
|
215
|
+
|
|
216
|
+
@delete("/api/{model_name:str}/{pk:str}", status_code=200)
|
|
217
|
+
async def model_delete(model_name: str, pk: str) -> dict:
|
|
218
|
+
return await admin._handle_delete(model_name, pk)
|
|
219
|
+
|
|
220
|
+
@post("/api/{model_name:str}/bulk-delete")
|
|
221
|
+
async def model_bulk_delete(model_name: str, data: dict) -> dict:
|
|
222
|
+
return await admin._handle_bulk_delete(model_name, data["ids"])
|
|
223
|
+
|
|
224
|
+
@post("/api/{model_name:str}/bulk-update")
|
|
225
|
+
async def model_bulk_update(model_name: str, data: dict) -> dict:
|
|
226
|
+
return await admin._handle_bulk_update(
|
|
227
|
+
model_name, data["ids"], data["data"]
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# -- SPA catch-all -------------------------------------------------
|
|
231
|
+
|
|
232
|
+
def _get_mount_prefix(request: Request) -> str:
|
|
233
|
+
root = request.scope.get("root_path", "")
|
|
234
|
+
if root:
|
|
235
|
+
return root
|
|
236
|
+
raw = request.scope.get("raw_path", b"")
|
|
237
|
+
if isinstance(raw, bytes):
|
|
238
|
+
raw = raw.decode("latin-1")
|
|
239
|
+
inner_path = request.scope.get("path", "")
|
|
240
|
+
if raw.rstrip("/").endswith(inner_path.rstrip("/")):
|
|
241
|
+
prefix = raw[: len(raw.rstrip("/")) - len(inner_path.rstrip("/"))]
|
|
242
|
+
if prefix:
|
|
243
|
+
return prefix.rstrip("/")
|
|
244
|
+
return ""
|
|
245
|
+
|
|
246
|
+
def _serve_spa(request: Request, path: str = "") -> Response:
|
|
247
|
+
static_file = admin._resolve_static_file(path.lstrip("/"))
|
|
248
|
+
if static_file is not None:
|
|
249
|
+
return File(path=static_file)
|
|
250
|
+
prefix = _get_mount_prefix(request)
|
|
251
|
+
html = admin._render_index_html(prefix)
|
|
252
|
+
if html is not None:
|
|
253
|
+
return Response(content=html, media_type="text/html")
|
|
254
|
+
return Response(content={"detail": "Frontend not built"}, status_code=404)
|
|
255
|
+
|
|
256
|
+
@get("/", name="index")
|
|
257
|
+
async def index(request: Request) -> Response:
|
|
258
|
+
return _serve_spa(request)
|
|
259
|
+
|
|
260
|
+
@get("/{path:path}", name="catch_all")
|
|
261
|
+
async def catch_all(request: Request, path: str) -> Response:
|
|
262
|
+
return _serve_spa(request, path)
|
|
263
|
+
|
|
264
|
+
return [
|
|
265
|
+
admin_config,
|
|
266
|
+
models_list,
|
|
267
|
+
models_counts,
|
|
268
|
+
model_schema,
|
|
269
|
+
model_list,
|
|
270
|
+
model_options,
|
|
271
|
+
model_export,
|
|
272
|
+
model_get,
|
|
273
|
+
model_create,
|
|
274
|
+
model_update,
|
|
275
|
+
model_delete,
|
|
276
|
+
model_bulk_delete,
|
|
277
|
+
model_bulk_update,
|
|
278
|
+
index,
|
|
279
|
+
catch_all,
|
|
280
|
+
]
|