fastadmin 0.4.8__tar.gz → 0.5.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 (53) hide show
  1. fastadmin-0.5.0/PKG-INFO +215 -0
  2. fastadmin-0.5.0/README.md +169 -0
  3. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/api/frameworks/django/app/api.py +3 -1
  4. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/api/frameworks/fastapi/api.py +4 -1
  5. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/api/frameworks/flask/api.py +4 -2
  6. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/api/service.py +13 -5
  7. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/models/base.py +45 -5
  8. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/models/decorators.py +1 -1
  9. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/models/helpers.py +1 -1
  10. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/models/orms/ponyorm.py +1 -1
  11. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/models/orms/sqlalchemy.py +1 -1
  12. fastadmin-0.5.0/fastadmin/static/index.min.js +653 -0
  13. fastadmin-0.5.0/pyproject.toml +153 -0
  14. fastadmin-0.4.8/PKG-INFO +0 -877
  15. fastadmin-0.4.8/README.md +0 -834
  16. fastadmin-0.4.8/fastadmin/static/index.min.js +0 -680
  17. fastadmin-0.4.8/pyproject.toml +0 -147
  18. /fastadmin-0.4.8/LICENSE → /fastadmin-0.5.0/LICENSE.md +0 -0
  19. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/__init__.py +0 -0
  20. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/api/__init__.py +0 -0
  21. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/api/exceptions.py +0 -0
  22. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/api/frameworks/__init__.py +0 -0
  23. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/api/frameworks/django/__init__.py +0 -0
  24. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/api/frameworks/django/app/__init__.py +0 -0
  25. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/api/frameworks/django/app/urls.py +0 -0
  26. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/api/frameworks/django/app/views.py +0 -0
  27. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/api/frameworks/fastapi/__init__.py +0 -0
  28. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/api/frameworks/fastapi/app.py +0 -0
  29. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/api/frameworks/fastapi/views.py +0 -0
  30. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/api/frameworks/flask/__init__.py +0 -0
  31. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/api/frameworks/flask/app.py +0 -0
  32. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/api/frameworks/flask/views.py +0 -0
  33. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/api/helpers.py +0 -0
  34. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/api/schemas.py +0 -0
  35. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/models/__init__.py +0 -0
  36. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/models/orms/__init__.py +0 -0
  37. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/models/orms/django.py +0 -0
  38. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/models/orms/tortoise.py +0 -0
  39. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/models/schemas.py +0 -0
  40. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/settings.py +0 -0
  41. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/static/assets/worker-6Z7niv9l.js +0 -0
  42. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/static/assets/worker-C151k0-L.js +0 -0
  43. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/static/assets/worker-C9KPOOKD.js +0 -0
  44. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/static/assets/worker-D1fCgYB7.js +0 -0
  45. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/static/assets/worker-D3TWcJOI.js +0 -0
  46. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/static/assets/worker-DRNcaZ-V.js +0 -0
  47. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/static/assets/worker-MF2p-l5_.js +0 -0
  48. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/static/images/favicon.png +0 -0
  49. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/static/images/header-logo.svg +0 -0
  50. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/static/images/sign-in-logo.svg +0 -0
  51. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/static/index.html +0 -0
  52. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/static/index.min.css +0 -0
  53. {fastadmin-0.4.8 → fastadmin-0.5.0}/fastadmin/templates/index.html +0 -0
@@ -0,0 +1,215 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastadmin
3
+ Version: 0.5.0
4
+ Summary: FastAdmin is an easy-to-use Admin Dashboard App for FastAPI/Flask/Django inspired by Django Admin.
5
+ Keywords: admin,dashboard,fastapi,flask,django,tortoise-orm,sqlalchemy,pony,admin-dashboard,django-admin
6
+ Author: Seva D
7
+ Author-email: Seva D <vsdudakov@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE.md
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Environment :: Console
12
+ Classifier: Framework :: FastAPI
13
+ Classifier: Framework :: Django
14
+ Classifier: Framework :: Flask
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Documentation
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Topic :: Software Development :: Widget Sets
23
+ Classifier: Typing :: Typed
24
+ Requires-Dist: pyjwt>=2.6
25
+ Requires-Dist: asgiref>=3.11
26
+ Requires-Dist: django>=6,<7 ; extra == 'django'
27
+ Requires-Dist: fastapi>=0.100,<1 ; extra == 'fastapi'
28
+ Requires-Dist: python-multipart>=0.0.5,<1 ; extra == 'fastapi'
29
+ Requires-Dist: flask>=3,<4 ; extra == 'flask'
30
+ Requires-Dist: pony>=0.7,<1 ; extra == 'pony'
31
+ Requires-Dist: sqlalchemy[asyncio]>=2,<3 ; extra == 'sqlalchemy'
32
+ Requires-Dist: greenlet>=3 ; extra == 'sqlalchemy'
33
+ Requires-Dist: tortoise-orm>=1,<2 ; extra == 'tortoise-orm'
34
+ Requires-Python: >=3.12
35
+ Project-URL: Homepage, https://vsdudakov.github.io/fastadmin/
36
+ Project-URL: Repository, https://github.com/vsdudakov/fastadmin
37
+ Project-URL: Documentation, https://vsdudakov.github.io/fastadmin/
38
+ Project-URL: Issues, https://github.com/vsdudakov/fastadmin/issues
39
+ Provides-Extra: django
40
+ Provides-Extra: fastapi
41
+ Provides-Extra: flask
42
+ Provides-Extra: pony
43
+ Provides-Extra: sqlalchemy
44
+ Provides-Extra: tortoise-orm
45
+ Description-Content-Type: text/markdown
46
+
47
+ <h1 align="center">FastAdmin — Admin Dashboard for FastAPI, Flask and Django</h1>
48
+
49
+ <p align="center">
50
+ <a href="https://github.com/vsdudakov/fastadmin/actions/workflows/ci.yml"><img src="https://github.com/vsdudakov/fastadmin/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
51
+ <a href="https://app.codecov.io/gh/vsdudakov/fastadmin"><img src="https://codecov.io/gh/vsdudakov/fastadmin/branch/main/graph/badge.svg?token=RNGX5HOW3T" alt="codecov"></a>
52
+ <a href="https://pypi.org/project/fastadmin/"><img src="https://badgen.net/pypi/v/fastadmin" alt="PyPI"></a>
53
+ <a href="https://pypi.org/project/fastadmin/"><img src="https://img.shields.io/badge/python-3.12%2B-blue.svg" alt="Python 3.12+"></a>
54
+ <a href="https://github.com/vsdudakov/fastadmin/blob/main/LICENSE.md"><img src="https://img.shields.io/github/license/vsdudakov/fastadmin" alt="License: MIT"></a>
55
+ </p>
56
+
57
+ **FastAdmin** is an easy-to-use **admin dashboard (admin panel) for FastAPI, Flask and
58
+ Django**, inspired by Django Admin. It gives your Python web application a
59
+ production-ready **CRUD admin interface** in minutes — on top of **Tortoise ORM,
60
+ Django ORM, SQLAlchemy or Pony ORM** — with authentication, filters, search,
61
+ inline editing, file uploads, CSV/JSON export and dashboard charts out of the box.
62
+
63
+ FastAdmin is built with relationships in mind and admiration for Django Admin.
64
+ It aims to be minimal, functional and familiar: if you know Django Admin, you
65
+ already know FastAdmin.
66
+
67
+ 📚 **Documentation: [https://vsdudakov.github.io/fastadmin/](https://vsdudakov.github.io/fastadmin/)**
68
+
69
+ ## Demo
70
+
71
+ ![FastAdmin demo — admin panel for FastAPI, Flask and Django](https://raw.githubusercontent.com/vsdudakov/fastadmin/main/docs/assets/images/demo.gif)
72
+
73
+ ## Features
74
+
75
+ - **Familiar Django-Admin-style API** — `list_display`, `list_filter`,
76
+ `search_fields`, `fieldsets`, `readonly_fields`, inlines, actions and more.
77
+ - **Any web framework** — mount as a FastAPI sub-app, a Flask blueprint or
78
+ Django urlpatterns.
79
+ - **Any ORM** — first-class admin classes for Tortoise ORM, Django ORM,
80
+ SQLAlchemy (async) and Pony ORM.
81
+ - **Authentication & permissions** — pluggable sign-in against your own user
82
+ model, per-action permission hooks, request/user context in every admin method.
83
+ - **Rich form widgets** — 20+ antd-based widgets (rich text, JSON, async
84
+ select, phone, slug, date/time pickers, switches, radio groups, …) via
85
+ `formfield_overrides`.
86
+ - **File & image uploads** — storage-agnostic `upload_file` hook and
87
+ presigned-URL support via `get_file_url` (local disk, S3, …).
88
+ - **Dashboard widgets** — declarative line/area/column/bar/pie charts and
89
+ action widgets with filters, powered by antd charts.
90
+ - **Bulk actions & export** — custom bulk actions, CSV/JSON export.
91
+ - **Quality** — fully typed and linted (ruff + ty), 100% backend test coverage,
92
+ modern React (Vite + antd) frontend bundled with the package — no Node.js
93
+ needed at install time.
94
+
95
+ ## Installation
96
+
97
+ Install with the extras matching your web framework and ORM (each combination
98
+ works standalone):
99
+
100
+ ```bash
101
+ pip install fastadmin[fastapi,django] # FastAPI with Django ORM
102
+ pip install fastadmin[fastapi,tortoise-orm] # FastAPI with Tortoise ORM
103
+ pip install fastadmin[fastapi,pony] # FastAPI with Pony ORM
104
+ pip install fastadmin[fastapi,sqlalchemy] # FastAPI with SQLAlchemy (includes greenlet)
105
+ pip install fastadmin[django] # Django with Django ORM
106
+ pip install fastadmin[django,pony] # Django with Pony ORM
107
+ pip install fastadmin[flask,sqlalchemy] # Flask with SQLAlchemy (includes greenlet)
108
+ ```
109
+
110
+ > **Note:** on zsh (the default macOS shell) quote the extras: `pip install 'fastadmin[fastapi,django]'`.
111
+
112
+ Configure the required settings:
113
+
114
+ ```bash
115
+ export ADMIN_USER_MODEL=User
116
+ export ADMIN_USER_MODEL_USERNAME_FIELD=username
117
+ export ADMIN_SECRET_KEY=secret_key
118
+ ```
119
+
120
+ ## Quick start
121
+
122
+ ```python
123
+ import bcrypt
124
+ from fastapi import FastAPI
125
+
126
+ from fastadmin import TortoiseModelAdmin, register
127
+ from fastadmin import fastapi_app as admin_app
128
+
129
+ from models import User
130
+
131
+
132
+ @register(User)
133
+ class UserAdmin(TortoiseModelAdmin):
134
+ exclude = ("hash_password",)
135
+ list_display = ("id", "username", "is_superuser", "is_active")
136
+ list_display_links = ("id", "username")
137
+ list_filter = ("id", "username", "is_superuser", "is_active")
138
+ search_fields = ("username",)
139
+
140
+ async def authenticate(self, username: str, password: str) -> int | None:
141
+ user = await User.filter(username=username, is_superuser=True).first()
142
+ if not user:
143
+ return None
144
+ if not bcrypt.checkpw(password.encode(), user.hash_password.encode()):
145
+ return None
146
+ return user.id
147
+
148
+ async def change_password(self, id: int, password: str) -> None:
149
+ user = await User.filter(id=id).first()
150
+ if not user:
151
+ return
152
+ user.hash_password = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
153
+ await user.save(update_fields=("hash_password",))
154
+
155
+
156
+ app = FastAPI()
157
+ app.mount("/admin", admin_app)
158
+ ```
159
+
160
+ Run `uvicorn example:app` and open `http://localhost:8000/admin`.
161
+
162
+ Mounting for the other frameworks:
163
+
164
+ ```python
165
+ # Flask
166
+ from fastadmin import flask_app as admin_app
167
+ app.register_blueprint(admin_app, url_prefix="/admin")
168
+
169
+ # Django (urls.py)
170
+ from fastadmin import get_django_admin_urls as get_admin_urls
171
+ urlpatterns = [path("admin/", get_admin_urls())]
172
+ ```
173
+
174
+ ## Documentation
175
+
176
+ Full documentation lives at
177
+ **[vsdudakov.github.io/fastadmin](https://vsdudakov.github.io/fastadmin/)**:
178
+
179
+ - [Installation](https://vsdudakov.github.io/fastadmin/getting-started/installation/)
180
+ and [Quick start](https://vsdudakov.github.io/fastadmin/getting-started/quickstart/)
181
+ - [Settings](https://vsdudakov.github.io/fastadmin/guides/settings/) — all environment variables
182
+ - [Registering models](https://vsdudakov.github.io/fastadmin/guides/registering-models/) — complete runnable examples for all four ORMs
183
+ - [Authentication](https://vsdudakov.github.io/fastadmin/guides/authentication/) and permissions
184
+ - [Model admins](https://vsdudakov.github.io/fastadmin/guides/model-admins/) — attributes, actions, display fields, exports
185
+ - [Form widgets & file uploads](https://vsdudakov.github.io/fastadmin/guides/form-widgets/)
186
+ - [Inline admins](https://vsdudakov.github.io/fastadmin/guides/inline-admins/)
187
+ - [Dashboard widgets](https://vsdudakov.github.io/fastadmin/guides/dashboard-widgets/) — charts and actions
188
+ - [API reference](https://vsdudakov.github.io/fastadmin/api-reference/)
189
+
190
+ Runnable example apps for every framework/ORM combination are in
191
+ [`examples/`](https://github.com/vsdudakov/fastadmin/tree/main/examples).
192
+
193
+ ## Why FastAdmin?
194
+
195
+ If you are looking for a **Django-Admin-like admin panel for FastAPI**, an
196
+ **admin interface for SQLAlchemy or Tortoise ORM**, or a lightweight
197
+ **alternative to building a custom back office**, FastAdmin gives you a
198
+ batteries-included, themeable admin UI without code generation, without tying
199
+ your app to a specific framework, and without writing a single React component.
200
+
201
+ ## Contributing
202
+
203
+ Contributions are welcome — see the
204
+ [contributing guide](https://vsdudakov.github.io/fastadmin/contributing/).
205
+ Development uses `uv` and `make`: `make dev`, `make lint`, `make test`.
206
+ See [CHANGELOG.md](https://github.com/vsdudakov/fastadmin/blob/main/CHANGELOG.md)
207
+ for release history.
208
+
209
+ If you have questions beyond the documentation, feel free to
210
+ [email us](mailto:vsdudakov@gmail.com).
211
+
212
+ ## License
213
+
214
+ FastAdmin is released under the
215
+ [MIT License](https://github.com/vsdudakov/fastadmin/blob/main/LICENSE.md).
@@ -0,0 +1,169 @@
1
+ <h1 align="center">FastAdmin — Admin Dashboard for FastAPI, Flask and Django</h1>
2
+
3
+ <p align="center">
4
+ <a href="https://github.com/vsdudakov/fastadmin/actions/workflows/ci.yml"><img src="https://github.com/vsdudakov/fastadmin/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
5
+ <a href="https://app.codecov.io/gh/vsdudakov/fastadmin"><img src="https://codecov.io/gh/vsdudakov/fastadmin/branch/main/graph/badge.svg?token=RNGX5HOW3T" alt="codecov"></a>
6
+ <a href="https://pypi.org/project/fastadmin/"><img src="https://badgen.net/pypi/v/fastadmin" alt="PyPI"></a>
7
+ <a href="https://pypi.org/project/fastadmin/"><img src="https://img.shields.io/badge/python-3.12%2B-blue.svg" alt="Python 3.12+"></a>
8
+ <a href="https://github.com/vsdudakov/fastadmin/blob/main/LICENSE.md"><img src="https://img.shields.io/github/license/vsdudakov/fastadmin" alt="License: MIT"></a>
9
+ </p>
10
+
11
+ **FastAdmin** is an easy-to-use **admin dashboard (admin panel) for FastAPI, Flask and
12
+ Django**, inspired by Django Admin. It gives your Python web application a
13
+ production-ready **CRUD admin interface** in minutes — on top of **Tortoise ORM,
14
+ Django ORM, SQLAlchemy or Pony ORM** — with authentication, filters, search,
15
+ inline editing, file uploads, CSV/JSON export and dashboard charts out of the box.
16
+
17
+ FastAdmin is built with relationships in mind and admiration for Django Admin.
18
+ It aims to be minimal, functional and familiar: if you know Django Admin, you
19
+ already know FastAdmin.
20
+
21
+ 📚 **Documentation: [https://vsdudakov.github.io/fastadmin/](https://vsdudakov.github.io/fastadmin/)**
22
+
23
+ ## Demo
24
+
25
+ ![FastAdmin demo — admin panel for FastAPI, Flask and Django](https://raw.githubusercontent.com/vsdudakov/fastadmin/main/docs/assets/images/demo.gif)
26
+
27
+ ## Features
28
+
29
+ - **Familiar Django-Admin-style API** — `list_display`, `list_filter`,
30
+ `search_fields`, `fieldsets`, `readonly_fields`, inlines, actions and more.
31
+ - **Any web framework** — mount as a FastAPI sub-app, a Flask blueprint or
32
+ Django urlpatterns.
33
+ - **Any ORM** — first-class admin classes for Tortoise ORM, Django ORM,
34
+ SQLAlchemy (async) and Pony ORM.
35
+ - **Authentication & permissions** — pluggable sign-in against your own user
36
+ model, per-action permission hooks, request/user context in every admin method.
37
+ - **Rich form widgets** — 20+ antd-based widgets (rich text, JSON, async
38
+ select, phone, slug, date/time pickers, switches, radio groups, …) via
39
+ `formfield_overrides`.
40
+ - **File & image uploads** — storage-agnostic `upload_file` hook and
41
+ presigned-URL support via `get_file_url` (local disk, S3, …).
42
+ - **Dashboard widgets** — declarative line/area/column/bar/pie charts and
43
+ action widgets with filters, powered by antd charts.
44
+ - **Bulk actions & export** — custom bulk actions, CSV/JSON export.
45
+ - **Quality** — fully typed and linted (ruff + ty), 100% backend test coverage,
46
+ modern React (Vite + antd) frontend bundled with the package — no Node.js
47
+ needed at install time.
48
+
49
+ ## Installation
50
+
51
+ Install with the extras matching your web framework and ORM (each combination
52
+ works standalone):
53
+
54
+ ```bash
55
+ pip install fastadmin[fastapi,django] # FastAPI with Django ORM
56
+ pip install fastadmin[fastapi,tortoise-orm] # FastAPI with Tortoise ORM
57
+ pip install fastadmin[fastapi,pony] # FastAPI with Pony ORM
58
+ pip install fastadmin[fastapi,sqlalchemy] # FastAPI with SQLAlchemy (includes greenlet)
59
+ pip install fastadmin[django] # Django with Django ORM
60
+ pip install fastadmin[django,pony] # Django with Pony ORM
61
+ pip install fastadmin[flask,sqlalchemy] # Flask with SQLAlchemy (includes greenlet)
62
+ ```
63
+
64
+ > **Note:** on zsh (the default macOS shell) quote the extras: `pip install 'fastadmin[fastapi,django]'`.
65
+
66
+ Configure the required settings:
67
+
68
+ ```bash
69
+ export ADMIN_USER_MODEL=User
70
+ export ADMIN_USER_MODEL_USERNAME_FIELD=username
71
+ export ADMIN_SECRET_KEY=secret_key
72
+ ```
73
+
74
+ ## Quick start
75
+
76
+ ```python
77
+ import bcrypt
78
+ from fastapi import FastAPI
79
+
80
+ from fastadmin import TortoiseModelAdmin, register
81
+ from fastadmin import fastapi_app as admin_app
82
+
83
+ from models import User
84
+
85
+
86
+ @register(User)
87
+ class UserAdmin(TortoiseModelAdmin):
88
+ exclude = ("hash_password",)
89
+ list_display = ("id", "username", "is_superuser", "is_active")
90
+ list_display_links = ("id", "username")
91
+ list_filter = ("id", "username", "is_superuser", "is_active")
92
+ search_fields = ("username",)
93
+
94
+ async def authenticate(self, username: str, password: str) -> int | None:
95
+ user = await User.filter(username=username, is_superuser=True).first()
96
+ if not user:
97
+ return None
98
+ if not bcrypt.checkpw(password.encode(), user.hash_password.encode()):
99
+ return None
100
+ return user.id
101
+
102
+ async def change_password(self, id: int, password: str) -> None:
103
+ user = await User.filter(id=id).first()
104
+ if not user:
105
+ return
106
+ user.hash_password = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
107
+ await user.save(update_fields=("hash_password",))
108
+
109
+
110
+ app = FastAPI()
111
+ app.mount("/admin", admin_app)
112
+ ```
113
+
114
+ Run `uvicorn example:app` and open `http://localhost:8000/admin`.
115
+
116
+ Mounting for the other frameworks:
117
+
118
+ ```python
119
+ # Flask
120
+ from fastadmin import flask_app as admin_app
121
+ app.register_blueprint(admin_app, url_prefix="/admin")
122
+
123
+ # Django (urls.py)
124
+ from fastadmin import get_django_admin_urls as get_admin_urls
125
+ urlpatterns = [path("admin/", get_admin_urls())]
126
+ ```
127
+
128
+ ## Documentation
129
+
130
+ Full documentation lives at
131
+ **[vsdudakov.github.io/fastadmin](https://vsdudakov.github.io/fastadmin/)**:
132
+
133
+ - [Installation](https://vsdudakov.github.io/fastadmin/getting-started/installation/)
134
+ and [Quick start](https://vsdudakov.github.io/fastadmin/getting-started/quickstart/)
135
+ - [Settings](https://vsdudakov.github.io/fastadmin/guides/settings/) — all environment variables
136
+ - [Registering models](https://vsdudakov.github.io/fastadmin/guides/registering-models/) — complete runnable examples for all four ORMs
137
+ - [Authentication](https://vsdudakov.github.io/fastadmin/guides/authentication/) and permissions
138
+ - [Model admins](https://vsdudakov.github.io/fastadmin/guides/model-admins/) — attributes, actions, display fields, exports
139
+ - [Form widgets & file uploads](https://vsdudakov.github.io/fastadmin/guides/form-widgets/)
140
+ - [Inline admins](https://vsdudakov.github.io/fastadmin/guides/inline-admins/)
141
+ - [Dashboard widgets](https://vsdudakov.github.io/fastadmin/guides/dashboard-widgets/) — charts and actions
142
+ - [API reference](https://vsdudakov.github.io/fastadmin/api-reference/)
143
+
144
+ Runnable example apps for every framework/ORM combination are in
145
+ [`examples/`](https://github.com/vsdudakov/fastadmin/tree/main/examples).
146
+
147
+ ## Why FastAdmin?
148
+
149
+ If you are looking for a **Django-Admin-like admin panel for FastAPI**, an
150
+ **admin interface for SQLAlchemy or Tortoise ORM**, or a lightweight
151
+ **alternative to building a custom back office**, FastAdmin gives you a
152
+ batteries-included, themeable admin UI without code generation, without tying
153
+ your app to a specific framework, and without writing a single React component.
154
+
155
+ ## Contributing
156
+
157
+ Contributions are welcome — see the
158
+ [contributing guide](https://vsdudakov.github.io/fastadmin/contributing/).
159
+ Development uses `uv` and `make`: `make dev`, `make lint`, `make test`.
160
+ See [CHANGELOG.md](https://github.com/vsdudakov/fastadmin/blob/main/CHANGELOG.md)
161
+ for release history.
162
+
163
+ If you have questions beyond the documentation, feel free to
164
+ [email us](mailto:vsdudakov@gmail.com).
165
+
166
+ ## License
167
+
168
+ FastAdmin is released under the
169
+ [MIT License](https://github.com/vsdudakov/fastadmin/blob/main/LICENSE.md).
@@ -301,12 +301,14 @@ async def upload_file(
301
301
  return JsonResponse({"error": "File not found"}, status=400)
302
302
  file_name = file.name
303
303
  file_content = file.read()
304
+ obj_id = request.GET.get("id") or None
304
305
  file_url = await api_service.upload_file(
305
306
  request.COOKIES.get(settings.ADMIN_SESSION_ID_KEY, None),
306
307
  model,
307
308
  field_name,
308
309
  file_name,
309
310
  file_content,
311
+ id=obj_id,
310
312
  request=request,
311
313
  )
312
314
  return JsonResponse(file_url, safe=False)
@@ -315,7 +317,7 @@ async def upload_file(
315
317
 
316
318
 
317
319
  @csrf_exempt
318
- async def export(request: HttpRequest, model: str) -> JsonResponse:
320
+ async def export(request: HttpRequest, model: str) -> JsonResponse | StreamingHttpResponse:
319
321
  """This method is used to export a list of objects.
320
322
 
321
323
  :params request: a request object.
@@ -244,6 +244,7 @@ async def upload_file(
244
244
  model: str,
245
245
  field_name: str,
246
246
  file: UploadFile,
247
+ id: str | None = None,
247
248
  ) -> str:
248
249
  """This method is used to upload files.
249
250
 
@@ -251,6 +252,7 @@ async def upload_file(
251
252
  :params model: a name of model.
252
253
  :params field_name: a name of field.
253
254
  :params file: a file object.
255
+ :params id: an optional id of an existing object (change page). None on the add page.
254
256
  :return: A file url.
255
257
  """
256
258
  try:
@@ -264,6 +266,7 @@ async def upload_file(
264
266
  field_name,
265
267
  file_name,
266
268
  file_content,
269
+ id=id,
267
270
  request=request,
268
271
  )
269
272
  except AdminApiException as e:
@@ -304,7 +307,7 @@ async def export(
304
307
  )
305
308
  headers = {"Content-Disposition": f'attachment; filename="{file_name}"'}
306
309
  return StreamingResponse(
307
- stream, # type: ignore [arg-type]
310
+ stream, # ty: ignore[invalid-argument-type]
308
311
  headers=headers,
309
312
  media_type=content_type,
310
313
  )
@@ -188,7 +188,7 @@ async def add(model: str) -> dict:
188
188
  raise http_exception from e
189
189
 
190
190
 
191
- @api_router.route("/change-password/<string:id>", methods=["PATCH"]) # type: ignore [type-var]
191
+ @api_router.route("/change-password/<string:id>", methods=["PATCH"]) # ty: ignore[invalid-argument-type]
192
192
  async def change_password(id: UUID | int | str) -> UUID | int | str:
193
193
  """This method is used to change password.
194
194
 
@@ -267,12 +267,14 @@ async def upload_file(
267
267
  http_exception.code = 422
268
268
  raise http_exception from None
269
269
  file_content = file.read()
270
+ obj_id = request.args.get("id") or None
270
271
  return await api_service.upload_file(
271
272
  request.cookies.get(settings.ADMIN_SESSION_ID_KEY, None),
272
273
  model,
273
274
  field_name,
274
275
  file_name,
275
276
  file_content,
277
+ id=obj_id,
276
278
  request=request,
277
279
  )
278
280
  except AdminApiException as e:
@@ -320,7 +322,7 @@ async def export(model: str) -> Response:
320
322
  raise http_exception from e
321
323
 
322
324
 
323
- @api_router.route("/delete/<string:model>/<string:id>", methods=["DELETE"]) # type: ignore [type-var]
325
+ @api_router.route("/delete/<string:model>/<string:id>", methods=["DELETE"]) # ty: ignore[invalid-argument-type]
324
326
  async def delete(
325
327
  model: str,
326
328
  id: UUID | int | str,
@@ -1,7 +1,7 @@
1
1
  import inspect
2
2
  import logging
3
3
  from collections.abc import Sequence
4
- from datetime import datetime, timedelta, timezone
4
+ from datetime import UTC, datetime, timedelta
5
5
  from io import BytesIO, StringIO
6
6
  from typing import Any, cast
7
7
  from uuid import UUID
@@ -68,7 +68,7 @@ async def get_user_id_from_session_id(session_id: str | None) -> UUID | int | No
68
68
  if not session_expired_at:
69
69
  return None
70
70
 
71
- if datetime.fromisoformat(session_expired_at).replace(tzinfo=timezone.utc) < datetime.now(timezone.utc):
71
+ if datetime.fromisoformat(session_expired_at).replace(tzinfo=UTC) < datetime.now(UTC):
72
72
  return None
73
73
 
74
74
  user_id = token_payload.get("user_id")
@@ -125,7 +125,7 @@ class ApiService:
125
125
  if not user_id or not isinstance(user_id, int | UUID):
126
126
  raise AdminApiException(401, detail="Invalid credentials.")
127
127
 
128
- now = datetime.now(timezone.utc)
128
+ now = datetime.now(UTC)
129
129
  session_expired_at = now + timedelta(seconds=settings.ADMIN_SESSION_EXPIRED_AT)
130
130
  if isinstance(user_id, UUID):
131
131
  user_id = str(user_id)
@@ -254,7 +254,7 @@ class ApiService:
254
254
  raise AdminApiException(404, detail=f"{model} model is not registered.")
255
255
  self._bind_admin_context(admin_model, request=request, user=current_user)
256
256
  try:
257
- return await admin_model.save_model(None, payload) # type: ignore [return-value]
257
+ return await admin_model.save_model(None, payload) # ty: ignore[invalid-return-type]
258
258
  except Exception as e:
259
259
  logger.error("Error adding %s: %s", model, e)
260
260
  raise AdminApiException(500, detail=f"Error adding {model}: {e}") from e
@@ -329,6 +329,7 @@ class ApiService:
329
329
  field_name: str,
330
330
  file_name: str,
331
331
  file_content: bytes,
332
+ id: UUID | int | str | None = None,
332
333
  request: Any | None = None,
333
334
  ) -> str:
334
335
  _current_user_id, current_user = await self._get_authenticated_user(session_id)
@@ -339,16 +340,23 @@ class ApiService:
339
340
  self._bind_admin_context(admin_model, request=request, user=current_user)
340
341
 
341
342
  try:
343
+ obj = await admin_model.orm_get_obj(id) if id is not None else None
344
+ if id is not None and obj is None:
345
+ raise AdminApiException(404, detail=f"{model} with id {id} not found.")
346
+
342
347
  if inspect.iscoroutinefunction(admin_model.upload_file):
343
348
  upload_file_fn = admin_model.upload_file
344
349
  else:
345
350
  upload_file_fn = sync_to_async(admin_model.upload_file) # type: ignore [arg-type]
346
351
 
347
- return await upload_file_fn(
352
+ return await upload_file_fn( # ty: ignore[invalid-return-type]
348
353
  field_name,
349
354
  file_name,
350
355
  file_content,
356
+ obj=obj,
351
357
  )
358
+ except AdminApiException:
359
+ raise
352
360
  except Exception as e:
353
361
  raise AdminApiException(500, detail=f"Error uploading file for {model}: {e}") from e
354
362
 
@@ -14,6 +14,8 @@ from asgiref.sync import sync_to_async
14
14
  from fastadmin.api.schemas import ExportFormat
15
15
  from fastadmin.models.schemas import ModelFieldWidgetSchema, WidgetType
16
16
 
17
+ Model = Any
18
+
17
19
 
18
20
  class BaseModelAdmin:
19
21
  """Base class for model admin"""
@@ -373,7 +375,7 @@ class BaseModelAdmin:
373
375
  return sort_by
374
376
 
375
377
  async def serialize_obj_attributes(
376
- self, obj: Any, attributes_to_serizalize: list[ModelFieldWidgetSchema]
378
+ self, obj: Any, attributes_to_serizalize: list[ModelFieldWidgetSchema], list_view: bool = False
377
379
  ) -> dict[str, Any]:
378
380
  """Serialize orm model obj attribute to dict.
379
381
 
@@ -388,7 +390,10 @@ class BaseModelAdmin:
388
390
  # Avoid scientific notation for Decimal values in API responses,
389
391
  # e.g. 3.75E+3 -> "3750"
390
392
  value = format(value, "f")
393
+
391
394
  serialized_dict[field.name] = value
395
+ if not list_view and field.form_widget_type in (WidgetType.UploadFile, WidgetType.UploadImage) and value:
396
+ serialized_dict[f"{field.name}__url"] = await self.get_file_url(field.name, value, obj)
392
397
  if inspect.iscoroutinefunction(obj.__str__):
393
398
  str_fn = obj.__str__
394
399
  else:
@@ -435,7 +440,7 @@ class BaseModelAdmin:
435
440
  else:
436
441
  attributes_to_serizalize.append(field)
437
442
 
438
- obj_dict.update(await self.serialize_obj_attributes(obj, attributes_to_serizalize))
443
+ obj_dict.update(await self.serialize_obj_attributes(obj, attributes_to_serizalize, list_view=list_view))
439
444
 
440
445
  for field_name in fields_for_serialize:
441
446
  display_field_function = getattr(self, field_name, None)
@@ -464,6 +469,7 @@ class BaseModelAdmin:
464
469
  return datetime.datetime.fromisoformat(value).date()
465
470
  case WidgetType.DateTimePicker:
466
471
  return datetime.datetime.fromisoformat(value)
472
+
467
473
  case _:
468
474
  return value
469
475
 
@@ -541,17 +547,51 @@ class BaseModelAdmin:
541
547
  """
542
548
  await self.orm_delete_obj(id)
543
549
 
550
+ async def get_file_url(self, field_name: str, value: str, obj: Any | None = None) -> str:
551
+ """Return the display URL for an uploaded file field.
552
+
553
+ Called during serialization for every ``UploadFile`` and ``UploadImage``
554
+ field that has a non-empty stored value. The result is emitted alongside
555
+ the stored value as ``{field_name}__url`` and forwarded to the upload
556
+ widget as ``valueRepr`` so the clickable link can differ from the raw
557
+ stored key (e.g. an S3 presigned URL instead of an ``s3://`` URI).
558
+
559
+ The stored ``value`` is never modified — it is still what the form
560
+ submits on save — so there is no risk of data corruption.
561
+
562
+ Override to return a dynamic URL. Example using aiobotocore:
563
+
564
+ .. code-block:: python
565
+
566
+ async def get_file_url(self, field_name: str, value: str, obj=None) -> str:
567
+ bucket, key = value.replace("s3://", "").split("/", 1)
568
+ async with aiobotocore.session.get_session().create_client("s3") as s3:
569
+ return await s3.generate_presigned_url(
570
+ "get_object",
571
+ Params={"Bucket": bucket, "Key": key},
572
+ ExpiresIn=3600,
573
+ )
574
+
575
+ :params field_name: name of the upload field being serialized.
576
+ :params value: raw stored value (e.g. ``"s3://bucket/key"`` or ``"/media/file.pdf"``).
577
+ :params obj: the orm/db model instance being serialized, or None.
578
+ :return: URL string to expose to the frontend for display/download.
579
+ """
580
+ return value
581
+
544
582
  async def upload_file(
545
583
  self,
546
584
  field_name: str,
547
585
  file_name: str,
548
586
  file_content: bytes,
587
+ obj: Any | None = None,
549
588
  ) -> str:
550
589
  """This method is used to upload files.
551
590
 
552
591
  :params field_name: a name of field.
553
592
  :params file_name: a name of file.
554
593
  :params file_content: a content of file.
594
+ :params obj: an orm/db model object. None on the add (create) page; the existing model instance on the change (edit) page.
555
595
  :return: A file url.
556
596
  """
557
597
  raise NotImplementedError
@@ -597,11 +637,11 @@ class BaseModelAdmin:
597
637
  case ExportFormat.JSON:
598
638
 
599
639
  class JSONEncoder(json.JSONEncoder):
600
- def default(self, obj):
640
+ def default(self, o):
601
641
  try:
602
- return super().default(obj)
642
+ return super().default(o)
603
643
  except TypeError:
604
- return str(obj)
644
+ return str(o)
605
645
 
606
646
  output = StringIO()
607
647
  json.dump([await self.serialize_obj(obj, list_view=True) for obj in objs], output, cls=JSONEncoder)
@@ -200,7 +200,7 @@ def register(*orm_model_classes, **kwargs):
200
200
  if not issubclass(model_admin_cls, ModelAdmin):
201
201
  raise ValueError("Wrapped class must subclass ModelAdmin.")
202
202
 
203
- register_admin_model_class(model_admin_cls, orm_model_classes, **kwargs)
203
+ register_admin_model_class(model_admin_cls, list(orm_model_classes), **kwargs)
204
204
 
205
205
  return model_admin_cls
206
206
 
@@ -284,7 +284,7 @@ async def generate_models_schema(
284
284
  save_as=admin_model_obj.save_as,
285
285
  save_as_continue=admin_model_obj.save_as_continue,
286
286
  view_on_site=admin_model_obj.view_on_site,
287
- inlines=await generate_models_schema(
287
+ inlines=await generate_models_schema( # ty: ignore[invalid-argument-type]
288
288
  {inline.model: inline(inline.model) for inline in admin_model_obj.inlines},
289
289
  user=user,
290
290
  request=request,