fastapi-lite-admin 0.1.7__tar.gz → 0.1.8__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.
- {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/PKG-INFO +20 -2
- {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/README.md +19 -1
- {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/main.py +20 -1
- {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/routers/admin.py +14 -0
- {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/ui/templates/model_detail.html +4 -3
- {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/ui/templates/model_form.html +5 -2
- {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/ui/templates/model_list.html +13 -0
- {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_lite_admin.egg-info/PKG-INFO +20 -2
- {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/pyproject.toml +1 -1
- {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/LICENSE +0 -0
- {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/__init__.py +0 -0
- {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/core/config.py +0 -0
- {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/core/crud.py +0 -0
- {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/core/registry.py +0 -0
- {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/core/schema.py +0 -0
- {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/dependencies/db.py +0 -0
- {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/integrations/sqlalchemy.py +0 -0
- {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/ui/templates/dashboard.html +0 -0
- {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/ui/templates/layout.html +0 -0
- {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/ui/views.py +0 -0
- {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_lite_admin.egg-info/SOURCES.txt +0 -0
- {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_lite_admin.egg-info/dependency_links.txt +0 -0
- {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_lite_admin.egg-info/requires.txt +0 -0
- {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_lite_admin.egg-info/top_level.txt +0 -0
- {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-lite-admin
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Summary: A lightweight, pluggable admin panel for FastAPI
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/rishiqwerty/admin-panel-fast-api
|
|
@@ -145,6 +145,7 @@ The `Admin` class constructor supports the following parameters for customizatio
|
|
|
145
145
|
| **`upload_dir`** | `str` | `"uploads"` | Local directory path where uploaded files will be stored. |
|
|
146
146
|
| **`upload_url`** | `str` | `"/uploads"` | URL prefix used to serve uploaded files statically. |
|
|
147
147
|
| **`upload_handler`** | `Callable` | `None` | Optional custom upload handler callback for buckets (S3, GCS, Azure). |
|
|
148
|
+
| **`url_resolver`** | `Callable` | `None` | Optional custom URL resolver callback to resolve database keys to presigned URLs (S3, GCS). |
|
|
148
149
|
|
|
149
150
|
---
|
|
150
151
|
|
|
@@ -240,7 +241,24 @@ admin = Admin(
|
|
|
240
241
|
)
|
|
241
242
|
```
|
|
242
243
|
|
|
243
|
-
### 3.
|
|
244
|
+
### 3. Storage URL Resolution (Presigned URLs)
|
|
245
|
+
If the database stores relative paths/keys (e.g. `generated/uuid.jpg` in GCS or S3) rather than full absolute URLs, browser requests will fail. You can provide a custom `url_resolver` callback:
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
async def get_s3_presigned_url(path: str) -> str:
|
|
249
|
+
# 1. Generate temporary presigned GET URL for GCS/S3 key
|
|
250
|
+
# 2. Return URL
|
|
251
|
+
return s3_client.generate_presigned_url('get_object', Params={'Bucket': 'my-bucket', 'Key': path})
|
|
252
|
+
|
|
253
|
+
admin = Admin(
|
|
254
|
+
title="Cloud Admin",
|
|
255
|
+
url_resolver=get_s3_presigned_url
|
|
256
|
+
)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
The admin panel routes all file rendering and download links through the `/admin/api/media?path=...` redirect proxy, which executes `url_resolver` to redirect the browser to the temporary accessible URL safely, keeping your database values clean.
|
|
260
|
+
|
|
261
|
+
### 4. Enabling File Upload in Models
|
|
244
262
|
Pass the `file_fields` parameter when registering your model:
|
|
245
263
|
|
|
246
264
|
```python
|
|
@@ -124,6 +124,7 @@ The `Admin` class constructor supports the following parameters for customizatio
|
|
|
124
124
|
| **`upload_dir`** | `str` | `"uploads"` | Local directory path where uploaded files will be stored. |
|
|
125
125
|
| **`upload_url`** | `str` | `"/uploads"` | URL prefix used to serve uploaded files statically. |
|
|
126
126
|
| **`upload_handler`** | `Callable` | `None` | Optional custom upload handler callback for buckets (S3, GCS, Azure). |
|
|
127
|
+
| **`url_resolver`** | `Callable` | `None` | Optional custom URL resolver callback to resolve database keys to presigned URLs (S3, GCS). |
|
|
127
128
|
|
|
128
129
|
---
|
|
129
130
|
|
|
@@ -219,7 +220,24 @@ admin = Admin(
|
|
|
219
220
|
)
|
|
220
221
|
```
|
|
221
222
|
|
|
222
|
-
### 3.
|
|
223
|
+
### 3. Storage URL Resolution (Presigned URLs)
|
|
224
|
+
If the database stores relative paths/keys (e.g. `generated/uuid.jpg` in GCS or S3) rather than full absolute URLs, browser requests will fail. You can provide a custom `url_resolver` callback:
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
async def get_s3_presigned_url(path: str) -> str:
|
|
228
|
+
# 1. Generate temporary presigned GET URL for GCS/S3 key
|
|
229
|
+
# 2. Return URL
|
|
230
|
+
return s3_client.generate_presigned_url('get_object', Params={'Bucket': 'my-bucket', 'Key': path})
|
|
231
|
+
|
|
232
|
+
admin = Admin(
|
|
233
|
+
title="Cloud Admin",
|
|
234
|
+
url_resolver=get_s3_presigned_url
|
|
235
|
+
)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The admin panel routes all file rendering and download links through the `/admin/api/media?path=...` redirect proxy, which executes `url_resolver` to redirect the browser to the temporary accessible URL safely, keeping your database values clean.
|
|
239
|
+
|
|
240
|
+
### 4. Enabling File Upload in Models
|
|
223
241
|
Pass the `file_fields` parameter when registering your model:
|
|
224
242
|
|
|
225
243
|
```python
|
|
@@ -19,7 +19,8 @@ class Admin:
|
|
|
19
19
|
logs_config: Optional[Dict[str, Any]] = None,
|
|
20
20
|
upload_dir: str = "uploads",
|
|
21
21
|
upload_url: str = "/uploads",
|
|
22
|
-
upload_handler: Optional[Callable[[Any], Any]] = None
|
|
22
|
+
upload_handler: Optional[Callable[[Any], Any]] = None,
|
|
23
|
+
url_resolver: Optional[Callable[[str], Any]] = None
|
|
23
24
|
):
|
|
24
25
|
self.title = title
|
|
25
26
|
self.base_url = base_url
|
|
@@ -40,6 +41,9 @@ class Admin:
|
|
|
40
41
|
# Auth & Permissions
|
|
41
42
|
self.auth_dependency = auth_dependency
|
|
42
43
|
self.permission_checker = permission_checker or self.default_permission
|
|
44
|
+
|
|
45
|
+
# Media resolver
|
|
46
|
+
self.url_resolver = url_resolver or self.default_url_resolver
|
|
43
47
|
|
|
44
48
|
# Print warnings if not configured
|
|
45
49
|
if not auth_dependency:
|
|
@@ -51,6 +55,21 @@ class Admin:
|
|
|
51
55
|
"""Default permission checker that allows everything."""
|
|
52
56
|
return True
|
|
53
57
|
|
|
58
|
+
def default_url_resolver(self, path: str) -> str:
|
|
59
|
+
"""Default URL resolver that resolves relative paths to the local static uploads URL."""
|
|
60
|
+
if not path:
|
|
61
|
+
return ""
|
|
62
|
+
if path.startswith("http://") or path.startswith("https://") or path.startswith("data:"):
|
|
63
|
+
return path
|
|
64
|
+
|
|
65
|
+
clean_path = path.lstrip("/")
|
|
66
|
+
clean_prefix = self.upload_url.strip("/")
|
|
67
|
+
|
|
68
|
+
if clean_path.startswith(clean_prefix):
|
|
69
|
+
return f"/{clean_path}"
|
|
70
|
+
|
|
71
|
+
return f"{self.upload_url}/{clean_path}"
|
|
72
|
+
|
|
54
73
|
def register(
|
|
55
74
|
self,
|
|
56
75
|
model: Type[Any],
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from typing import Any, Dict, List
|
|
2
2
|
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File
|
|
3
|
+
from fastapi.responses import RedirectResponse
|
|
3
4
|
import shutil
|
|
4
5
|
import uuid
|
|
5
6
|
import os
|
|
@@ -42,6 +43,19 @@ def create_admin_router(admin: Any) -> APIRouter:
|
|
|
42
43
|
|
|
43
44
|
return {"url": f"{admin.upload_url}/{filename}"}
|
|
44
45
|
|
|
46
|
+
@router.get("/media", name="admin_resolve_media")
|
|
47
|
+
async def resolve_media(path: str = Query(...)):
|
|
48
|
+
if not path:
|
|
49
|
+
raise HTTPException(status_code=400, detail="Path parameter is required")
|
|
50
|
+
try:
|
|
51
|
+
if asyncio.iscoroutinefunction(admin.url_resolver):
|
|
52
|
+
resolved_url = await admin.url_resolver(path)
|
|
53
|
+
else:
|
|
54
|
+
resolved_url = admin.url_resolver(path)
|
|
55
|
+
return RedirectResponse(url=resolved_url)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
raise HTTPException(status_code=500, detail=f"Failed to resolve media URL: {str(e)}")
|
|
58
|
+
|
|
45
59
|
@router.get("/models")
|
|
46
60
|
async def list_models():
|
|
47
61
|
return list(admin.registry.get_models().keys())
|
|
@@ -165,19 +165,20 @@
|
|
|
165
165
|
if (!val) {
|
|
166
166
|
displayVal = '<span style="color: var(--text-muted);">None</span>';
|
|
167
167
|
} else {
|
|
168
|
+
const displayUrl = val.startsWith('http://') || val.startsWith('https://') ? val : `${apiBaseUrl}/media?path=${encodeURIComponent(val)}`;
|
|
168
169
|
const isImg = val.match(/\.(jpeg|jpg|gif|png|webp)$/i);
|
|
169
170
|
if (isImg) {
|
|
170
171
|
displayVal = `
|
|
171
172
|
<div style="display: flex; flex-direction: column; gap: 8px; margin-top: 8px;">
|
|
172
|
-
<img src="${
|
|
173
|
-
<a href="${
|
|
173
|
+
<img src="${displayUrl}" alt="${key}" style="max-width: 300px; max-height: 300px; object-fit: contain; border-radius: 8px; border: 1px solid var(--border);">
|
|
174
|
+
<a href="${displayUrl}" target="_blank" class="file-preview-link" style="color: var(--primary); text-decoration: none; font-size: 0.85rem; font-weight: 500;">
|
|
174
175
|
<i class="fas fa-external-link-alt"></i> View full size
|
|
175
176
|
</a>
|
|
176
177
|
</div>
|
|
177
178
|
`;
|
|
178
179
|
} else {
|
|
179
180
|
displayVal = `
|
|
180
|
-
<a href="${
|
|
181
|
+
<a href="${displayUrl}" target="_blank" class="file-preview-link" style="display: inline-flex; align-items: center; gap: 8px; color: var(--primary); text-decoration: none; font-weight: 500; margin-top: 4px;">
|
|
181
182
|
<i class="fas fa-file-alt" style="font-size: 1.25rem;"></i> View File (${val.split('/').pop()})
|
|
182
183
|
</a>
|
|
183
184
|
`;
|
|
@@ -451,12 +451,13 @@
|
|
|
451
451
|
|
|
452
452
|
const isFile = fileFields.includes(name);
|
|
453
453
|
if (isFile) {
|
|
454
|
+
const displayUrl = val ? (val.startsWith('http://') || val.startsWith('https://') ? val : `${apiBaseUrl}/media?path=${encodeURIComponent(val)}`) : '';
|
|
454
455
|
const previewHtml = val ? `
|
|
455
456
|
<div class="file-preview-container" id="preview-${name}">
|
|
456
457
|
<div class="file-preview-icon"><i class="fas fa-file-alt"></i></div>
|
|
457
458
|
<div class="file-preview-info">
|
|
458
459
|
<span class="file-preview-name">${val.split('/').pop()}</span>
|
|
459
|
-
<a href="${
|
|
460
|
+
<a href="${displayUrl}" target="_blank" class="file-preview-link">View file</a>
|
|
460
461
|
</div>
|
|
461
462
|
<button type="button" class="btn-remove-file" onclick="clearFile('${name}')">
|
|
462
463
|
<i class="fas fa-times"></i>
|
|
@@ -591,13 +592,15 @@
|
|
|
591
592
|
// Set value in hidden input
|
|
592
593
|
hiddenInput.value = fileUrl;
|
|
593
594
|
|
|
595
|
+
const displayUrl = fileUrl.startsWith('http://') || fileUrl.startsWith('https://') ? fileUrl : `${apiBaseUrl}/media?path=${encodeURIComponent(fileUrl)}`;
|
|
596
|
+
|
|
594
597
|
// Update preview
|
|
595
598
|
previewWrapper.innerHTML = `
|
|
596
599
|
<div class="file-preview-container" id="preview-${fieldName}">
|
|
597
600
|
<div class="file-preview-icon"><i class="fas fa-file-alt"></i></div>
|
|
598
601
|
<div class="file-preview-info">
|
|
599
602
|
<span class="file-preview-name">${file.name}</span>
|
|
600
|
-
<a href="${
|
|
603
|
+
<a href="${displayUrl}" target="_blank" class="file-preview-link">View file</a>
|
|
601
604
|
</div>
|
|
602
605
|
<button type="button" class="btn-remove-file" onclick="clearFile('${fieldName}')">
|
|
603
606
|
<i class="fas fa-times"></i>
|
|
@@ -303,6 +303,7 @@
|
|
|
303
303
|
const schema = await schemaResponse.json();
|
|
304
304
|
const modelSchema = schema.models.find(m => m.name === modelName);
|
|
305
305
|
const listDisplay = modelSchema ? modelSchema.list_display : null;
|
|
306
|
+
const fileFields = modelSchema?.config?.file_fields || [];
|
|
306
307
|
|
|
307
308
|
// 2. Fetch Data
|
|
308
309
|
const skip = (currentPage - 1) * pageSize;
|
|
@@ -385,6 +386,18 @@
|
|
|
385
386
|
`;
|
|
386
387
|
}
|
|
387
388
|
|
|
389
|
+
if (fileFields.includes(col)) {
|
|
390
|
+
if (!val) return '<td>--</td>';
|
|
391
|
+
const displayUrl = val.startsWith('http://') || val.startsWith('https://') ? val : `${apiBaseUrl}/media?path=${encodeURIComponent(val)}`;
|
|
392
|
+
return `
|
|
393
|
+
<td>
|
|
394
|
+
<a href="${displayUrl}" target="_blank" class="file-preview-link" style="color: var(--primary); text-decoration: none; font-weight: 500; display: inline-flex; align-items: center; gap: 6px;">
|
|
395
|
+
<i class="fas fa-external-link-alt"></i> View File
|
|
396
|
+
</a>
|
|
397
|
+
</td>
|
|
398
|
+
`;
|
|
399
|
+
}
|
|
400
|
+
|
|
388
401
|
return `<td>${val}</td>`;
|
|
389
402
|
}).join('')}
|
|
390
403
|
<td>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-lite-admin
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Summary: A lightweight, pluggable admin panel for FastAPI
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/rishiqwerty/admin-panel-fast-api
|
|
@@ -145,6 +145,7 @@ The `Admin` class constructor supports the following parameters for customizatio
|
|
|
145
145
|
| **`upload_dir`** | `str` | `"uploads"` | Local directory path where uploaded files will be stored. |
|
|
146
146
|
| **`upload_url`** | `str` | `"/uploads"` | URL prefix used to serve uploaded files statically. |
|
|
147
147
|
| **`upload_handler`** | `Callable` | `None` | Optional custom upload handler callback for buckets (S3, GCS, Azure). |
|
|
148
|
+
| **`url_resolver`** | `Callable` | `None` | Optional custom URL resolver callback to resolve database keys to presigned URLs (S3, GCS). |
|
|
148
149
|
|
|
149
150
|
---
|
|
150
151
|
|
|
@@ -240,7 +241,24 @@ admin = Admin(
|
|
|
240
241
|
)
|
|
241
242
|
```
|
|
242
243
|
|
|
243
|
-
### 3.
|
|
244
|
+
### 3. Storage URL Resolution (Presigned URLs)
|
|
245
|
+
If the database stores relative paths/keys (e.g. `generated/uuid.jpg` in GCS or S3) rather than full absolute URLs, browser requests will fail. You can provide a custom `url_resolver` callback:
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
async def get_s3_presigned_url(path: str) -> str:
|
|
249
|
+
# 1. Generate temporary presigned GET URL for GCS/S3 key
|
|
250
|
+
# 2. Return URL
|
|
251
|
+
return s3_client.generate_presigned_url('get_object', Params={'Bucket': 'my-bucket', 'Key': path})
|
|
252
|
+
|
|
253
|
+
admin = Admin(
|
|
254
|
+
title="Cloud Admin",
|
|
255
|
+
url_resolver=get_s3_presigned_url
|
|
256
|
+
)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
The admin panel routes all file rendering and download links through the `/admin/api/media?path=...` redirect proxy, which executes `url_resolver` to redirect the browser to the temporary accessible URL safely, keeping your database values clean.
|
|
260
|
+
|
|
261
|
+
### 4. Enabling File Upload in Models
|
|
244
262
|
Pass the `file_fields` parameter when registering your model:
|
|
245
263
|
|
|
246
264
|
```python
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/integrations/sqlalchemy.py
RENAMED
|
File without changes
|
{fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/ui/templates/dashboard.html
RENAMED
|
File without changes
|
{fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/ui/templates/layout.html
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_lite_admin.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_lite_admin.egg-info/requires.txt
RENAMED
|
File without changes
|
{fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_lite_admin.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|