sqladmin 0.17.0__tar.gz → 0.19.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 (54) hide show
  1. {sqladmin-0.17.0 → sqladmin-0.19.0}/PKG-INFO +1 -1
  2. {sqladmin-0.17.0 → sqladmin-0.19.0}/pyproject.toml +1 -1
  3. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/__init__.py +1 -1
  4. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/_menu.py +11 -9
  5. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/_queries.py +8 -6
  6. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/ajax.py +12 -5
  7. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/application.py +39 -35
  8. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/authentication.py +4 -2
  9. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/fields.py +35 -33
  10. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/forms.py +66 -61
  11. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/helpers.py +8 -10
  12. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/models.py +60 -2
  13. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/pagination.py +10 -3
  14. sqladmin-0.19.0/sqladmin/statics/css/tabler-icons.min.css +4 -0
  15. sqladmin-0.19.0/sqladmin/statics/css/tabler-icons.min.css.map +1 -0
  16. sqladmin-0.19.0/sqladmin/statics/webfonts/tabler-icons.woff2 +0 -0
  17. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/templates/sqladmin/_macros.html +33 -0
  18. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/templates/sqladmin/base.html +4 -0
  19. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/templates/sqladmin/create.html +2 -18
  20. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/templates/sqladmin/edit.html +8 -24
  21. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/templates/sqladmin/list.html +1 -1
  22. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/templating.py +9 -7
  23. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/widgets.py +4 -1
  24. {sqladmin-0.17.0 → sqladmin-0.19.0}/.gitignore +0 -0
  25. {sqladmin-0.17.0 → sqladmin-0.19.0}/LICENSE.md +0 -0
  26. {sqladmin-0.17.0 → sqladmin-0.19.0}/README.md +0 -0
  27. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/_types.py +0 -0
  28. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/_validators.py +0 -0
  29. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/exceptions.py +0 -0
  30. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/formatters.py +0 -0
  31. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/py.typed +0 -0
  32. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/statics/css/flatpickr.min.css +0 -0
  33. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/statics/css/fontawesome.min.css +0 -0
  34. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/statics/css/main.css +0 -0
  35. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/statics/css/select2.min.css +0 -0
  36. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/statics/css/tabler.min.css +0 -0
  37. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/statics/js/bootstrap.min.js +0 -0
  38. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/statics/js/flatpickr.min.js +0 -0
  39. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/statics/js/jquery.min.js +0 -0
  40. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/statics/js/main.js +0 -0
  41. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/statics/js/popper.min.js +0 -0
  42. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/statics/js/select2.full.min.js +0 -0
  43. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/statics/js/tabler.min.js +0 -0
  44. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/statics/webfonts/fa-brands-400.woff2 +0 -0
  45. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/statics/webfonts/fa-regular-400.woff2 +0 -0
  46. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/statics/webfonts/fa-solid-900.woff2 +0 -0
  47. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/templates/sqladmin/details.html +0 -0
  48. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/templates/sqladmin/error.html +0 -0
  49. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/templates/sqladmin/index.html +0 -0
  50. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/templates/sqladmin/layout.html +0 -0
  51. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/templates/sqladmin/login.html +0 -0
  52. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/templates/sqladmin/modals/delete.html +0 -0
  53. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/templates/sqladmin/modals/details_action_confirmation.html +0 -0
  54. {sqladmin-0.17.0 → sqladmin-0.19.0}/sqladmin/templates/sqladmin/modals/list_action_confirmation.html +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sqladmin
3
- Version: 0.17.0
3
+ Version: 0.19.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
@@ -95,7 +95,7 @@ dependencies = [
95
95
  dependencies = [
96
96
  "mkdocs-material==9.1.3",
97
97
  "mkdocs==1.4.2",
98
- "mkdocstrings[python]==0.20.0",
98
+ "mkdocstrings[python]==0.25.0",
99
99
  ]
100
100
 
101
101
  [tool.hatch.envs.test.scripts]
@@ -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.17.0"
4
+ __version__ = "0.19.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,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
  import anyio
4
6
  from sqlalchemy import select
@@ -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,7 +149,7 @@ 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
 
@@ -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
 
@@ -24,6 +26,7 @@ class QueryAjaxModelLoader:
24
26
  self.model_admin = model_admin
25
27
  self.fields = options.get("fields", {})
26
28
  self.order_by = options.get("order_by")
29
+ self.limit = options.get("limit", DEFAULT_PAGE_SIZE)
27
30
 
28
31
  pks = get_primary_keys(self.model)
29
32
  self.pk = pks[0] if len(pks) == 1 else None
@@ -52,13 +55,13 @@ class QueryAjaxModelLoader:
52
55
 
53
56
  return remote_fields
54
57
 
55
- def format(self, model: type) -> Dict[str, Any]:
58
+ def format(self, model: type) -> dict[str, Any]:
56
59
  if not model:
57
60
  return {}
58
61
 
59
62
  return {"id": str(get_object_identifier(model)), "text": str(model)}
60
63
 
61
- async def get_list(self, term: str, limit: int = DEFAULT_PAGE_SIZE) -> List[Any]:
64
+ async def get_list(self, term: str) -> list[Any]:
62
65
  stmt = select(self.model)
63
66
 
64
67
  # no type casting to string if a ColumnAssociationProxyInstance is given
@@ -69,9 +72,13 @@ class QueryAjaxModelLoader:
69
72
  stmt = stmt.filter(or_(*filters))
70
73
 
71
74
  if self.order_by:
72
- stmt = stmt.order_by(self.order_by)
75
+ if isinstance(self.order_by, list):
76
+ for o in self.order_by:
77
+ stmt = stmt.order_by(o)
78
+ else:
79
+ stmt = stmt.order_by(self.order_by)
73
80
 
74
- stmt = stmt.limit(limit)
81
+ stmt = stmt.limit(self.limit)
75
82
  result = await self.model_admin._run_query(stmt)
76
83
  return result
77
84
 
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import inspect
2
4
  import io
3
5
  import logging
@@ -7,12 +9,7 @@ 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
  )
@@ -66,14 +63,15 @@ 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,
71
+ favicon_url: str | None = None,
74
72
  templates_dir: str = "templates",
75
- middlewares: Optional[Sequence[Middleware]] = None,
76
- authentication_backend: Optional[AuthenticationBackend] = None,
73
+ middlewares: Sequence[Middleware] | None = None,
74
+ authentication_backend: AuthenticationBackend | None = None,
77
75
  ) -> None:
78
76
  self.app = app
79
77
  self.engine = engine
@@ -81,6 +79,7 @@ class BaseAdmin:
81
79
  self.templates_dir = templates_dir
82
80
  self.title = title
83
81
  self.logo_url = logo_url
82
+ self.favicon_url = favicon_url
84
83
 
85
84
  if session_maker:
86
85
  self.session_maker = session_maker
@@ -100,7 +99,7 @@ class BaseAdmin:
100
99
 
101
100
  self.admin = Starlette(middleware=middlewares)
102
101
  self.templates = self.init_templating_engine()
103
- self._views: List[Union[BaseView, ModelView]] = []
102
+ self._views: list[BaseView | ModelView] = []
104
103
  self._menu = Menu()
105
104
 
106
105
  def init_templating_engine(self) -> Jinja2Templates:
@@ -120,7 +119,7 @@ class BaseAdmin:
120
119
  return templates
121
120
 
122
121
  @property
123
- def views(self) -> List[Union[BaseView, ModelView]]:
122
+ def views(self) -> list[BaseView | ModelView]:
124
123
  """Get list of ModelView and BaseView instances lazily.
125
124
 
126
125
  Returns:
@@ -136,7 +135,7 @@ class BaseAdmin:
136
135
 
137
136
  raise HTTPException(status_code=404)
138
137
 
139
- def add_view(self, view: Union[Type[ModelView], Type[BaseView]]) -> None:
138
+ def add_view(self, view: type[ModelView] | type[BaseView]) -> None:
140
139
  """Add ModelView or BaseView classes to Admin.
141
140
  This is a shortcut that will handle both `add_model_view` and `add_base_view`.
142
141
  """
@@ -149,10 +148,10 @@ class BaseAdmin:
149
148
 
150
149
  def _find_decorated_funcs(
151
150
  self,
152
- view: Type[Union[BaseView, ModelView]],
153
- view_instance: Union[BaseView, ModelView],
151
+ view: type[BaseView | ModelView],
152
+ view_instance: BaseView | ModelView,
154
153
  handle_fn: Callable[
155
- [MethodType, Type[Union[BaseView, ModelView]], Union[BaseView, ModelView]],
154
+ [MethodType, type[BaseView | ModelView], BaseView | ModelView],
156
155
  None,
157
156
  ],
158
157
  ) -> None:
@@ -164,8 +163,8 @@ class BaseAdmin:
164
163
  def _handle_action_decorated_func(
165
164
  self,
166
165
  func: MethodType,
167
- view: Type[Union[BaseView, ModelView]],
168
- view_instance: Union[BaseView, ModelView],
166
+ view: type[BaseView | ModelView],
167
+ view_instance: BaseView | ModelView,
169
168
  ) -> None:
170
169
  if hasattr(func, "_action"):
171
170
  view_instance = cast(ModelView, view_instance)
@@ -194,8 +193,8 @@ class BaseAdmin:
194
193
  def _handle_expose_decorated_func(
195
194
  self,
196
195
  func: MethodType,
197
- view: Type[Union[BaseView, ModelView]],
198
- view_instance: Union[BaseView, ModelView],
196
+ view: type[BaseView | ModelView],
197
+ view_instance: BaseView | ModelView,
199
198
  ) -> None:
200
199
  if hasattr(func, "_exposed"):
201
200
  self.admin.add_route(
@@ -208,7 +207,7 @@ class BaseAdmin:
208
207
 
209
208
  view.identity = getattr(func, "_identity")
210
209
 
211
- def add_model_view(self, view: Type[ModelView]) -> None:
210
+ def add_model_view(self, view: type[ModelView]) -> None:
212
211
  """Add ModelView to the Admin.
213
212
 
214
213
  ???+ usage
@@ -237,7 +236,7 @@ class BaseAdmin:
237
236
  self._views.append(view_instance)
238
237
  self._build_menu(view_instance)
239
238
 
240
- def add_base_view(self, view: Type[BaseView]) -> None:
239
+ def add_base_view(self, view: type[BaseView]) -> None:
241
240
  """Add BaseView to the Admin.
242
241
 
243
242
  ???+ usage
@@ -265,7 +264,7 @@ class BaseAdmin:
265
264
  self._views.append(view_instance)
266
265
  self._build_menu(view_instance)
267
266
 
268
- def _build_menu(self, view: Union[ModelView, BaseView]) -> None:
267
+ def _build_menu(self, view: ModelView | BaseView) -> None:
269
268
  if view.category:
270
269
  menu = CategoryMenu(name=view.category)
271
270
  menu.add_child(ViewMenu(view=view, name=view.name, icon=view.icon))
@@ -338,15 +337,16 @@ class Admin(BaseAdminView):
338
337
  def __init__(
339
338
  self,
340
339
  app: Starlette,
341
- engine: Optional[ENGINE_TYPE] = None,
342
- session_maker: Optional[Union[sessionmaker, "async_sessionmaker"]] = None,
340
+ engine: ENGINE_TYPE | None = None,
341
+ session_maker: sessionmaker | "async_sessionmaker" | None = None,
343
342
  base_url: str = "/admin",
344
343
  title: str = "Admin",
345
- logo_url: Optional[str] = None,
346
- middlewares: Optional[Sequence[Middleware]] = None,
344
+ logo_url: str | None = None,
345
+ favicon_url: str | None = None,
346
+ middlewares: Sequence[Middleware] | None = None,
347
347
  debug: bool = False,
348
348
  templates_dir: str = "templates",
349
- authentication_backend: Optional[AuthenticationBackend] = None,
349
+ authentication_backend: AuthenticationBackend | None = None,
350
350
  ) -> None:
351
351
  """
352
352
  Args:
@@ -356,6 +356,7 @@ class Admin(BaseAdminView):
356
356
  base_url: Base URL for Admin interface.
357
357
  title: Admin title.
358
358
  logo_url: URL of logo to be displayed instead of title.
359
+ favicon_url: URL of favicon to be displayed.
359
360
  """
360
361
 
361
362
  super().__init__(
@@ -365,6 +366,7 @@ class Admin(BaseAdminView):
365
366
  base_url=base_url,
366
367
  title=title,
367
368
  logo_url=logo_url,
369
+ favicon_url=favicon_url,
368
370
  templates_dir=templates_dir,
369
371
  middlewares=middlewares,
370
372
  authentication_backend=authentication_backend,
@@ -374,7 +376,7 @@ class Admin(BaseAdminView):
374
376
 
375
377
  async def http_exception(
376
378
  request: Request, exc: Exception
377
- ) -> Union[Response, Awaitable[Response]]:
379
+ ) -> Response | Awaitable[Response]:
378
380
  assert isinstance(exc, HTTPException)
379
381
  context = {
380
382
  "status_code": exc.status_code,
@@ -509,6 +511,7 @@ class Admin(BaseAdminView):
509
511
  model_view = self._find_model_view(identity)
510
512
 
511
513
  Form = await model_view.scaffold_form()
514
+ model_view._validate_form_class(model_view._form_create_rules, Form)
512
515
  form_data = await self._handle_form_data(request)
513
516
  form = Form(form_data)
514
517
 
@@ -559,6 +562,7 @@ class Admin(BaseAdminView):
559
562
  raise HTTPException(status_code=404)
560
563
 
561
564
  Form = await model_view.scaffold_form()
565
+ model_view._validate_form_class(model_view._form_edit_rules, Form)
562
566
  context = {
563
567
  "obj": model,
564
568
  "model_view": model_view,
@@ -660,7 +664,7 @@ class Admin(BaseAdminView):
660
664
 
661
665
  def get_save_redirect_url(
662
666
  self, request: Request, form: FormData, model_view: ModelView, obj: Any
663
- ) -> Union[str, URL]:
667
+ ) -> str | URL:
664
668
  """
665
669
  Get the redirect URL after a save action
666
670
  which is triggered from create/edit page.
@@ -685,7 +689,7 @@ class Admin(BaseAdminView):
685
689
  """
686
690
 
687
691
  form = await request.form()
688
- form_data: List[Tuple[str, Union[str, UploadFile]]] = []
692
+ form_data: list[tuple[str, str | UploadFile]] = []
689
693
  for key, value in form.multi_items():
690
694
  if not isinstance(value, UploadFile):
691
695
  form_data.append((key, value))
@@ -726,8 +730,8 @@ class Admin(BaseAdminView):
726
730
  def expose(
727
731
  path: str,
728
732
  *,
729
- methods: List[str] = ["GET"],
730
- identity: Optional[str] = None,
733
+ methods: list[str] = ["GET"],
734
+ identity: str | None = None,
731
735
  include_in_schema: bool = True,
732
736
  ) -> Callable[..., Any]:
733
737
  """Expose View with information."""
@@ -746,8 +750,8 @@ def expose(
746
750
 
747
751
  def action(
748
752
  name: str,
749
- label: Optional[str] = None,
750
- confirmation_message: Optional[str] = None,
753
+ label: str | None = None,
754
+ confirmation_message: str | None = None,
751
755
  *,
752
756
  include_in_schema: bool = True,
753
757
  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