fastapi-admin-kit 0.1.0__py3-none-any.whl

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 (163) hide show
  1. fastapi_admin_kit/__init__.py +73 -0
  2. fastapi_admin_kit/actions/__init__.py +63 -0
  3. fastapi_admin_kit/actions/base.py +68 -0
  4. fastapi_admin_kit/actions/registry.py +43 -0
  5. fastapi_admin_kit/admin/__init__.py +17 -0
  6. fastapi_admin_kit/admin/admin_config.py +95 -0
  7. fastapi_admin_kit/admin/admin_database.py +138 -0
  8. fastapi_admin_kit/admin/admin_router.py +74 -0
  9. fastapi_admin_kit/admin/admin_template.py +203 -0
  10. fastapi_admin_kit/admin/builtin_models.py +284 -0
  11. fastapi_admin_kit/admin/core.py +1036 -0
  12. fastapi_admin_kit/admin/decorators.py +70 -0
  13. fastapi_admin_kit/admin/state.py +76 -0
  14. fastapi_admin_kit/admin.py +728 -0
  15. fastapi_admin_kit/api/__init__.py +44 -0
  16. fastapi_admin_kit/api/auth.py +342 -0
  17. fastapi_admin_kit/api/crud.py +128 -0
  18. fastapi_admin_kit/api/deps.py +79 -0
  19. fastapi_admin_kit/api/roles.py +128 -0
  20. fastapi_admin_kit/api/schema_generator.py +171 -0
  21. fastapi_admin_kit/api/schemas.py +81 -0
  22. fastapi_admin_kit/api/search.py +132 -0
  23. fastapi_admin_kit/audit/__init__.py +36 -0
  24. fastapi_admin_kit/audit/context.py +62 -0
  25. fastapi_admin_kit/audit/diff.py +77 -0
  26. fastapi_admin_kit/audit/event_bus.py +96 -0
  27. fastapi_admin_kit/audit/events.py +48 -0
  28. fastapi_admin_kit/audit/listener.py +159 -0
  29. fastapi_admin_kit/audit/logger.py +28 -0
  30. fastapi_admin_kit/audit/middleware.py +39 -0
  31. fastapi_admin_kit/audit/models.py +53 -0
  32. fastapi_admin_kit/audit/sqlalchemy_logger.py +58 -0
  33. fastapi_admin_kit/auth/__init__.py +34 -0
  34. fastapi_admin_kit/auth/backend.py +95 -0
  35. fastapi_admin_kit/auth/csrf.py +240 -0
  36. fastapi_admin_kit/auth/dependencies.py +150 -0
  37. fastapi_admin_kit/auth/identity.py +181 -0
  38. fastapi_admin_kit/auth/models.py +246 -0
  39. fastapi_admin_kit/auth/password.py +35 -0
  40. fastapi_admin_kit/auth/permissions.py +205 -0
  41. fastapi_admin_kit/auth/protocol.py +22 -0
  42. fastapi_admin_kit/auth/ratelimit.py +88 -0
  43. fastapi_admin_kit/auth/router.py +10 -0
  44. fastapi_admin_kit/auth/session.py +79 -0
  45. fastapi_admin_kit/auth/totp.py +83 -0
  46. fastapi_admin_kit/auth/views.py +165 -0
  47. fastapi_admin_kit/cli.py +229 -0
  48. fastapi_admin_kit/config/__init__.py +19 -0
  49. fastapi_admin_kit/config/audit.py +18 -0
  50. fastapi_admin_kit/config/auth.py +54 -0
  51. fastapi_admin_kit/config/behavior.py +27 -0
  52. fastapi_admin_kit/config/nav.py +32 -0
  53. fastapi_admin_kit/config/storage.py +22 -0
  54. fastapi_admin_kit/config/theme.py +215 -0
  55. fastapi_admin_kit/config/ui.py +147 -0
  56. fastapi_admin_kit/dashboard/__init__.py +64 -0
  57. fastapi_admin_kit/db.py +133 -0
  58. fastapi_admin_kit/exceptions.py +5 -0
  59. fastapi_admin_kit/field_types.py +81 -0
  60. fastapi_admin_kit/filters/__init__.py +21 -0
  61. fastapi_admin_kit/filters/base.py +170 -0
  62. fastapi_admin_kit/filters/registry.py +68 -0
  63. fastapi_admin_kit/flash.py +45 -0
  64. fastapi_admin_kit/form/__init__.py +1 -0
  65. fastapi_admin_kit/form/pipeline.py +106 -0
  66. fastapi_admin_kit/inspection/__init__.py +117 -0
  67. fastapi_admin_kit/inspection/registry.py +253 -0
  68. fastapi_admin_kit/inspection.py +115 -0
  69. fastapi_admin_kit/modeladmin.py +375 -0
  70. fastapi_admin_kit/models/__init__.py +7 -0
  71. fastapi_admin_kit/models/base.py +7 -0
  72. fastapi_admin_kit/nav.py +208 -0
  73. fastapi_admin_kit/pagination/__init__.py +14 -0
  74. fastapi_admin_kit/pagination/base.py +40 -0
  75. fastapi_admin_kit/pagination/cursor.py +97 -0
  76. fastapi_admin_kit/pagination/dynamic.py +48 -0
  77. fastapi_admin_kit/pagination/offset.py +42 -0
  78. fastapi_admin_kit/plugins/__init__.py +1 -0
  79. fastapi_admin_kit/py.typed +0 -0
  80. fastapi_admin_kit/registry/__init__.py +5 -0
  81. fastapi_admin_kit/registry/core.py +287 -0
  82. fastapi_admin_kit/registry/validation.py +107 -0
  83. fastapi_admin_kit/registry.py +15 -0
  84. fastapi_admin_kit/router.py +335 -0
  85. fastapi_admin_kit/static/css/admin.css +4736 -0
  86. fastapi_admin_kit/static/css/presets.css +317 -0
  87. fastapi_admin_kit/static/css/tokens.css +217 -0
  88. fastapi_admin_kit/static/css/variables.css +74 -0
  89. fastapi_admin_kit/static/icons/heroicons.svg +160 -0
  90. fastapi_admin_kit/static/js/admin.js +692 -0
  91. fastapi_admin_kit/static/js/htmx-config.js +42 -0
  92. fastapi_admin_kit/storage/__init__.py +6 -0
  93. fastapi_admin_kit/storage/base.py +48 -0
  94. fastapi_admin_kit/storage/local.py +73 -0
  95. fastapi_admin_kit/templates/base.html +142 -0
  96. fastapi_admin_kit/templates/macros/form_fields.html +660 -0
  97. fastapi_admin_kit/templates/macros/icons.html +50 -0
  98. fastapi_admin_kit/templates/macros/table.html +108 -0
  99. fastapi_admin_kit/templates/macros/widgets.html +159 -0
  100. fastapi_admin_kit/templates/pages/2fa/setup.html +122 -0
  101. fastapi_admin_kit/templates/pages/2fa/verify.html +55 -0
  102. fastapi_admin_kit/templates/pages/audit_detail.html +122 -0
  103. fastapi_admin_kit/templates/pages/audit_log.html +102 -0
  104. fastapi_admin_kit/templates/pages/dashboard.html +295 -0
  105. fastapi_admin_kit/templates/pages/detail.html +183 -0
  106. fastapi_admin_kit/templates/pages/form.html +119 -0
  107. fastapi_admin_kit/templates/pages/list.html +277 -0
  108. fastapi_admin_kit/templates/pages/login.html +85 -0
  109. fastapi_admin_kit/templates/pages/profile/password.html +78 -0
  110. fastapi_admin_kit/templates/pages/profile/profile.html +73 -0
  111. fastapi_admin_kit/templates/pages/role_form.html +75 -0
  112. fastapi_admin_kit/templates/pages/roles/form.html +117 -0
  113. fastapi_admin_kit/templates/pages/roles/list.html +69 -0
  114. fastapi_admin_kit/templates/pages/roles.html +77 -0
  115. fastapi_admin_kit/templates/pages/settings/theme.html +255 -0
  116. fastapi_admin_kit/templates/pages/users/form.html +229 -0
  117. fastapi_admin_kit/templates/pages/users/list.html +83 -0
  118. fastapi_admin_kit/templates/partials/command_palette.html +52 -0
  119. fastapi_admin_kit/templates/partials/field_wrapper.html +2 -0
  120. fastapi_admin_kit/templates/partials/flash_messages.html +39 -0
  121. fastapi_admin_kit/templates/partials/head.html +21 -0
  122. fastapi_admin_kit/templates/partials/head_minimal.html +18 -0
  123. fastapi_admin_kit/templates/partials/list_table.html +178 -0
  124. fastapi_admin_kit/templates/partials/mobile_backdrop.html +2 -0
  125. fastapi_admin_kit/templates/partials/pagination.html +82 -0
  126. fastapi_admin_kit/templates/partials/permission_widget.html +86 -0
  127. fastapi_admin_kit/templates/partials/scripts.html +13 -0
  128. fastapi_admin_kit/templates/partials/sidebar.html +94 -0
  129. fastapi_admin_kit/templates/partials/topbar.html +95 -0
  130. fastapi_admin_kit/types.py +145 -0
  131. fastapi_admin_kit/validation.py +43 -0
  132. fastapi_admin_kit/views/__init__.py +78 -0
  133. fastapi_admin_kit/views/audit.py +134 -0
  134. fastapi_admin_kit/views/bulk.py +28 -0
  135. fastapi_admin_kit/views/class_views.py +1040 -0
  136. fastapi_admin_kit/views/context.py +588 -0
  137. fastapi_admin_kit/views/dashboard.py +162 -0
  138. fastapi_admin_kit/views/delete.py +31 -0
  139. fastapi_admin_kit/views/extra.py +65 -0
  140. fastapi_admin_kit/views/factory.py +667 -0
  141. fastapi_admin_kit/views/form.py +159 -0
  142. fastapi_admin_kit/views/list.py +28 -0
  143. fastapi_admin_kit/views/profile.py +219 -0
  144. fastapi_admin_kit/views/protocols.py +54 -0
  145. fastapi_admin_kit/views/renderers.py +634 -0
  146. fastapi_admin_kit/views/roles.py +230 -0
  147. fastapi_admin_kit/views/search.py +31 -0
  148. fastapi_admin_kit/views/settings.py +31 -0
  149. fastapi_admin_kit/views/sidebar.py +101 -0
  150. fastapi_admin_kit/views/totp.py +249 -0
  151. fastapi_admin_kit/views/users.py +347 -0
  152. fastapi_admin_kit/views.py +117 -0
  153. fastapi_admin_kit/widgets/__init__.py +44 -0
  154. fastapi_admin_kit/widgets/base.py +44 -0
  155. fastapi_admin_kit/widgets/inputs.py +363 -0
  156. fastapi_admin_kit/widgets/registry.py +110 -0
  157. fastapi_admin_kit/widgets/relation.py +70 -0
  158. fastapi_admin_kit/widgets/resolver.py +102 -0
  159. fastapi_admin_kit-0.1.0.dist-info/METADATA +210 -0
  160. fastapi_admin_kit-0.1.0.dist-info/RECORD +163 -0
  161. fastapi_admin_kit-0.1.0.dist-info/WHEEL +4 -0
  162. fastapi_admin_kit-0.1.0.dist-info/entry_points.txt +3 -0
  163. fastapi_admin_kit-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,335 @@
1
+ """Auto-route generation per registered model with RBAC."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fastapi import APIRouter, Depends, HTTPException, Request
6
+
7
+ from fastapi_admin_kit.auth.csrf import require_csrf_token
8
+ from fastapi_admin_kit.auth.dependencies import require_permission
9
+ from fastapi_admin_kit.db import get_db_session
10
+ from fastapi_admin_kit.registry import RegisteredModel
11
+ from fastapi_admin_kit.views.class_views import (
12
+ BulkView,
13
+ CreateView,
14
+ DeleteView,
15
+ EditView,
16
+ ListView,
17
+ SearchView,
18
+ _resolve_view_class,
19
+ )
20
+
21
+
22
+ def build_model_router(registered: RegisteredModel) -> APIRouter:
23
+ router = APIRouter(prefix=f"/{registered.table_name}")
24
+
25
+ # DIP: view classes resolved from ModelAdmin config with defaults
26
+ admin = registered.admin
27
+ list_v = _resolve_view_class(admin, "list_view_class", ListView)(registered)
28
+ create_v = _resolve_view_class(admin, "create_view_class", CreateView)(registered)
29
+ edit_v = _resolve_view_class(admin, "edit_view_class", EditView)(registered)
30
+ delete_v = _resolve_view_class(admin, "delete_view_class", DeleteView)(registered)
31
+ bulk_v = _resolve_view_class(admin, "bulk_view_class", BulkView)(registered)
32
+ search_v = _resolve_view_class(admin, "search_view_class", SearchView)(registered)
33
+
34
+ router.add_api_route(
35
+ "/",
36
+ list_v.html_response,
37
+ methods=["GET"],
38
+ dependencies=[Depends(require_permission(registered.table_name, "view"))],
39
+ )
40
+ router.add_api_route(
41
+ "/create",
42
+ create_v.html_response,
43
+ methods=["GET"],
44
+ dependencies=[Depends(require_permission(registered.table_name, "create"))],
45
+ )
46
+ router.add_api_route(
47
+ "/create",
48
+ create_v.html_response,
49
+ methods=["POST"],
50
+ dependencies=[
51
+ Depends(require_permission(registered.table_name, "create")),
52
+ Depends(require_csrf_token),
53
+ ],
54
+ )
55
+ router.add_api_route(
56
+ "/search",
57
+ search_v.html_response,
58
+ methods=["GET"],
59
+ dependencies=[Depends(require_permission(registered.table_name, "view"))],
60
+ )
61
+ router.add_api_route(
62
+ "/bulk",
63
+ bulk_v.html_response,
64
+ methods=["POST"],
65
+ dependencies=[
66
+ Depends(require_permission(registered.table_name, "edit")),
67
+ Depends(require_csrf_token),
68
+ ],
69
+ )
70
+
71
+ @router.post("/validate-field")
72
+ async def validate_field_endpoint(
73
+ request: Request,
74
+ _csrf: bool = Depends(require_csrf_token),
75
+ _: None = Depends(require_permission(registered.table_name, "view")),
76
+ ):
77
+ templates = request.app.state.admin_jinja_env
78
+ form = await request.form()
79
+ field_name = form.get("field_name")
80
+ raw_value = form.get(field_name)
81
+
82
+ field_meta = next(
83
+ (f for f in registered.form_fields if f.name == field_name), None
84
+ )
85
+ if field_meta is None:
86
+ raise HTTPException(status_code=422, detail="Field not found")
87
+
88
+ widget = registered.get_widget(field_name)
89
+ parsed = widget.parse(raw_value)
90
+ errors = widget.validate(parsed, field_meta)
91
+
92
+ validator_fn = getattr(registered.admin, f"validate_{field_name}", None)
93
+ if validator_fn and not errors:
94
+ err = validator_fn(parsed, obj=None)
95
+ if err:
96
+ errors = [err]
97
+
98
+ from fastapi_admin_kit.types import FieldRenderContext
99
+
100
+ field_ctx = FieldRenderContext(
101
+ meta=field_meta,
102
+ widget_macro=widget.macro_name,
103
+ widget_context=widget.render_context(field_meta, raw_value),
104
+ errors=errors,
105
+ )
106
+ return templates.TemplateResponse(request, "partials/field_wrapper.html", {
107
+ "field_ctx": field_ctx,
108
+ "model_name": registered.table_name,
109
+ })
110
+
111
+ router.add_api_route(
112
+ "/{id}",
113
+ edit_v.html_response,
114
+ methods=["GET"],
115
+ dependencies=[Depends(require_permission(registered.table_name, "edit"))],
116
+ )
117
+ router.add_api_route(
118
+ "/{id}",
119
+ edit_v.html_response,
120
+ methods=["POST"],
121
+ dependencies=[
122
+ Depends(require_permission(registered.table_name, "edit")),
123
+ Depends(require_csrf_token),
124
+ ],
125
+ )
126
+ router.add_api_route(
127
+ "/{id}/delete",
128
+ delete_v.html_response,
129
+ methods=["POST"],
130
+ dependencies=[
131
+ Depends(require_permission(registered.table_name, "delete")),
132
+ Depends(require_csrf_token),
133
+ ],
134
+ )
135
+
136
+ # ── Custom Actions ─────────────────────────────────────────────
137
+
138
+ @router.post("/action/{action_name}")
139
+ async def execute_list_action(
140
+ request: Request,
141
+ action_name: str,
142
+ _csrf: bool = Depends(require_csrf_token),
143
+ ):
144
+ """Execute a list-level action on selected objects."""
145
+ session = get_db_session(request)
146
+ form = await request.form()
147
+ ids = form.getlist("ids[]")
148
+
149
+ action_obj = None
150
+ for a in registered.admin.get_list_actions():
151
+ if a.name == action_name:
152
+ action_obj = a
153
+ break
154
+
155
+ if not action_obj:
156
+ raise HTTPException(status_code=404, detail=f"Unknown action: {action_name}")
157
+
158
+ objects = []
159
+ for pid in ids:
160
+ obj = await session.get(registered.model, pid)
161
+ if obj:
162
+ objects.append(obj)
163
+
164
+ if objects:
165
+ await action_obj.execute(objects, request)
166
+ await session.flush()
167
+
168
+ from fastapi.responses import HTMLResponse
169
+ return HTMLResponse(content="OK")
170
+
171
+ @router.post("/action/{action_name}/{id}")
172
+ async def execute_row_action(
173
+ request: Request,
174
+ action_name: str,
175
+ id: str,
176
+ _csrf: bool = Depends(require_csrf_token),
177
+ ):
178
+ """Execute a row-level action on a single object."""
179
+ session = get_db_session(request)
180
+
181
+ action_obj = None
182
+ for a in registered.admin.get_row_actions():
183
+ if a.name == action_name:
184
+ action_obj = a
185
+ break
186
+
187
+ if not action_obj:
188
+ raise HTTPException(status_code=404, detail=f"Unknown action: {action_name}")
189
+
190
+ obj = await session.get(registered.model, int(id))
191
+ if not obj:
192
+ raise HTTPException(status_code=404, detail="Not found")
193
+
194
+ await action_obj.execute([obj], request)
195
+ await session.flush()
196
+
197
+ from fastapi.responses import HTMLResponse
198
+ return HTMLResponse(content="OK")
199
+
200
+ # ── Sortable Endpoint ──────────────────────────────────────────
201
+
202
+ @router.post("/sort")
203
+ async def sort_items(
204
+ request: Request,
205
+ _csrf: bool = Depends(require_csrf_token),
206
+ ):
207
+ """Handle drag-drop sort updates."""
208
+ body = await request.json()
209
+ ordering_field = getattr(registered.admin, "ordering_field", None)
210
+ if not ordering_field:
211
+ raise HTTPException(status_code=400, detail="Sorting not configured")
212
+
213
+ session = get_db_session(request)
214
+ items = body.get("items", [])
215
+ for idx, item_id in enumerate(items):
216
+ obj = await session.get(registered.model, item_id)
217
+ if obj:
218
+ setattr(obj, ordering_field, idx)
219
+ await session.flush()
220
+
221
+ from fastapi.responses import HTMLResponse
222
+ return HTMLResponse(content="OK")
223
+
224
+ # ── Autocomplete Endpoint ──────────────────────────────────────
225
+
226
+ @router.get("/autocomplete/")
227
+ async def autocomplete(
228
+ request: Request,
229
+ q: str = "",
230
+ _csrf: bool = Depends(require_permission(registered.table_name, "view")),
231
+ ):
232
+ """Search-as-you-type endpoint for relation pickers."""
233
+ from fastapi.responses import JSONResponse
234
+
235
+ session = get_db_session(request)
236
+ model = registered.model
237
+ results = []
238
+
239
+ search_fields = getattr(registered.admin, "search_fields", None) or ["name", "title"]
240
+ clauses = []
241
+ for sf in search_fields:
242
+ if hasattr(model, sf):
243
+ col = getattr(model, sf)
244
+ if hasattr(col, "ilike"):
245
+ clauses.append(col.ilike(f"%{q}%"))
246
+
247
+ if clauses:
248
+ from sqlalchemy import or_, select
249
+ query = select(model).where(or_(*clauses)).limit(20)
250
+ result = await session.execute(query)
251
+ for obj in result.scalars():
252
+ label = str(
253
+ getattr(obj, "name", None)
254
+ or getattr(obj, "title", None)
255
+ or f"#{getattr(obj, 'id', '?')}"
256
+ )
257
+ results.append({"id": str(obj.id), "label": label})
258
+
259
+ return JSONResponse(content=results)
260
+
261
+ # ── Inline Field Update (existing) ─────────────────────────────
262
+ @router.patch("/{id}/field")
263
+ async def update_field(
264
+ request: Request,
265
+ id: str,
266
+ _csrf: bool = Depends(require_csrf_token),
267
+ ):
268
+ """Inline field update — used by toggle switches in list view."""
269
+ from fastapi_admin_kit.auth.csrf import _get_secret_key, generate_csrf_token
270
+
271
+ content_type = request.headers.get("content-type", "")
272
+ if "application/json" in content_type:
273
+ body = await request.json()
274
+ else:
275
+ form = await request.form()
276
+ body = dict(form)
277
+
278
+ field_name = body.get("field")
279
+ new_value = body.get("value")
280
+
281
+ if not field_name:
282
+ raise HTTPException(status_code=400, detail="Missing field name")
283
+
284
+ readonly = getattr(registered.admin, "readonly_fields", []) or []
285
+ if field_name in readonly:
286
+ raise HTTPException(status_code=403, detail="Field is read-only")
287
+
288
+ col = next((c for c in registered.columns if c.name == field_name), None)
289
+ if col is None:
290
+ raise HTTPException(status_code=400, detail="Invalid field name")
291
+
292
+ if col.type.__class__.__name__ == "Boolean":
293
+ new_value = str(new_value).lower() in ("true", "1", "yes")
294
+
295
+ session = get_db_session(request)
296
+ try:
297
+ obj = await session.get(registered.model, int(id))
298
+ if obj is None:
299
+ raise HTTPException(status_code=404, detail="Object not found")
300
+
301
+ setattr(obj, field_name, new_value)
302
+ await session.flush()
303
+ except Exception:
304
+ await session.rollback()
305
+ raise
306
+
307
+ val = getattr(obj, field_name)
308
+ secret = _get_secret_key(request) or ""
309
+ csrf_token = generate_csrf_token(secret)
310
+ admin_path = request.app.state.admin_config["admin_path"]
311
+
312
+ if val:
313
+ toggle_html = (
314
+ f'<button class="toggle-switch toggle-switch--on"'
315
+ f' hx-patch="{admin_path}/{registered.table_name}/{obj.id}/field"'
316
+ f' hx-vals=\'{{"field": "{field_name}", "value": "false"}}\''
317
+ f' hx-target="this" hx-swap="outerHTML"'
318
+ f' hx-headers=\'{{"X-CSRF-Token": "{csrf_token}"}}\''
319
+ f' role="checkbox" aria-checked="true">'
320
+ f'<span class="toggle-switch__thumb"></span></button>'
321
+ )
322
+ else:
323
+ toggle_html = (
324
+ f'<button class="toggle-switch"'
325
+ f' hx-patch="{admin_path}/{registered.table_name}/{obj.id}/field"'
326
+ f' hx-vals=\'{{"field": "{field_name}", "value": "true"}}\''
327
+ f' hx-target="this" hx-swap="outerHTML"'
328
+ f' hx-headers=\'{{"X-CSRF-Token": "{csrf_token}"}}\''
329
+ f' role="checkbox" aria-checked="false">'
330
+ f'<span class="toggle-switch__thumb"></span></button>'
331
+ )
332
+ from fastapi.responses import HTMLResponse
333
+ return HTMLResponse(content=toggle_html)
334
+
335
+ return router