sqladmin 0.16.1__tar.gz → 0.18.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 (52) hide show
  1. {sqladmin-0.16.1 → sqladmin-0.18.0}/PKG-INFO +2 -2
  2. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/__init__.py +1 -1
  3. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/_menu.py +11 -9
  4. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/_queries.py +10 -8
  5. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/ajax.py +5 -3
  6. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/application.py +54 -43
  7. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/authentication.py +4 -2
  8. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/fields.py +35 -33
  9. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/forms.py +56 -59
  10. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/helpers.py +13 -12
  11. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/models.py +98 -20
  12. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/pagination.py +5 -3
  13. {sqladmin-0.16.1/sqladmin/templates → sqladmin-0.18.0/sqladmin/templates/sqladmin}/_macros.html +33 -0
  14. {sqladmin-0.16.1/sqladmin/templates → sqladmin-0.18.0/sqladmin/templates/sqladmin}/create.html +3 -16
  15. {sqladmin-0.16.1/sqladmin/templates → sqladmin-0.18.0/sqladmin/templates/sqladmin}/details.html +3 -3
  16. {sqladmin-0.16.1/sqladmin/templates → sqladmin-0.18.0/sqladmin/templates/sqladmin}/edit.html +5 -16
  17. {sqladmin-0.16.1/sqladmin/templates → sqladmin-0.18.0/sqladmin/templates/sqladmin}/error.html +1 -1
  18. sqladmin-0.18.0/sqladmin/templates/sqladmin/index.html +3 -0
  19. {sqladmin-0.16.1/sqladmin/templates → sqladmin-0.18.0/sqladmin/templates/sqladmin}/layout.html +2 -2
  20. {sqladmin-0.16.1/sqladmin/templates → sqladmin-0.18.0/sqladmin/templates/sqladmin}/list.html +6 -6
  21. {sqladmin-0.16.1/sqladmin/templates → sqladmin-0.18.0/sqladmin/templates/sqladmin}/login.html +1 -1
  22. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/templating.py +9 -7
  23. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/widgets.py +20 -12
  24. sqladmin-0.16.1/sqladmin/templates/index.html +0 -3
  25. {sqladmin-0.16.1 → sqladmin-0.18.0}/.gitignore +0 -0
  26. {sqladmin-0.16.1 → sqladmin-0.18.0}/LICENSE.md +0 -0
  27. {sqladmin-0.16.1 → sqladmin-0.18.0}/README.md +0 -0
  28. {sqladmin-0.16.1 → sqladmin-0.18.0}/pyproject.toml +0 -0
  29. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/_types.py +0 -0
  30. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/_validators.py +0 -0
  31. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/exceptions.py +0 -0
  32. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/formatters.py +0 -0
  33. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/py.typed +0 -0
  34. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/statics/css/flatpickr.min.css +0 -0
  35. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/statics/css/fontawesome.min.css +0 -0
  36. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/statics/css/main.css +0 -0
  37. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/statics/css/select2.min.css +0 -0
  38. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/statics/css/tabler.min.css +0 -0
  39. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/statics/js/bootstrap.min.js +0 -0
  40. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/statics/js/flatpickr.min.js +0 -0
  41. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/statics/js/jquery.min.js +0 -0
  42. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/statics/js/main.js +0 -0
  43. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/statics/js/popper.min.js +0 -0
  44. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/statics/js/select2.full.min.js +0 -0
  45. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/statics/js/tabler.min.js +0 -0
  46. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/statics/webfonts/fa-brands-400.woff2 +0 -0
  47. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/statics/webfonts/fa-regular-400.woff2 +0 -0
  48. {sqladmin-0.16.1 → sqladmin-0.18.0}/sqladmin/statics/webfonts/fa-solid-900.woff2 +0 -0
  49. {sqladmin-0.16.1/sqladmin/templates → sqladmin-0.18.0/sqladmin/templates/sqladmin}/base.html +0 -0
  50. {sqladmin-0.16.1/sqladmin/templates → sqladmin-0.18.0/sqladmin/templates/sqladmin}/modals/delete.html +0 -0
  51. {sqladmin-0.16.1/sqladmin/templates → sqladmin-0.18.0/sqladmin/templates/sqladmin}/modals/details_action_confirmation.html +0 -0
  52. {sqladmin-0.16.1/sqladmin/templates → sqladmin-0.18.0/sqladmin/templates/sqladmin}/modals/list_action_confirmation.html +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: sqladmin
3
- Version: 0.16.1
3
+ Version: 0.18.0
4
4
  Summary: SQLAlchemy admin for FastAPI and Starlette
5
5
  Project-URL: Documentation, https://aminalaee.dev/sqladmin
6
6
  Project-URL: Issues, https://github.com/aminalaee/sqladmin/issues
@@ -1,7 +1,7 @@
1
1
  from sqladmin.application import Admin, action, expose
2
2
  from sqladmin.models import BaseView, ModelView
3
3
 
4
- __version__ = "0.16.1"
4
+ __version__ = "0.18.0"
5
5
 
6
6
  __all__ = [
7
7
  "Admin",
@@ -1,4 +1,6 @@
1
- from typing import TYPE_CHECKING, List, Optional, Union
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
2
4
 
3
5
  from starlette.datastructures import URL
4
6
  from starlette.requests import Request
@@ -8,11 +10,11 @@ if TYPE_CHECKING:
8
10
 
9
11
 
10
12
  class ItemMenu:
11
- def __init__(self, name: str, icon: Optional[str] = None) -> None:
13
+ def __init__(self, name: str, icon: str | None = None) -> None:
12
14
  self.name = name
13
15
  self.icon = icon
14
- self.parent: Optional["ItemMenu"] = None
15
- self.children: List["ItemMenu"] = []
16
+ self.parent: "ItemMenu" | None = None
17
+ self.children: list["ItemMenu"] = []
16
18
 
17
19
  def add_child(self, item: "ItemMenu") -> None:
18
20
  item.parent = self
@@ -27,7 +29,7 @@ class ItemMenu:
27
29
  def is_active(self, request: Request) -> bool:
28
30
  return False
29
31
 
30
- def url(self, request: Request) -> Union[str, URL]:
32
+ def url(self, request: Request) -> str | URL:
31
33
  return "#"
32
34
 
33
35
  @property
@@ -53,9 +55,9 @@ class CategoryMenu(ItemMenu):
53
55
  class ViewMenu(ItemMenu):
54
56
  def __init__(
55
57
  self,
56
- view: Union["BaseView", "ModelView"],
58
+ view: "BaseView" | "ModelView",
57
59
  name: str,
58
- icon: Optional[str] = None,
60
+ icon: str | None = None,
59
61
  ) -> None:
60
62
  super().__init__(name=name, icon=icon)
61
63
  self.view = view
@@ -69,7 +71,7 @@ class ViewMenu(ItemMenu):
69
71
  def is_active(self, request: Request) -> bool:
70
72
  return self.view.identity == request.path_params.get("identity")
71
73
 
72
- def url(self, request: Request) -> Union[str, URL]:
74
+ def url(self, request: Request) -> str | URL:
73
75
  if self.view.is_model:
74
76
  return request.url_for("admin:list", identity=self.view.identity)
75
77
  return request.url_for(f"admin:{self.view.identity}")
@@ -85,7 +87,7 @@ class ViewMenu(ItemMenu):
85
87
 
86
88
  class Menu:
87
89
  def __init__(self) -> None:
88
- self.items: List[ItemMenu] = []
90
+ self.items: list[ItemMenu] = []
89
91
 
90
92
  def add(self, item: ItemMenu) -> None:
91
93
  # Only works for one-level menu
@@ -1,9 +1,11 @@
1
- from typing import TYPE_CHECKING, Any, Dict, List
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
2
4
 
3
5
  import anyio
4
6
  from sqlalchemy import select
5
7
  from sqlalchemy.ext.asyncio import AsyncSession
6
- from sqlalchemy.orm import Session, joinedload
8
+ from sqlalchemy.orm import Session, selectinload
7
9
  from sqlalchemy.sql.expression import Select, and_, or_
8
10
  from starlette.requests import Request
9
11
 
@@ -24,7 +26,7 @@ class Query:
24
26
  def __init__(self, model_view: "ModelView") -> None:
25
27
  self.model_view = model_view
26
28
 
27
- def _get_to_many_stmt(self, relation: MODEL_PROPERTY, values: List[Any]) -> Select:
29
+ def _get_to_many_stmt(self, relation: MODEL_PROPERTY, values: list[Any]) -> Select:
28
30
  target = relation.mapper.class_
29
31
 
30
32
  target_pks = get_primary_keys(target)
@@ -131,7 +133,7 @@ class Query:
131
133
  setattr(obj, key, value)
132
134
  return obj
133
135
 
134
- def _update_sync(self, pk: Any, data: Dict[str, Any], request: Request) -> Any:
136
+ def _update_sync(self, pk: Any, data: dict[str, Any], request: Request) -> Any:
135
137
  stmt = self.model_view._stmt_by_identifier(pk)
136
138
 
137
139
  with self.model_view.session_maker(expire_on_commit=False) as session:
@@ -147,12 +149,12 @@ class Query:
147
149
  return obj
148
150
 
149
151
  async def _update_async(
150
- self, pk: Any, data: Dict[str, Any], request: Request
152
+ self, pk: Any, data: dict[str, Any], request: Request
151
153
  ) -> Any:
152
154
  stmt = self.model_view._stmt_by_identifier(pk)
153
155
 
154
156
  for relation in self.model_view._form_relations:
155
- stmt = stmt.options(joinedload(relation))
157
+ stmt = stmt.options(selectinload(relation))
156
158
 
157
159
  async with self.model_view.session_maker(expire_on_commit=False) as session:
158
160
  result = await session.execute(stmt)
@@ -187,7 +189,7 @@ class Query:
187
189
  await session.commit()
188
190
  await self.model_view.after_model_delete(obj, request)
189
191
 
190
- def _insert_sync(self, data: Dict[str, Any], request: Request) -> Any:
192
+ def _insert_sync(self, data: dict[str, Any], request: Request) -> Any:
191
193
  obj = self.model_view.model()
192
194
 
193
195
  with self.model_view.session_maker(expire_on_commit=False) as session:
@@ -202,7 +204,7 @@ class Query:
202
204
  )
203
205
  return obj
204
206
 
205
- async def _insert_async(self, data: Dict[str, Any], request: Request) -> Any:
207
+ async def _insert_async(self, data: dict[str, Any], request: Request) -> Any:
206
208
  obj = self.model_view.model()
207
209
 
208
210
  async with self.model_view.session_maker(expire_on_commit=False) as session:
@@ -1,4 +1,6 @@
1
- from typing import TYPE_CHECKING, Any, Dict, List
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
2
4
 
3
5
  from sqlalchemy import String, cast, inspect, or_, select
4
6
 
@@ -52,13 +54,13 @@ class QueryAjaxModelLoader:
52
54
 
53
55
  return remote_fields
54
56
 
55
- def format(self, model: type) -> Dict[str, Any]:
57
+ def format(self, model: type) -> dict[str, Any]:
56
58
  if not model:
57
59
  return {}
58
60
 
59
61
  return {"id": str(get_object_identifier(model)), "text": str(model)}
60
62
 
61
- async def get_list(self, term: str, limit: int = DEFAULT_PAGE_SIZE) -> List[Any]:
63
+ async def get_list(self, term: str, limit: int = DEFAULT_PAGE_SIZE) -> list[Any]:
62
64
  stmt = select(self.model)
63
65
 
64
66
  # no type casting to string if a ColumnAssociationProxyInstance is given
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import inspect
2
4
  import io
3
5
  import logging
@@ -7,23 +9,18 @@ from typing import (
7
9
  Any,
8
10
  Awaitable,
9
11
  Callable,
10
- List,
11
- Optional,
12
12
  Sequence,
13
- Tuple,
14
- Type,
15
- Union,
16
13
  cast,
17
14
  no_type_check,
18
15
  )
19
- from urllib.parse import urljoin
16
+ from urllib.parse import parse_qsl, urljoin
20
17
 
21
18
  from jinja2 import ChoiceLoader, FileSystemLoader, PackageLoader
22
19
  from sqlalchemy.engine import Engine
23
20
  from sqlalchemy.ext.asyncio import AsyncSession
24
21
  from sqlalchemy.orm import Session, sessionmaker
25
22
  from starlette.applications import Starlette
26
- from starlette.datastructures import URL, FormData, UploadFile
23
+ from starlette.datastructures import URL, FormData, MultiDict, UploadFile
27
24
  from starlette.exceptions import HTTPException
28
25
  from starlette.middleware import Middleware
29
26
  from starlette.requests import Request
@@ -66,14 +63,14 @@ class BaseAdmin:
66
63
  def __init__(
67
64
  self,
68
65
  app: Starlette,
69
- engine: Optional[ENGINE_TYPE] = None,
70
- session_maker: Optional[sessionmaker] = None,
66
+ engine: ENGINE_TYPE | None = None,
67
+ session_maker: sessionmaker | None = None,
71
68
  base_url: str = "/admin",
72
69
  title: str = "Admin",
73
- logo_url: Optional[str] = None,
70
+ logo_url: str | None = None,
74
71
  templates_dir: str = "templates",
75
- middlewares: Optional[Sequence[Middleware]] = None,
76
- authentication_backend: Optional[AuthenticationBackend] = None,
72
+ middlewares: Sequence[Middleware] | None = None,
73
+ authentication_backend: AuthenticationBackend | None = None,
77
74
  ) -> None:
78
75
  self.app = app
79
76
  self.engine = engine
@@ -100,7 +97,7 @@ class BaseAdmin:
100
97
 
101
98
  self.admin = Starlette(middleware=middlewares)
102
99
  self.templates = self.init_templating_engine()
103
- self._views: List[Union[BaseView, ModelView]] = []
100
+ self._views: list[BaseView | ModelView] = []
104
101
  self._menu = Menu()
105
102
 
106
103
  def init_templating_engine(self) -> Jinja2Templates:
@@ -120,7 +117,7 @@ class BaseAdmin:
120
117
  return templates
121
118
 
122
119
  @property
123
- def views(self) -> List[Union[BaseView, ModelView]]:
120
+ def views(self) -> list[BaseView | ModelView]:
124
121
  """Get list of ModelView and BaseView instances lazily.
125
122
 
126
123
  Returns:
@@ -136,7 +133,7 @@ class BaseAdmin:
136
133
 
137
134
  raise HTTPException(status_code=404)
138
135
 
139
- def add_view(self, view: Union[Type[ModelView], Type[BaseView]]) -> None:
136
+ def add_view(self, view: type[ModelView] | type[BaseView]) -> None:
140
137
  """Add ModelView or BaseView classes to Admin.
141
138
  This is a shortcut that will handle both `add_model_view` and `add_base_view`.
142
139
  """
@@ -149,10 +146,10 @@ class BaseAdmin:
149
146
 
150
147
  def _find_decorated_funcs(
151
148
  self,
152
- view: Type[Union[BaseView, ModelView]],
153
- view_instance: Union[BaseView, ModelView],
149
+ view: type[BaseView | ModelView],
150
+ view_instance: BaseView | ModelView,
154
151
  handle_fn: Callable[
155
- [MethodType, Type[Union[BaseView, ModelView]], Union[BaseView, ModelView]],
152
+ [MethodType, type[BaseView | ModelView], BaseView | ModelView],
156
153
  None,
157
154
  ],
158
155
  ) -> None:
@@ -164,8 +161,8 @@ class BaseAdmin:
164
161
  def _handle_action_decorated_func(
165
162
  self,
166
163
  func: MethodType,
167
- view: Type[Union[BaseView, ModelView]],
168
- view_instance: Union[BaseView, ModelView],
164
+ view: type[BaseView | ModelView],
165
+ view_instance: BaseView | ModelView,
169
166
  ) -> None:
170
167
  if hasattr(func, "_action"):
171
168
  view_instance = cast(ModelView, view_instance)
@@ -194,8 +191,8 @@ class BaseAdmin:
194
191
  def _handle_expose_decorated_func(
195
192
  self,
196
193
  func: MethodType,
197
- view: Type[Union[BaseView, ModelView]],
198
- view_instance: Union[BaseView, ModelView],
194
+ view: type[BaseView | ModelView],
195
+ view_instance: BaseView | ModelView,
199
196
  ) -> None:
200
197
  if hasattr(func, "_exposed"):
201
198
  self.admin.add_route(
@@ -208,7 +205,7 @@ class BaseAdmin:
208
205
 
209
206
  view.identity = getattr(func, "_identity")
210
207
 
211
- def add_model_view(self, view: Type[ModelView]) -> None:
208
+ def add_model_view(self, view: type[ModelView]) -> None:
212
209
  """Add ModelView to the Admin.
213
210
 
214
211
  ???+ usage
@@ -237,7 +234,7 @@ class BaseAdmin:
237
234
  self._views.append(view_instance)
238
235
  self._build_menu(view_instance)
239
236
 
240
- def add_base_view(self, view: Type[BaseView]) -> None:
237
+ def add_base_view(self, view: type[BaseView]) -> None:
241
238
  """Add BaseView to the Admin.
242
239
 
243
240
  ???+ usage
@@ -265,7 +262,7 @@ class BaseAdmin:
265
262
  self._views.append(view_instance)
266
263
  self._build_menu(view_instance)
267
264
 
268
- def _build_menu(self, view: Union[ModelView, BaseView]) -> None:
265
+ def _build_menu(self, view: ModelView | BaseView) -> None:
269
266
  if view.category:
270
267
  menu = CategoryMenu(name=view.category)
271
268
  menu.add_child(ViewMenu(view=view, name=view.name, icon=view.icon))
@@ -338,15 +335,15 @@ class Admin(BaseAdminView):
338
335
  def __init__(
339
336
  self,
340
337
  app: Starlette,
341
- engine: Optional[ENGINE_TYPE] = None,
342
- session_maker: Optional[Union[sessionmaker, "async_sessionmaker"]] = None,
338
+ engine: ENGINE_TYPE | None = None,
339
+ session_maker: sessionmaker | "async_sessionmaker" | None = None,
343
340
  base_url: str = "/admin",
344
341
  title: str = "Admin",
345
- logo_url: Optional[str] = None,
346
- middlewares: Optional[Sequence[Middleware]] = None,
342
+ logo_url: str | None = None,
343
+ middlewares: Sequence[Middleware] | None = None,
347
344
  debug: bool = False,
348
345
  templates_dir: str = "templates",
349
- authentication_backend: Optional[AuthenticationBackend] = None,
346
+ authentication_backend: AuthenticationBackend | None = None,
350
347
  ) -> None:
351
348
  """
352
349
  Args:
@@ -374,14 +371,14 @@ class Admin(BaseAdminView):
374
371
 
375
372
  async def http_exception(
376
373
  request: Request, exc: Exception
377
- ) -> Union[Response, Awaitable[Response]]:
374
+ ) -> Response | Awaitable[Response]:
378
375
  assert isinstance(exc, HTTPException)
379
376
  context = {
380
377
  "status_code": exc.status_code,
381
378
  "message": exc.detail,
382
379
  }
383
380
  return await self.templates.TemplateResponse(
384
- request, "error.html", context, status_code=exc.status_code
381
+ request, "sqladmin/error.html", context, status_code=exc.status_code
385
382
  )
386
383
 
387
384
  routes = [
@@ -428,7 +425,7 @@ class Admin(BaseAdminView):
428
425
  async def index(self, request: Request) -> Response:
429
426
  """Index route which can be overridden to create dashboards."""
430
427
 
431
- return await self.templates.TemplateResponse(request, "index.html")
428
+ return await self.templates.TemplateResponse(request, "sqladmin/index.html")
432
429
 
433
430
  @login_required
434
431
  async def list(self, request: Request) -> Response:
@@ -440,6 +437,14 @@ class Admin(BaseAdminView):
440
437
  pagination = await model_view.list(request)
441
438
  pagination.add_pagination_urls(request.url)
442
439
 
440
+ if (
441
+ pagination.page * pagination.page_size
442
+ > pagination.count + pagination.page_size
443
+ ):
444
+ raise HTTPException(
445
+ status_code=400, detail="Invalid page or pageSize parameter"
446
+ )
447
+
443
448
  context = {"model_view": model_view, "pagination": pagination}
444
449
  return await self.templates.TemplateResponse(
445
450
  request, model_view.list_template, context
@@ -485,7 +490,11 @@ class Admin(BaseAdminView):
485
490
 
486
491
  await model_view.delete_model(request, pk)
487
492
 
488
- return Response(content=str(request.url_for("admin:list", identity=identity)))
493
+ referer_url = URL(request.headers.get("referer", ""))
494
+ referer_params = MultiDict(parse_qsl(referer_url.query))
495
+ url = URL(str(request.url_for("admin:list", identity=identity)))
496
+ url = url.include_query_params(**referer_params)
497
+ return Response(content=str(url))
489
498
 
490
499
  @login_required
491
500
  async def create(self, request: Request) -> Response:
@@ -497,6 +506,7 @@ class Admin(BaseAdminView):
497
506
  model_view = self._find_model_view(identity)
498
507
 
499
508
  Form = await model_view.scaffold_form()
509
+ model_view._validate_form_class(model_view._form_create_rules, Form)
500
510
  form_data = await self._handle_form_data(request)
501
511
  form = Form(form_data)
502
512
 
@@ -542,11 +552,12 @@ class Admin(BaseAdminView):
542
552
  identity = request.path_params["identity"]
543
553
  model_view = self._find_model_view(identity)
544
554
 
545
- model = await model_view.get_object_for_edit(request.path_params["pk"])
555
+ model = await model_view.get_object_for_edit(request)
546
556
  if not model:
547
557
  raise HTTPException(status_code=404)
548
558
 
549
559
  Form = await model_view.scaffold_form()
560
+ model_view._validate_form_class(model_view._form_edit_rules, Form)
550
561
  context = {
551
562
  "obj": model,
552
563
  "model_view": model_view,
@@ -609,13 +620,13 @@ class Admin(BaseAdminView):
609
620
 
610
621
  context = {}
611
622
  if request.method == "GET":
612
- return await self.templates.TemplateResponse(request, "login.html")
623
+ return await self.templates.TemplateResponse(request, "sqladmin/login.html")
613
624
 
614
625
  ok = await self.authentication_backend.login(request)
615
626
  if not ok:
616
627
  context["error"] = "Invalid credentials."
617
628
  return await self.templates.TemplateResponse(
618
- request, "login.html", context, status_code=400
629
+ request, "sqladmin/login.html", context, status_code=400
619
630
  )
620
631
 
621
632
  return RedirectResponse(request.url_for("admin:index"), status_code=302)
@@ -648,7 +659,7 @@ class Admin(BaseAdminView):
648
659
 
649
660
  def get_save_redirect_url(
650
661
  self, request: Request, form: FormData, model_view: ModelView, obj: Any
651
- ) -> Union[str, URL]:
662
+ ) -> str | URL:
652
663
  """
653
664
  Get the redirect URL after a save action
654
665
  which is triggered from create/edit page.
@@ -673,7 +684,7 @@ class Admin(BaseAdminView):
673
684
  """
674
685
 
675
686
  form = await request.form()
676
- form_data: List[Tuple[str, Union[str, UploadFile]]] = []
687
+ form_data: list[tuple[str, str | UploadFile]] = []
677
688
  for key, value in form.multi_items():
678
689
  if not isinstance(value, UploadFile):
679
690
  form_data.append((key, value))
@@ -714,8 +725,8 @@ class Admin(BaseAdminView):
714
725
  def expose(
715
726
  path: str,
716
727
  *,
717
- methods: List[str] = ["GET"],
718
- identity: Optional[str] = None,
728
+ methods: list[str] = ["GET"],
729
+ identity: str | None = None,
719
730
  include_in_schema: bool = True,
720
731
  ) -> Callable[..., Any]:
721
732
  """Expose View with information."""
@@ -734,8 +745,8 @@ def expose(
734
745
 
735
746
  def action(
736
747
  name: str,
737
- label: Optional[str] = None,
738
- confirmation_message: Optional[str] = None,
748
+ label: str | None = None,
749
+ confirmation_message: str | None = None,
739
750
  *,
740
751
  include_in_schema: bool = True,
741
752
  add_in_detail: bool = True,
@@ -1,6 +1,8 @@
1
+ from __future__ import annotations
2
+
1
3
  import functools
2
4
  import inspect
3
- from typing import Any, Callable, Union
5
+ from typing import Any, Callable
4
6
 
5
7
  from starlette.middleware import Middleware
6
8
  from starlette.requests import Request
@@ -33,7 +35,7 @@ class AuthenticationBackend:
33
35
  """
34
36
  raise NotImplementedError()
35
37
 
36
- async def authenticate(self, request: Request) -> Union[Response, bool]:
38
+ async def authenticate(self, request: Request) -> Response | bool:
37
39
  """Implement authenticate logic here.
38
40
  This method will be called for each incoming request
39
41
  to validate the authentication.
@@ -1,6 +1,8 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
2
4
  import operator
3
- from typing import Any, Callable, Dict, Generator, List, Optional, Set, Tuple, Union
5
+ from typing import Any, Callable, Generator
4
6
 
5
7
  from wtforms import Form, ValidationError, fields, widgets
6
8
 
@@ -43,7 +45,7 @@ class IntervalField(fields.StringField):
43
45
  A text field which stores a `datetime.timedelta` object.
44
46
  """
45
47
 
46
- def process_formdata(self, valuelist: List[str]) -> None:
48
+ def process_formdata(self, valuelist: list[str]) -> None:
47
49
  if not valuelist:
48
50
  return
49
51
 
@@ -57,19 +59,19 @@ class IntervalField(fields.StringField):
57
59
  class SelectField(fields.SelectField):
58
60
  def __init__(
59
61
  self,
60
- label: Optional[str] = None,
61
- validators: Optional[list] = None,
62
+ label: str | None = None,
63
+ validators: list | None = None,
62
64
  coerce: type = str,
63
- choices: Optional[Union[list, Callable]] = None,
65
+ choices: list | Callable | None = None,
64
66
  allow_blank: bool = False,
65
- blank_text: Optional[str] = None,
67
+ blank_text: str | None = None,
66
68
  **kwargs: Any,
67
69
  ) -> None:
68
70
  super().__init__(label, validators, coerce, choices, **kwargs)
69
71
  self.allow_blank = allow_blank
70
72
  self.blank_text = blank_text or " "
71
73
 
72
- def iter_choices(self) -> Generator[Tuple[str, str, bool, Dict], None, None]:
74
+ def iter_choices(self) -> Generator[tuple[str, str, bool, dict], None, None]:
73
75
  choices = self.choices or []
74
76
 
75
77
  if self.allow_blank:
@@ -86,7 +88,7 @@ class SelectField(fields.SelectField):
86
88
  {},
87
89
  )
88
90
 
89
- def process_formdata(self, valuelist: List[str]) -> None:
91
+ def process_formdata(self, valuelist: list[str]) -> None:
90
92
  if valuelist:
91
93
  if valuelist[0] == "__None":
92
94
  self.data = None
@@ -112,7 +114,7 @@ class JSONField(fields.TextAreaField):
112
114
  else:
113
115
  return "{}"
114
116
 
115
- def process_formdata(self, valuelist: List[str]) -> None:
117
+ def process_formdata(self, valuelist: list[str]) -> None:
116
118
  if valuelist:
117
119
  value = valuelist[0]
118
120
 
@@ -132,10 +134,10 @@ class QuerySelectField(fields.SelectFieldBase):
132
134
 
133
135
  def __init__(
134
136
  self,
135
- data: Optional[list] = None,
136
- label: Optional[str] = None,
137
- validators: Optional[list] = None,
138
- get_label: Optional[Union[Callable, str]] = None,
137
+ data: list | None = None,
138
+ label: str | None = None,
139
+ validators: list | None = None,
140
+ get_label: Callable | str | None = None,
139
141
  allow_blank: bool = False,
140
142
  blank_text: str = "",
141
143
  **kwargs: Any,
@@ -153,11 +155,11 @@ class QuerySelectField(fields.SelectFieldBase):
153
155
 
154
156
  self.allow_blank = allow_blank
155
157
  self.blank_text = blank_text
156
- self._data: Optional[tuple]
157
- self._formdata: Optional[Union[str, List[str]]]
158
+ self._data: tuple | None
159
+ self._formdata: str | list[str] | None
158
160
 
159
161
  @property
160
- def data(self) -> Optional[tuple]:
162
+ def data(self) -> tuple | None:
161
163
  if self._formdata is not None:
162
164
  for pk, _ in self._select_data:
163
165
  if pk == self._formdata:
@@ -170,7 +172,7 @@ class QuerySelectField(fields.SelectFieldBase):
170
172
  self._data = data
171
173
  self._formdata = None
172
174
 
173
- def iter_choices(self) -> Generator[Tuple[str, str, bool, Dict], None, None]:
175
+ def iter_choices(self) -> Generator[tuple[str, str, bool, dict], None, None]:
174
176
  if self.allow_blank:
175
177
  yield ("__None", self.blank_text, self.data is None, {})
176
178
 
@@ -186,7 +188,7 @@ class QuerySelectField(fields.SelectFieldBase):
186
188
  for pk, label in self._select_data:
187
189
  yield (pk, self.get_label(label), str(pk) == primary_key, {})
188
190
 
189
- def process_formdata(self, valuelist: List[str]) -> None:
191
+ def process_formdata(self, valuelist: list[str]) -> None:
190
192
  if valuelist:
191
193
  if self.allow_blank and valuelist[0] == "__None":
192
194
  self.data = None
@@ -220,9 +222,9 @@ class QuerySelectMultipleField(QuerySelectField):
220
222
 
221
223
  def __init__(
222
224
  self,
223
- data: Optional[list] = None,
224
- label: Optional[str] = None,
225
- validators: Optional[list] = None,
225
+ data: list | None = None,
226
+ label: str | None = None,
227
+ validators: list | None = None,
226
228
  default: Any = None,
227
229
  **kwargs: Any,
228
230
  ) -> None:
@@ -238,11 +240,11 @@ class QuerySelectMultipleField(QuerySelectField):
238
240
  "allow_blank=True does not do anything for QuerySelectMultipleField."
239
241
  )
240
242
  self._invalid_formdata = False
241
- self._formdata: Optional[List[str]] = None
242
- self._data: Optional[tuple] = None
243
+ self._formdata: list[str] | None = None
244
+ self._data: tuple | None = None
243
245
 
244
246
  @property
245
- def data(self) -> Optional[tuple]:
247
+ def data(self) -> tuple | None:
246
248
  formdata = self._formdata
247
249
  if formdata is not None:
248
250
  data = []
@@ -262,7 +264,7 @@ class QuerySelectMultipleField(QuerySelectField):
262
264
  self._data = data
263
265
  self._formdata = None
264
266
 
265
- def iter_choices(self) -> Generator[Tuple[str, Any, bool, Dict], None, None]:
267
+ def iter_choices(self) -> Generator[tuple[str, Any, bool, dict], None, None]:
266
268
  if self.data is not None:
267
269
  primary_keys = (
268
270
  self.data
@@ -272,7 +274,7 @@ class QuerySelectMultipleField(QuerySelectField):
272
274
  for pk, label in self._select_data:
273
275
  yield (pk, self.get_label(label), pk in primary_keys, {})
274
276
 
275
- def process_formdata(self, valuelist: List[str]) -> None:
277
+ def process_formdata(self, valuelist: list[str]) -> None:
276
278
  self._formdata = list(set(valuelist))
277
279
 
278
280
  def pre_validate(self, form: Form) -> None:
@@ -292,8 +294,8 @@ class AjaxSelectField(fields.SelectFieldBase):
292
294
  def __init__(
293
295
  self,
294
296
  loader: QueryAjaxModelLoader,
295
- label: Optional[str] = None,
296
- validators: Optional[list] = None,
297
+ label: str | None = None,
298
+ validators: list | None = None,
297
299
  allow_blank: bool = False,
298
300
  **kwargs: Any,
299
301
  ) -> None:
@@ -334,9 +336,9 @@ class AjaxSelectMultipleField(fields.SelectFieldBase):
334
336
  def __init__(
335
337
  self,
336
338
  loader: QueryAjaxModelLoader,
337
- label: Optional[str] = None,
338
- validators: Optional[list] = None,
339
- default: Optional[list] = None,
339
+ label: str | None = None,
340
+ validators: list | None = None,
341
+ default: list | None = None,
340
342
  allow_blank: bool = False,
341
343
  **kwargs: Any,
342
344
  ) -> None:
@@ -344,7 +346,7 @@ class AjaxSelectMultipleField(fields.SelectFieldBase):
344
346
  self.loader = loader
345
347
  self.allow_blank = allow_blank
346
348
  default = default or []
347
- self._formdata: Set[Any] = set()
349
+ self._formdata: set[Any] = set()
348
350
 
349
351
  super().__init__(label, validators, default=default, **kwargs)
350
352
 
@@ -377,7 +379,7 @@ class Select2TagsField(fields.SelectField):
377
379
  def process_formdata(self, valuelist: list) -> None:
378
380
  self.data = valuelist
379
381
 
380
- def process_data(self, value: Optional[list]) -> None:
382
+ def process_data(self, value: list | None) -> None:
381
383
  self.data = value or []
382
384
 
383
385