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.
Files changed (25) hide show
  1. {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/PKG-INFO +20 -2
  2. {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/README.md +19 -1
  3. {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/main.py +20 -1
  4. {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/routers/admin.py +14 -0
  5. {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/ui/templates/model_detail.html +4 -3
  6. {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/ui/templates/model_form.html +5 -2
  7. {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/ui/templates/model_list.html +13 -0
  8. {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_lite_admin.egg-info/PKG-INFO +20 -2
  9. {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/pyproject.toml +1 -1
  10. {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/LICENSE +0 -0
  11. {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/__init__.py +0 -0
  12. {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/core/config.py +0 -0
  13. {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/core/crud.py +0 -0
  14. {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/core/registry.py +0 -0
  15. {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/core/schema.py +0 -0
  16. {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/dependencies/db.py +0 -0
  17. {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/integrations/sqlalchemy.py +0 -0
  18. {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/ui/templates/dashboard.html +0 -0
  19. {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/ui/templates/layout.html +0 -0
  20. {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_admin_lite/ui/views.py +0 -0
  21. {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_lite_admin.egg-info/SOURCES.txt +0 -0
  22. {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_lite_admin.egg-info/dependency_links.txt +0 -0
  23. {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_lite_admin.egg-info/requires.txt +0 -0
  24. {fastapi_lite_admin-0.1.7 → fastapi_lite_admin-0.1.8}/fastapi_lite_admin.egg-info/top_level.txt +0 -0
  25. {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.7
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. Enabling File Upload in Models
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. Enabling File Upload in Models
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="${val}" alt="${key}" style="max-width: 300px; max-height: 300px; object-fit: contain; border-radius: 8px; border: 1px solid var(--border);">
173
- <a href="${val}" target="_blank" class="file-preview-link" style="color: var(--primary); text-decoration: none; font-size: 0.85rem; font-weight: 500;">
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="${val}" 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
+ <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="${val}" target="_blank" class="file-preview-link">View file</a>
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="${fileUrl}" target="_blank" class="file-preview-link">View file</a>
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.7
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. Enabling File Upload in Models
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fastapi-lite-admin"
7
- version = "0.1.7"
7
+ version = "0.1.8"
8
8
  description = "A lightweight, pluggable admin panel for FastAPI"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"