oxyde-admin 0.1.0__tar.gz

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.
Files changed (46) hide show
  1. oxyde_admin-0.1.0/LICENSE +21 -0
  2. oxyde_admin-0.1.0/PKG-INFO +219 -0
  3. oxyde_admin-0.1.0/README.md +180 -0
  4. oxyde_admin-0.1.0/oxyde_admin/__init__.py +40 -0
  5. oxyde_admin-0.1.0/oxyde_admin/_version.py +1 -0
  6. oxyde_admin-0.1.0/oxyde_admin/adapters/__init__.py +0 -0
  7. oxyde_admin-0.1.0/oxyde_admin/adapters/_fastapi.py +185 -0
  8. oxyde_admin-0.1.0/oxyde_admin/adapters/_litestar.py +280 -0
  9. oxyde_admin-0.1.0/oxyde_admin/adapters/_sanic.py +261 -0
  10. oxyde_admin-0.1.0/oxyde_admin/adapters/base.py +100 -0
  11. oxyde_admin-0.1.0/oxyde_admin/api/__init__.py +0 -0
  12. oxyde_admin-0.1.0/oxyde_admin/api/routes.py +229 -0
  13. oxyde_admin-0.1.0/oxyde_admin/config.py +55 -0
  14. oxyde_admin-0.1.0/oxyde_admin/schema.py +66 -0
  15. oxyde_admin-0.1.0/oxyde_admin/site.py +420 -0
  16. oxyde_admin-0.1.0/oxyde_admin/static/assets/Dashboard-lLXvWYMG.js +1 -0
  17. oxyde_admin-0.1.0/oxyde_admin/static/assets/Login-CW4VUq6N.css +1 -0
  18. oxyde_admin-0.1.0/oxyde_admin/static/assets/Login-C_CHPOS1.js +101 -0
  19. oxyde_admin-0.1.0/oxyde_admin/static/assets/ModelDetail-CytWpk63.js +389 -0
  20. oxyde_admin-0.1.0/oxyde_admin/static/assets/ModelList-BrniEYli.js +1246 -0
  21. oxyde_admin-0.1.0/oxyde_admin/static/assets/index-BoOIem2p.css +1 -0
  22. oxyde_admin-0.1.0/oxyde_admin/static/assets/index-Cy4fw_D3.js +806 -0
  23. oxyde_admin-0.1.0/oxyde_admin/static/assets/index-DJs38nb7.js +1348 -0
  24. oxyde_admin-0.1.0/oxyde_admin/static/assets/index-Dyk0SQwm.js +54 -0
  25. oxyde_admin-0.1.0/oxyde_admin/static/assets/index-WzCL-nzV.js +590 -0
  26. oxyde_admin-0.1.0/oxyde_admin/static/assets/primeicons-C6QP2o4f.woff2 +0 -0
  27. oxyde_admin-0.1.0/oxyde_admin/static/assets/primeicons-DMOk5skT.eot +0 -0
  28. oxyde_admin-0.1.0/oxyde_admin/static/assets/primeicons-Dr5RGzOO.svg +345 -0
  29. oxyde_admin-0.1.0/oxyde_admin/static/assets/primeicons-MpK4pl85.ttf +0 -0
  30. oxyde_admin-0.1.0/oxyde_admin/static/assets/primeicons-WjwUDZjB.woff +0 -0
  31. oxyde_admin-0.1.0/oxyde_admin/static/favicon.png +0 -0
  32. oxyde_admin-0.1.0/oxyde_admin/static/index.html +14 -0
  33. oxyde_admin-0.1.0/oxyde_admin.egg-info/PKG-INFO +219 -0
  34. oxyde_admin-0.1.0/oxyde_admin.egg-info/SOURCES.txt +44 -0
  35. oxyde_admin-0.1.0/oxyde_admin.egg-info/dependency_links.txt +1 -0
  36. oxyde_admin-0.1.0/oxyde_admin.egg-info/requires.txt +10 -0
  37. oxyde_admin-0.1.0/oxyde_admin.egg-info/top_level.txt +1 -0
  38. oxyde_admin-0.1.0/pyproject.toml +60 -0
  39. oxyde_admin-0.1.0/setup.cfg +4 -0
  40. oxyde_admin-0.1.0/tests/test_admin_site.py +96 -0
  41. oxyde_admin-0.1.0/tests/test_base_adapter.py +98 -0
  42. oxyde_admin-0.1.0/tests/test_cast_pk.py +35 -0
  43. oxyde_admin-0.1.0/tests/test_config.py +95 -0
  44. oxyde_admin-0.1.0/tests/test_filters.py +87 -0
  45. oxyde_admin-0.1.0/tests/test_routes.py +55 -0
  46. oxyde_admin-0.1.0/tests/test_schema.py +76 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nikita Ryzhenkov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,219 @@
1
+ Metadata-Version: 2.4
2
+ Name: oxyde-admin
3
+ Version: 0.1.0
4
+ Summary: Admin interface for Oxyde ORM
5
+ Author-email: Nikita Ryzhenkov <nikita.ryzhenkoff@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/mr-fatalyst/oxyde-admin
8
+ Project-URL: Repository, https://github.com/mr-fatalyst/oxyde-admin
9
+ Project-URL: Issues, https://github.com/mr-fatalyst/oxyde-admin/issues
10
+ Keywords: admin,orm,oxyde,crud,dashboard,pydantic
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Framework :: AsyncIO
21
+ Classifier: Framework :: FastAPI
22
+ Classifier: Framework :: Pydantic :: 2
23
+ Classifier: Topic :: Internet :: WWW/HTTP
24
+ Classifier: Topic :: Software Development :: Libraries
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.10
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: oxyde>=0.4.0
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest; extra == "dev"
32
+ Requires-Dist: pytest-asyncio; extra == "dev"
33
+ Requires-Dist: fastapi; extra == "dev"
34
+ Requires-Dist: litestar; extra == "dev"
35
+ Requires-Dist: sanic; extra == "dev"
36
+ Requires-Dist: uvicorn; extra == "dev"
37
+ Requires-Dist: pyjwt; extra == "dev"
38
+ Dynamic: license-file
39
+
40
+ <p align="center">
41
+ <img src="logo.png" alt="Logo" width="300">
42
+ </p>
43
+
44
+ <p align="center"> <b>Oxyde Admin</b> Auto-generated admin panel for <a href="https://github.com/mr-fatalyst/oxyde">Oxyde ORM</a> with zero boilerplate. </p>
45
+
46
+ <p align="center">
47
+ <img src="https://img.shields.io/github/license/mr-fatalyst/oxyde-admin">
48
+ <img src="https://github.com/mr-fatalyst/oxyde-admin/actions/workflows/test.yml/badge.svg">
49
+ <img src="https://img.shields.io/pypi/v/oxyde-admin">
50
+ <img src="https://img.shields.io/pypi/pyversions/oxyde-admin">
51
+ <img src="https://static.pepy.tech/badge/oxyde-admin" alt="PyPI Downloads">
52
+ </p>
53
+
54
+ ---
55
+
56
+ ## Features
57
+
58
+ - **Automatic CRUD** -list, create, edit, delete from your Oxyde models
59
+ - **Search & filters** -text search across fields, column filters (FK, bool, string)
60
+ - **Foreign key handling** -select dropdowns with inline create dialog
61
+ - **Export** -CSV and JSON export with applied filters
62
+ - **Authentication** -pluggable auth via callback, JWT-ready
63
+ - **Theming** -3 presets, 17 colors, 8 surface palettes
64
+ - **Bulk operations** -bulk delete and update from list view
65
+ - **Multi-framework** -FastAPI, Litestar and Sanic adapters
66
+
67
+ ![oxyde-admin list view](images/screenshot-list.png)
68
+
69
+ ## Installation
70
+
71
+ ```bash
72
+ pip install oxyde-admin
73
+ ```
74
+
75
+ ## Quick start
76
+
77
+ ```python
78
+ from fastapi import FastAPI
79
+ from oxyde import db
80
+ from oxyde_admin import FastAPIAdmin
81
+
82
+ from models import User, Post, Comment
83
+
84
+ admin = FastAPIAdmin(title="My Admin")
85
+ admin.register(User, list_display=["name", "email"], search_fields=["name", "email"])
86
+ admin.register(Post, list_display=["title", "is_published"], list_filter=["is_published"])
87
+ admin.register(Comment)
88
+
89
+ app = FastAPI(lifespan=db.lifespan(default="sqlite:///app.db"))
90
+ app.mount("/admin", admin.app)
91
+ ```
92
+
93
+ Open `http://localhost:8000/admin/` and get a full CRUD interface for your models.
94
+
95
+ ![edit form](images/screenshot-detail.png)
96
+
97
+ ## Frameworks
98
+
99
+ ### FastAPI
100
+
101
+ ```python
102
+ from oxyde_admin import FastAPIAdmin
103
+
104
+ admin = FastAPIAdmin(title="My Admin")
105
+ # register models...
106
+ app.mount("/admin", admin.app)
107
+ ```
108
+
109
+ ### Litestar
110
+
111
+ ```python
112
+ from litestar import Litestar, asgi
113
+ from oxyde_admin import LitestarAdmin
114
+
115
+ admin = LitestarAdmin(title="My Admin")
116
+ # register models...
117
+
118
+ app = Litestar(
119
+ route_handlers=[
120
+ asgi(path="/admin", is_mount=True)(admin.app),
121
+ ],
122
+ )
123
+ ```
124
+
125
+ ### Sanic
126
+
127
+ ```python
128
+ from sanic import Sanic
129
+ from oxyde_admin import SanicAdmin
130
+
131
+ admin = SanicAdmin(title="My Admin")
132
+ # register models...
133
+
134
+ app = Sanic("MyApp")
135
+ admin.register_exception_handlers(app)
136
+ app.blueprint(admin.blueprint)
137
+ ```
138
+
139
+ ## Model registration
140
+
141
+ ```python
142
+ admin.register(
143
+ Post,
144
+ list_display=["title", "author_id", "is_published", "views"],
145
+ search_fields=["title", "content"],
146
+ list_filter=["author_id", "is_published"],
147
+ readonly_fields=["views"],
148
+ ordering=["-views"],
149
+ display_field="title",
150
+ column_labels={"author_id": "Author", "is_published": "Published"},
151
+ exportable=True,
152
+ group="Content",
153
+ icon="pi pi-file-edit",
154
+ )
155
+ ```
156
+
157
+ | Parameter | Description |
158
+ |---|---|
159
+ | `list_display` | Columns shown in the list view |
160
+ | `search_fields` | Fields included in text search |
161
+ | `list_filter` | Columns available as filters |
162
+ | `readonly_fields` | Fields disabled in the edit form |
163
+ | `ordering` | Default sort order (prefix `-` for descending) |
164
+ | `display_field` | Field used as label in FK dropdowns |
165
+ | `column_labels` | Custom column headers |
166
+ | `exportable` | Enable CSV/JSON export (default: `True`) |
167
+ | `group` | Sidebar group name |
168
+ | `icon` | Sidebar icon ([PrimeIcons](https://primevue.org/icons/)) |
169
+
170
+ You can also auto-register all models at once:
171
+
172
+ ```python
173
+ admin.register_all()
174
+
175
+ # or exclude specific models
176
+ admin.register_all(exclude={InternalModel})
177
+ ```
178
+
179
+ ## Theming
180
+
181
+ ```python
182
+ from oxyde_admin import Preset, PrimaryColor, Surface
183
+
184
+ admin = FastAPIAdmin(
185
+ title="My Admin",
186
+ preset=Preset.AURA,
187
+ primary_color=PrimaryColor.TEAL,
188
+ surface=Surface.ZINC,
189
+ )
190
+ ```
191
+
192
+ ![themes](images/screenshot-themes.png)
193
+
194
+ **Presets:** `AURA`, `LARA`, `NORA`
195
+
196
+ **Colors:** `NOIR` `EMERALD` `GREEN` `LIME` `ORANGE` `AMBER` `YELLOW` `TEAL` `CYAN` `SKY` `BLUE` `INDIGO` `VIOLET` `PURPLE` `FUCHSIA` `PINK` `ROSE`
197
+
198
+ **Surfaces:** `SLATE` `GRAY` `ZINC` `NEUTRAL` `STONE` `SOHO` `VIVA` `OCEAN`
199
+
200
+ ## Authentication
201
+
202
+ Pass an `auth_check` callback and a `login_url`:
203
+
204
+ ```python
205
+ async def check_admin(request) -> bool:
206
+ token = request.headers.get("Authorization", "").removeprefix("Bearer ")
207
+ return await verify_admin_token(token)
208
+
209
+ admin = FastAPIAdmin(
210
+ auth_check=check_admin,
211
+ login_url="/auth/login",
212
+ )
213
+ ```
214
+
215
+ The admin UI redirects unauthenticated users to `login_url`. Your login endpoint should return a JSON response with a token - the frontend stores it and sends as `Authorization: Bearer <token>` on every request.
216
+
217
+ ## License
218
+
219
+ This project is licensed under the terms of the MIT license.
@@ -0,0 +1,180 @@
1
+ <p align="center">
2
+ <img src="logo.png" alt="Logo" width="300">
3
+ </p>
4
+
5
+ <p align="center"> <b>Oxyde Admin</b> Auto-generated admin panel for <a href="https://github.com/mr-fatalyst/oxyde">Oxyde ORM</a> with zero boilerplate. </p>
6
+
7
+ <p align="center">
8
+ <img src="https://img.shields.io/github/license/mr-fatalyst/oxyde-admin">
9
+ <img src="https://github.com/mr-fatalyst/oxyde-admin/actions/workflows/test.yml/badge.svg">
10
+ <img src="https://img.shields.io/pypi/v/oxyde-admin">
11
+ <img src="https://img.shields.io/pypi/pyversions/oxyde-admin">
12
+ <img src="https://static.pepy.tech/badge/oxyde-admin" alt="PyPI Downloads">
13
+ </p>
14
+
15
+ ---
16
+
17
+ ## Features
18
+
19
+ - **Automatic CRUD** -list, create, edit, delete from your Oxyde models
20
+ - **Search & filters** -text search across fields, column filters (FK, bool, string)
21
+ - **Foreign key handling** -select dropdowns with inline create dialog
22
+ - **Export** -CSV and JSON export with applied filters
23
+ - **Authentication** -pluggable auth via callback, JWT-ready
24
+ - **Theming** -3 presets, 17 colors, 8 surface palettes
25
+ - **Bulk operations** -bulk delete and update from list view
26
+ - **Multi-framework** -FastAPI, Litestar and Sanic adapters
27
+
28
+ ![oxyde-admin list view](images/screenshot-list.png)
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install oxyde-admin
34
+ ```
35
+
36
+ ## Quick start
37
+
38
+ ```python
39
+ from fastapi import FastAPI
40
+ from oxyde import db
41
+ from oxyde_admin import FastAPIAdmin
42
+
43
+ from models import User, Post, Comment
44
+
45
+ admin = FastAPIAdmin(title="My Admin")
46
+ admin.register(User, list_display=["name", "email"], search_fields=["name", "email"])
47
+ admin.register(Post, list_display=["title", "is_published"], list_filter=["is_published"])
48
+ admin.register(Comment)
49
+
50
+ app = FastAPI(lifespan=db.lifespan(default="sqlite:///app.db"))
51
+ app.mount("/admin", admin.app)
52
+ ```
53
+
54
+ Open `http://localhost:8000/admin/` and get a full CRUD interface for your models.
55
+
56
+ ![edit form](images/screenshot-detail.png)
57
+
58
+ ## Frameworks
59
+
60
+ ### FastAPI
61
+
62
+ ```python
63
+ from oxyde_admin import FastAPIAdmin
64
+
65
+ admin = FastAPIAdmin(title="My Admin")
66
+ # register models...
67
+ app.mount("/admin", admin.app)
68
+ ```
69
+
70
+ ### Litestar
71
+
72
+ ```python
73
+ from litestar import Litestar, asgi
74
+ from oxyde_admin import LitestarAdmin
75
+
76
+ admin = LitestarAdmin(title="My Admin")
77
+ # register models...
78
+
79
+ app = Litestar(
80
+ route_handlers=[
81
+ asgi(path="/admin", is_mount=True)(admin.app),
82
+ ],
83
+ )
84
+ ```
85
+
86
+ ### Sanic
87
+
88
+ ```python
89
+ from sanic import Sanic
90
+ from oxyde_admin import SanicAdmin
91
+
92
+ admin = SanicAdmin(title="My Admin")
93
+ # register models...
94
+
95
+ app = Sanic("MyApp")
96
+ admin.register_exception_handlers(app)
97
+ app.blueprint(admin.blueprint)
98
+ ```
99
+
100
+ ## Model registration
101
+
102
+ ```python
103
+ admin.register(
104
+ Post,
105
+ list_display=["title", "author_id", "is_published", "views"],
106
+ search_fields=["title", "content"],
107
+ list_filter=["author_id", "is_published"],
108
+ readonly_fields=["views"],
109
+ ordering=["-views"],
110
+ display_field="title",
111
+ column_labels={"author_id": "Author", "is_published": "Published"},
112
+ exportable=True,
113
+ group="Content",
114
+ icon="pi pi-file-edit",
115
+ )
116
+ ```
117
+
118
+ | Parameter | Description |
119
+ |---|---|
120
+ | `list_display` | Columns shown in the list view |
121
+ | `search_fields` | Fields included in text search |
122
+ | `list_filter` | Columns available as filters |
123
+ | `readonly_fields` | Fields disabled in the edit form |
124
+ | `ordering` | Default sort order (prefix `-` for descending) |
125
+ | `display_field` | Field used as label in FK dropdowns |
126
+ | `column_labels` | Custom column headers |
127
+ | `exportable` | Enable CSV/JSON export (default: `True`) |
128
+ | `group` | Sidebar group name |
129
+ | `icon` | Sidebar icon ([PrimeIcons](https://primevue.org/icons/)) |
130
+
131
+ You can also auto-register all models at once:
132
+
133
+ ```python
134
+ admin.register_all()
135
+
136
+ # or exclude specific models
137
+ admin.register_all(exclude={InternalModel})
138
+ ```
139
+
140
+ ## Theming
141
+
142
+ ```python
143
+ from oxyde_admin import Preset, PrimaryColor, Surface
144
+
145
+ admin = FastAPIAdmin(
146
+ title="My Admin",
147
+ preset=Preset.AURA,
148
+ primary_color=PrimaryColor.TEAL,
149
+ surface=Surface.ZINC,
150
+ )
151
+ ```
152
+
153
+ ![themes](images/screenshot-themes.png)
154
+
155
+ **Presets:** `AURA`, `LARA`, `NORA`
156
+
157
+ **Colors:** `NOIR` `EMERALD` `GREEN` `LIME` `ORANGE` `AMBER` `YELLOW` `TEAL` `CYAN` `SKY` `BLUE` `INDIGO` `VIOLET` `PURPLE` `FUCHSIA` `PINK` `ROSE`
158
+
159
+ **Surfaces:** `SLATE` `GRAY` `ZINC` `NEUTRAL` `STONE` `SOHO` `VIVA` `OCEAN`
160
+
161
+ ## Authentication
162
+
163
+ Pass an `auth_check` callback and a `login_url`:
164
+
165
+ ```python
166
+ async def check_admin(request) -> bool:
167
+ token = request.headers.get("Authorization", "").removeprefix("Bearer ")
168
+ return await verify_admin_token(token)
169
+
170
+ admin = FastAPIAdmin(
171
+ auth_check=check_admin,
172
+ login_url="/auth/login",
173
+ )
174
+ ```
175
+
176
+ The admin UI redirects unauthenticated users to `login_url`. Your login endpoint should return a JSON response with a token - the frontend stores it and sends as `Authorization: Bearer <token>` on every request.
177
+
178
+ ## License
179
+
180
+ This project is licensed under the terms of the MIT license.
@@ -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
+ ]
@@ -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)