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.
- oxyde_admin-0.1.0/LICENSE +21 -0
- oxyde_admin-0.1.0/PKG-INFO +219 -0
- oxyde_admin-0.1.0/README.md +180 -0
- oxyde_admin-0.1.0/oxyde_admin/__init__.py +40 -0
- oxyde_admin-0.1.0/oxyde_admin/_version.py +1 -0
- oxyde_admin-0.1.0/oxyde_admin/adapters/__init__.py +0 -0
- oxyde_admin-0.1.0/oxyde_admin/adapters/_fastapi.py +185 -0
- oxyde_admin-0.1.0/oxyde_admin/adapters/_litestar.py +280 -0
- oxyde_admin-0.1.0/oxyde_admin/adapters/_sanic.py +261 -0
- oxyde_admin-0.1.0/oxyde_admin/adapters/base.py +100 -0
- oxyde_admin-0.1.0/oxyde_admin/api/__init__.py +0 -0
- oxyde_admin-0.1.0/oxyde_admin/api/routes.py +229 -0
- oxyde_admin-0.1.0/oxyde_admin/config.py +55 -0
- oxyde_admin-0.1.0/oxyde_admin/schema.py +66 -0
- oxyde_admin-0.1.0/oxyde_admin/site.py +420 -0
- oxyde_admin-0.1.0/oxyde_admin/static/assets/Dashboard-lLXvWYMG.js +1 -0
- oxyde_admin-0.1.0/oxyde_admin/static/assets/Login-CW4VUq6N.css +1 -0
- oxyde_admin-0.1.0/oxyde_admin/static/assets/Login-C_CHPOS1.js +101 -0
- oxyde_admin-0.1.0/oxyde_admin/static/assets/ModelDetail-CytWpk63.js +389 -0
- oxyde_admin-0.1.0/oxyde_admin/static/assets/ModelList-BrniEYli.js +1246 -0
- oxyde_admin-0.1.0/oxyde_admin/static/assets/index-BoOIem2p.css +1 -0
- oxyde_admin-0.1.0/oxyde_admin/static/assets/index-Cy4fw_D3.js +806 -0
- oxyde_admin-0.1.0/oxyde_admin/static/assets/index-DJs38nb7.js +1348 -0
- oxyde_admin-0.1.0/oxyde_admin/static/assets/index-Dyk0SQwm.js +54 -0
- oxyde_admin-0.1.0/oxyde_admin/static/assets/index-WzCL-nzV.js +590 -0
- oxyde_admin-0.1.0/oxyde_admin/static/assets/primeicons-C6QP2o4f.woff2 +0 -0
- oxyde_admin-0.1.0/oxyde_admin/static/assets/primeicons-DMOk5skT.eot +0 -0
- oxyde_admin-0.1.0/oxyde_admin/static/assets/primeicons-Dr5RGzOO.svg +345 -0
- oxyde_admin-0.1.0/oxyde_admin/static/assets/primeicons-MpK4pl85.ttf +0 -0
- oxyde_admin-0.1.0/oxyde_admin/static/assets/primeicons-WjwUDZjB.woff +0 -0
- oxyde_admin-0.1.0/oxyde_admin/static/favicon.png +0 -0
- oxyde_admin-0.1.0/oxyde_admin/static/index.html +14 -0
- oxyde_admin-0.1.0/oxyde_admin.egg-info/PKG-INFO +219 -0
- oxyde_admin-0.1.0/oxyde_admin.egg-info/SOURCES.txt +44 -0
- oxyde_admin-0.1.0/oxyde_admin.egg-info/dependency_links.txt +1 -0
- oxyde_admin-0.1.0/oxyde_admin.egg-info/requires.txt +10 -0
- oxyde_admin-0.1.0/oxyde_admin.egg-info/top_level.txt +1 -0
- oxyde_admin-0.1.0/pyproject.toml +60 -0
- oxyde_admin-0.1.0/setup.cfg +4 -0
- oxyde_admin-0.1.0/tests/test_admin_site.py +96 -0
- oxyde_admin-0.1.0/tests/test_base_adapter.py +98 -0
- oxyde_admin-0.1.0/tests/test_cast_pk.py +35 -0
- oxyde_admin-0.1.0/tests/test_config.py +95 -0
- oxyde_admin-0.1.0/tests/test_filters.py +87 -0
- oxyde_admin-0.1.0/tests/test_routes.py +55 -0
- 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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

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