sqladmin 0.21.0__tar.gz → 0.23.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 (59) hide show
  1. {sqladmin-0.21.0 → sqladmin-0.23.0}/PKG-INFO +17 -16
  2. sqladmin-0.23.0/pyproject.toml +113 -0
  3. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/__init__.py +0 -2
  4. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/_menu.py +1 -1
  5. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/_queries.py +22 -14
  6. sqladmin-0.23.0/sqladmin/_types.py +55 -0
  7. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/_validators.py +14 -8
  8. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/ajax.py +2 -2
  9. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/application.py +35 -20
  10. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/fields.py +29 -20
  11. sqladmin-0.23.0/sqladmin/filters.py +369 -0
  12. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/formatters.py +1 -1
  13. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/forms.py +140 -51
  14. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/helpers.py +20 -7
  15. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/models.py +95 -32
  16. sqladmin-0.23.0/sqladmin/pretty_export.py +75 -0
  17. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/details.html +7 -7
  18. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/list.html +62 -13
  19. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/templating.py +1 -1
  20. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/widgets.py +20 -14
  21. sqladmin-0.21.0/.gitignore +0 -17
  22. sqladmin-0.21.0/LICENSE.md +0 -27
  23. sqladmin-0.21.0/pyproject.toml +0 -155
  24. sqladmin-0.21.0/sqladmin/_types.py +0 -25
  25. sqladmin-0.21.0/sqladmin/filters.py +0 -174
  26. {sqladmin-0.21.0 → sqladmin-0.23.0}/README.md +0 -0
  27. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/authentication.py +0 -0
  28. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/exceptions.py +0 -0
  29. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/pagination.py +0 -0
  30. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/py.typed +0 -0
  31. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/statics/css/flatpickr.min.css +0 -0
  32. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/statics/css/fontawesome.min.css +0 -0
  33. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/statics/css/main.css +0 -0
  34. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/statics/css/select2.min.css +0 -0
  35. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/statics/css/tabler-icons.min.css +0 -0
  36. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/statics/css/tabler-icons.min.css.map +0 -0
  37. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/statics/css/tabler.min.css +0 -0
  38. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/statics/js/bootstrap.min.js +0 -0
  39. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/statics/js/flatpickr.min.js +0 -0
  40. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/statics/js/jquery.min.js +0 -0
  41. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/statics/js/main.js +0 -0
  42. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/statics/js/popper.min.js +0 -0
  43. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/statics/js/select2.full.min.js +0 -0
  44. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/statics/js/tabler.min.js +0 -0
  45. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/statics/webfonts/fa-brands-400.woff2 +0 -0
  46. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/statics/webfonts/fa-regular-400.woff2 +0 -0
  47. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/statics/webfonts/fa-solid-900.woff2 +0 -0
  48. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/statics/webfonts/tabler-icons.woff2 +0 -0
  49. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/_macros.html +0 -0
  50. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/base.html +0 -0
  51. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/create.html +0 -0
  52. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/edit.html +0 -0
  53. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/error.html +0 -0
  54. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/index.html +0 -0
  55. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/layout.html +0 -0
  56. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/login.html +0 -0
  57. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/modals/delete.html +0 -0
  58. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/modals/details_action_confirmation.html +0 -0
  59. {sqladmin-0.21.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/modals/list_action_confirmation.html +0 -0
@@ -1,34 +1,35 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqladmin
3
- Version: 0.21.0
3
+ Version: 0.23.0
4
4
  Summary: SQLAlchemy admin for FastAPI and Starlette
5
- Project-URL: Documentation, https://aminalaee.github.io/sqladmin/
6
- Project-URL: Issues, https://github.com/aminalaee/sqladmin/issues
7
- Project-URL: Source, https://github.com/aminalaee/sqladmin
5
+ Keywords: sqlalchemy,fastapi,starlette,admin
6
+ Author: Amin Alaee
8
7
  Author-email: Amin Alaee <me@aminalaee.dev>
9
8
  License-Expression: BSD-3-Clause
10
- License-File: LICENSE.md
11
- Keywords: admin,fastapi,sqlalchemy,starlette
12
9
  Classifier: Development Status :: 4 - Beta
13
- Classifier: Environment :: Web Environment
14
- Classifier: Intended Audience :: Developers
15
- Classifier: License :: OSI Approved :: BSD License
16
- Classifier: Operating System :: OS Independent
17
10
  Classifier: Programming Language :: Python
18
- Classifier: Programming Language :: Python :: 3.8
19
11
  Classifier: Programming Language :: Python :: 3.9
20
12
  Classifier: Programming Language :: Python :: 3.10
21
13
  Classifier: Programming Language :: Python :: 3.11
22
14
  Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Environment :: Web Environment
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: License :: OSI Approved :: BSD License
23
20
  Classifier: Topic :: Internet :: WWW/HTTP
24
- Requires-Python: >=3.8
21
+ Classifier: Operating System :: OS Independent
22
+ Requires-Dist: starlette
23
+ Requires-Dist: wtforms>=3.1,<3.2
25
24
  Requires-Dist: jinja2
26
25
  Requires-Dist: python-multipart
27
- Requires-Dist: sqlalchemy>=1.4
28
- Requires-Dist: starlette
29
- Requires-Dist: wtforms<3.2,>=3.1
26
+ Requires-Dist: sqlalchemy>=2.0
27
+ Requires-Dist: itsdangerous ; extra == 'full'
28
+ Requires-Python: >=3.9
29
+ Project-URL: Documentation, https://aminalaee.github.io/sqladmin/
30
+ Project-URL: Issues, https://github.com/aminalaee/sqladmin/issues
31
+ Project-URL: Source, https://github.com/aminalaee/sqladmin
30
32
  Provides-Extra: full
31
- Requires-Dist: itsdangerous; extra == 'full'
32
33
  Description-Content-Type: text/markdown
33
34
 
34
35
  <p align="center">
@@ -0,0 +1,113 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.9.17,<0.10.0"]
3
+ build-backend = "uv_build"
4
+
5
+ [tool.uv.build-backend]
6
+ module-name = "sqladmin"
7
+ module-root = ""
8
+
9
+ [project]
10
+ name = "sqladmin"
11
+ version = "0.23.0"
12
+ description = 'SQLAlchemy admin for FastAPI and Starlette'
13
+ readme = "README.md"
14
+ requires-python = ">=3.9"
15
+ license = "BSD-3-Clause"
16
+ keywords = ["sqlalchemy", "fastapi", "starlette", "admin"]
17
+ authors = [{ name = "Amin Alaee", email = "me@aminalaee.dev" }]
18
+ classifiers = [
19
+ "Development Status :: 4 - Beta",
20
+ "Programming Language :: Python",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Programming Language :: Python :: 3.14",
27
+ "Environment :: Web Environment",
28
+ "Intended Audience :: Developers",
29
+ "License :: OSI Approved :: BSD License",
30
+ "Topic :: Internet :: WWW/HTTP",
31
+ "Operating System :: OS Independent",
32
+ ]
33
+ dependencies = [
34
+ "starlette",
35
+ "wtforms >=3.1, <3.2",
36
+ "jinja2",
37
+ "python-multipart",
38
+ "sqlalchemy >= 2.0",
39
+ ]
40
+
41
+
42
+ [project.optional-dependencies]
43
+ full = ["itsdangerous"]
44
+
45
+ [project.urls]
46
+ Documentation = "https://aminalaee.github.io/sqladmin/"
47
+ Issues = "https://github.com/aminalaee/sqladmin/issues"
48
+ Source = "https://github.com/aminalaee/sqladmin"
49
+
50
+
51
+ [dependency-groups]
52
+ dev = [
53
+ "aiosqlite",
54
+ "arrow",
55
+ "asyncpg",
56
+ "babel",
57
+ "build",
58
+ "colour",
59
+ "coverage",
60
+ "email-validator",
61
+ "fastapi-storages",
62
+ "greenlet",
63
+ "httpx",
64
+ "itsdangerous",
65
+ "phonenumbers",
66
+ "pillow",
67
+ "psycopg2-binary",
68
+ "psycopg[binary]",
69
+ "pytest",
70
+ "python-dateutil",
71
+ "sqlalchemy_utils",
72
+ "sqlmodel",
73
+ "bandit",
74
+ ]
75
+
76
+ lint = ["mypy==1.18.1", "ruff==0.14.10", "types-wtforms>=3.2.1.20250809"]
77
+
78
+ docs = [
79
+ "mkdocs-material==9.6.14",
80
+ "mkdocs==1.6.1",
81
+ "mkdocstrings[python]==0.26.1",
82
+ ]
83
+
84
+ [tool.mypy]
85
+ disallow_untyped_defs = true
86
+ ignore_missing_imports = true
87
+ show_error_codes = true
88
+ check_untyped_defs = true
89
+ strict_optional = true
90
+ no_implicit_optional = true
91
+ disable_error_code = ["var-annotated"]
92
+
93
+ [[tool.mypy.overrides]]
94
+ module = "tests.*"
95
+ ignore_errors = true
96
+
97
+ [tool.ruff]
98
+ lint.select = ["E", "F", "I"]
99
+
100
+ [tool.coverage.run]
101
+ source_pkgs = ["sqladmin", "tests"]
102
+
103
+ [tool.coverage.report]
104
+ exclude_lines = [
105
+ "pragma: no cover",
106
+ "pragma: nocover",
107
+ "except NotImplementedError",
108
+ "raise NotImplementedError",
109
+ "if TYPE_CHECKING:",
110
+ ]
111
+ fail_under = 95
112
+ show_missing = true
113
+ skip_covered = true
@@ -1,8 +1,6 @@
1
1
  from sqladmin.application import Admin, action, expose
2
2
  from sqladmin.models import BaseView, ModelView
3
3
 
4
- __version__ = "0.21.0"
5
-
6
4
  __all__ = [
7
5
  "Admin",
8
6
  "expose",
@@ -93,6 +93,6 @@ class Menu:
93
93
  # Only works for one-level menu
94
94
  for root in self.items:
95
95
  if root.name == item.name:
96
- root.children.append(*item.children)
96
+ root.children.extend(item.children)
97
97
  return
98
98
  self.items.append(item)
@@ -40,11 +40,13 @@ class Query:
40
40
  conditions = []
41
41
  for value in values:
42
42
  conditions.append(
43
- and_(
44
- pk == value
45
- for pk, value in zip(
46
- target_pks,
47
- object_identifier_values(value, target),
43
+ and_( # type: ignore[type-var]
44
+ *(
45
+ pk == value
46
+ for pk, value in zip(
47
+ target_pks,
48
+ object_identifier_values(value, target),
49
+ )
48
50
  )
49
51
  )
50
52
  )
@@ -65,10 +67,10 @@ class Query:
65
67
  # ``relation.local_remote_pairs`` is ordered by the foreign keys
66
68
  # but the values are ordered by the primary keys. This dict
67
69
  # ensures we write the correct value to the fk fields
68
- pk_value = {pk: value for pk, value in zip(pks, values)}
70
+ pk_value = dict(zip(pks, values))
69
71
 
70
- for fk, pk in relation.local_remote_pairs:
71
- setattr(obj, fk.name, pk_value[pk])
72
+ for fk, pk in relation.local_remote_pairs or []:
73
+ setattr(obj, fk.name, pk_value[pk]) # type: ignore[index]
72
74
 
73
75
  return obj
74
76
 
@@ -217,18 +219,24 @@ class Query:
217
219
 
218
220
  async def delete(self, obj: Any, request: Request) -> None:
219
221
  if self.model_view.is_async:
220
- await self._delete_async(obj, request)
222
+ coro = self._delete_async(obj, request)
221
223
  else:
222
- await anyio.to_thread.run_sync(self._delete_sync, obj, request)
224
+ coro = anyio.to_thread.run_sync(self._delete_sync, obj, request)
225
+
226
+ return await coro
223
227
 
224
228
  async def insert(self, data: dict, request: Request) -> Any:
225
229
  if self.model_view.is_async:
226
- return await self._insert_async(data, request)
230
+ coro = self._insert_async(data, request)
227
231
  else:
228
- return await anyio.to_thread.run_sync(self._insert_sync, data, request)
232
+ coro = anyio.to_thread.run_sync(self._insert_sync, data, request)
233
+
234
+ return await coro
229
235
 
230
236
  async def update(self, pk: Any, data: dict, request: Request) -> Any:
231
237
  if self.model_view.is_async:
232
- return await self._update_async(pk, data, request)
238
+ coro = self._update_async(pk, data, request)
233
239
  else:
234
- return await anyio.to_thread.run_sync(self._update_sync, pk, data, request)
240
+ coro = anyio.to_thread.run_sync(self._update_sync, pk, data, request)
241
+
242
+ return await coro
@@ -0,0 +1,55 @@
1
+ from typing import (
2
+ Any,
3
+ Callable,
4
+ List,
5
+ Protocol,
6
+ Tuple,
7
+ Union,
8
+ runtime_checkable,
9
+ )
10
+
11
+ from sqlalchemy.engine import Engine
12
+ from sqlalchemy.ext.asyncio import AsyncEngine
13
+ from sqlalchemy.orm import ColumnProperty, InstrumentedAttribute, RelationshipProperty
14
+ from sqlalchemy.sql.expression import Select
15
+ from starlette.requests import Request
16
+
17
+ MODEL_PROPERTY = Union[ColumnProperty, RelationshipProperty]
18
+ ENGINE_TYPE = Union[Engine, AsyncEngine]
19
+ MODEL_ATTR = Union[str, InstrumentedAttribute]
20
+
21
+
22
+ @runtime_checkable
23
+ class SimpleColumnFilter(Protocol):
24
+ """Protocol for filters with simple value-based filtering"""
25
+
26
+ title: str
27
+ parameter_name: str
28
+
29
+ async def lookups(
30
+ self, request: Request, model: Any, run_query: Callable[[Select], Any]
31
+ ) -> List[Tuple[str, str]]: ... # pragma: no cover
32
+
33
+ async def get_filtered_query(
34
+ self, query: Select, value: Any, model: Any
35
+ ) -> Select: ... # pragma: no cover
36
+
37
+
38
+ @runtime_checkable
39
+ class OperationColumnFilter(Protocol):
40
+ """Protocol for filters with operation-based filtering"""
41
+
42
+ title: str
43
+ parameter_name: str
44
+ has_operator: bool
45
+
46
+ async def lookups(
47
+ self, request: Request, model: Any, run_query: Callable[[Select], Any]
48
+ ) -> List[Tuple[str, str]]: ... # pragma: no cover
49
+
50
+ async def get_filtered_query(
51
+ self, query: Select, operation: str, value: Any, model: Any
52
+ ) -> Select: ... # pragma: no cover
53
+
54
+
55
+ ColumnFilter = Union[SimpleColumnFilter, OperationColumnFilter]
@@ -11,8 +11,10 @@ class CurrencyValidator:
11
11
 
12
12
  try:
13
13
  Currency(field.data)
14
- except (TypeError, ValueError):
15
- raise ValidationError("Not a valid ISO currency code (e.g. USD, EUR, CNY).")
14
+ except (TypeError, ValueError) as exc:
15
+ raise ValidationError(
16
+ "Not a valid ISO currency code (e.g. USD, EUR, CNY)."
17
+ ) from exc
16
18
 
17
19
 
18
20
  class PhoneNumberValidator:
@@ -23,8 +25,8 @@ class PhoneNumberValidator:
23
25
 
24
26
  try:
25
27
  PhoneNumber(field.data)
26
- except PhoneNumberParseException:
27
- raise ValidationError("Not a valid phone number.")
28
+ except PhoneNumberParseException as exc:
29
+ raise ValidationError("Not a valid phone number.") from exc
28
30
 
29
31
 
30
32
  class ColorValidator:
@@ -35,8 +37,10 @@ class ColorValidator:
35
37
 
36
38
  try:
37
39
  Color(field.data)
38
- except ValueError:
39
- raise ValidationError('Not a valid color (e.g. "red", "#f00", "#ff0000").')
40
+ except ValueError as exc:
41
+ raise ValidationError(
42
+ 'Not a valid color (e.g. "red", "#f00", "#ff0000").'
43
+ ) from exc
40
44
 
41
45
 
42
46
  class TimezoneValidator:
@@ -48,5 +52,7 @@ class TimezoneValidator:
48
52
  def __call__(self, form: Form, field: Field) -> None:
49
53
  try:
50
54
  self.coerce_function(str(field.data))
51
- except Exception:
52
- raise ValidationError("Not a valid timezone (e.g. 'Asia/Singapore').")
55
+ except Exception as exc:
56
+ raise ValidationError(
57
+ "Not a valid timezone (e.g. 'Asia/Singapore')."
58
+ ) from exc
@@ -93,8 +93,8 @@ def create_ajax_loader(
93
93
 
94
94
  try:
95
95
  attr = mapper.relationships[name]
96
- except KeyError:
97
- raise ValueError(f"{model_admin.model}.{name} is not a relation.")
96
+ except KeyError as exc:
97
+ raise ValueError(f"{model_admin.model}.{name} is not a relation.") from exc
98
98
 
99
99
  remote_model = attr.mapper.class_
100
100
  return QueryAjaxModelLoader(name, remote_model, model_admin, **options)
@@ -42,7 +42,7 @@ from sqladmin.models import BaseView, ModelView
42
42
  from sqladmin.templating import Jinja2Templates
43
43
 
44
44
  if TYPE_CHECKING:
45
- from sqlalchemy.ext.asyncio import async_sessionmaker
45
+ from sqlalchemy.ext.asyncio import async_sessionmaker # type: ignore[attr-defined]
46
46
 
47
47
  __all__ = [
48
48
  "Admin",
@@ -83,10 +83,13 @@ class BaseAdmin:
83
83
 
84
84
  if session_maker:
85
85
  self.session_maker = session_maker
86
- elif isinstance(engine, Engine):
86
+ elif isinstance(self.engine, Engine):
87
87
  self.session_maker = sessionmaker(bind=self.engine, class_=Session)
88
88
  else:
89
- self.session_maker = sessionmaker(bind=self.engine, class_=AsyncSession)
89
+ self.session_maker = sessionmaker(
90
+ bind=self.engine, # type: ignore[arg-type]
91
+ class_=AsyncSession,
92
+ )
90
93
 
91
94
  self.session_maker.configure(autoflush=False, autocommit=False)
92
95
  self.is_async = is_async_session_maker(self.session_maker)
@@ -113,7 +116,7 @@ class BaseAdmin:
113
116
  templates.env.globals["min"] = min
114
117
  templates.env.globals["zip"] = zip
115
118
  templates.env.globals["admin"] = self
116
- templates.env.globals["is_list"] = lambda x: isinstance(x, list)
119
+ templates.env.globals["is_list"] = lambda x: isinstance(x, (list, set))
117
120
  templates.env.globals["get_object_identifier"] = get_object_identifier
118
121
 
119
122
  return templates
@@ -181,14 +184,14 @@ class BaseAdmin:
181
184
  func, "_label"
182
185
  )
183
186
  if getattr(func, "_add_in_detail"):
184
- view_instance._custom_actions_in_detail[
185
- getattr(func, "_slug")
186
- ] = getattr(func, "_label")
187
+ view_instance._custom_actions_in_detail[getattr(func, "_slug")] = (
188
+ getattr(func, "_label")
189
+ )
187
190
 
188
191
  if getattr(func, "_confirmation_message"):
189
- view_instance._custom_actions_confirmation[
190
- getattr(func, "_slug")
191
- ] = getattr(func, "_confirmation_message")
192
+ view_instance._custom_actions_confirmation[getattr(func, "_slug")] = (
193
+ getattr(func, "_confirmation_message")
194
+ )
192
195
 
193
196
  def _handle_expose_decorated_func(
194
197
  self,
@@ -345,7 +348,7 @@ class Admin(BaseAdminView):
345
348
  ```
346
349
  """
347
350
 
348
- def __init__(
351
+ def __init__( # type: ignore[no-any-unimported]
349
352
  self,
350
353
  app: Starlette,
351
354
  engine: ENGINE_TYPE | None = None,
@@ -373,7 +376,7 @@ class Admin(BaseAdminView):
373
376
  super().__init__(
374
377
  app=app,
375
378
  engine=engine,
376
- session_maker=session_maker,
379
+ session_maker=session_maker, # type: ignore[arg-type]
377
380
  base_url=base_url,
378
381
  title=title,
379
382
  logo_url=logo_url,
@@ -388,7 +391,9 @@ class Admin(BaseAdminView):
388
391
  async def http_exception(
389
392
  request: Request, exc: Exception
390
393
  ) -> Response | Awaitable[Response]:
391
- assert isinstance(exc, HTTPException)
394
+ if not isinstance(exc, HTTPException):
395
+ raise TypeError("Expected HTTPException, got %s" % type(exc))
396
+
392
397
  context = {
393
398
  "status_code": exc.status_code,
394
399
  "message": exc.detail,
@@ -630,7 +635,11 @@ class Admin(BaseAdminView):
630
635
  return await model_view.export_data(rows, export_type=export_type)
631
636
 
632
637
  async def login(self, request: Request) -> Response:
633
- assert self.authentication_backend is not None
638
+ if self.authentication_backend is None:
639
+ raise HTTPException(
640
+ status_code=503,
641
+ detail="Authentication backend not configured.",
642
+ )
634
643
 
635
644
  context = {}
636
645
  if request.method == "GET":
@@ -646,7 +655,11 @@ class Admin(BaseAdminView):
646
655
  return RedirectResponse(request.url_for("admin:index"), status_code=302)
647
656
 
648
657
  async def logout(self, request: Request) -> Response:
649
- assert self.authentication_backend is not None
658
+ if self.authentication_backend is None:
659
+ raise HTTPException(
660
+ status_code=503,
661
+ detail="Authentication backend not configured.",
662
+ )
650
663
 
651
664
  response = await self.authentication_backend.logout(request)
652
665
 
@@ -669,8 +682,8 @@ class Admin(BaseAdminView):
669
682
 
670
683
  try:
671
684
  loader: QueryAjaxModelLoader = model_view._form_ajax_refs[name]
672
- except KeyError:
673
- raise HTTPException(status_code=400)
685
+ except KeyError as exc:
686
+ raise HTTPException(status_code=400) from exc
674
687
 
675
688
  data = [loader.format(m) for m in await loader.get_list(term)]
676
689
  return JSONResponse({"results": data})
@@ -688,10 +701,12 @@ class Admin(BaseAdminView):
688
701
 
689
702
  if form.get("save") == "Save":
690
703
  return request.url_for("admin:list", identity=identity)
691
- elif form.get("save") == "Save and continue editing" or (
704
+
705
+ if form.get("save") == "Save and continue editing" or (
692
706
  form.get("save") == "Save as new" and model_view.save_as_continue
693
707
  ):
694
708
  return request.url_for("admin:edit", identity=identity, pk=identifier)
709
+
695
710
  return request.url_for("admin:create", identity=identity)
696
711
 
697
712
  async def _handle_form_data(self, request: Request, obj: Any = None) -> FormData:
@@ -743,7 +758,7 @@ class Admin(BaseAdminView):
743
758
  def expose(
744
759
  path: str,
745
760
  *,
746
- methods: list[str] = ["GET"],
761
+ methods: list[str] | None = None,
747
762
  identity: str | None = None,
748
763
  include_in_schema: bool = True,
749
764
  ) -> Callable[..., Any]:
@@ -753,7 +768,7 @@ def expose(
753
768
  def wrap(func):
754
769
  func._exposed = True
755
770
  func._path = path
756
- func._methods = methods
771
+ func._methods = methods or ["GET"]
757
772
  func._identity = identity or func.__name__
758
773
  func._include_in_schema = include_in_schema
759
774
  return login_required(func)
@@ -1,7 +1,10 @@
1
+ # mypy: disable-error-code="override"
2
+
1
3
  from __future__ import annotations
2
4
 
3
5
  import json
4
6
  import operator
7
+ from enum import Enum
5
8
  from typing import Any, Callable, Generator
6
9
 
7
10
  from wtforms import Form, ValidationError, fields, widgets
@@ -29,7 +32,7 @@ class DateField(fields.DateField):
29
32
  Add custom DatePickerWidget for data-format and data-date-format fields
30
33
  """
31
34
 
32
- widget = sqladmin_widgets.DatePickerWidget()
35
+ widget = sqladmin_widgets.DatePickerWidget() # type: ignore[assignment]
33
36
 
34
37
 
35
38
  class DateTimeField(fields.DateTimeField):
@@ -37,7 +40,7 @@ class DateTimeField(fields.DateTimeField):
37
40
  Allows modifying the datetime format of a DateTimeField using form_args.
38
41
  """
39
42
 
40
- widget = sqladmin_widgets.DateTimePickerWidget()
43
+ widget = sqladmin_widgets.DateTimePickerWidget() # type: ignore[assignment]
41
44
 
42
45
 
43
46
  class IntervalField(fields.StringField):
@@ -53,7 +56,7 @@ class IntervalField(fields.StringField):
53
56
  if not interval:
54
57
  raise ValueError("Invalide timedelta format.")
55
58
 
56
- self.data = interval
59
+ self.data = interval # type: ignore[assignment]
57
60
 
58
61
 
59
62
  class SelectField(fields.SelectField):
@@ -80,13 +83,15 @@ class SelectField(fields.SelectField):
80
83
  for choice in choices:
81
84
  if isinstance(choice, tuple):
82
85
  yield (choice[0], choice[1], self.coerce(choice[0]) == self.data, {})
83
- else:
86
+ elif isinstance(choice, Enum):
84
87
  yield (
85
88
  choice.value,
86
89
  choice.name,
87
90
  self.coerce(choice.value) == self.data,
88
91
  {},
89
92
  )
93
+ else:
94
+ yield (str(choice), str(choice), self.coerce(choice) == self.data, {})
90
95
 
91
96
  def process_formdata(self, valuelist: list[str]) -> None:
92
97
  if valuelist:
@@ -95,8 +100,10 @@ class SelectField(fields.SelectField):
95
100
  else:
96
101
  try:
97
102
  self.data = self.coerce(valuelist[0])
98
- except ValueError:
99
- raise ValueError(self.gettext("Invalid Choice: could not coerce"))
103
+ except ValueError as exc:
104
+ raise ValueError(
105
+ self.gettext("Invalid Choice: could not coerce")
106
+ ) from exc
100
107
 
101
108
  def pre_validate(self, form: Form) -> None:
102
109
  if self.allow_blank and self.data is None:
@@ -109,10 +116,11 @@ class JSONField(fields.TextAreaField):
109
116
  def _value(self) -> str:
110
117
  if self.raw_data:
111
118
  return self.raw_data[0]
112
- elif self.data:
119
+
120
+ if self.data:
113
121
  return str(json.dumps(self.data, ensure_ascii=False))
114
- else:
115
- return "{}"
122
+
123
+ return "{}"
116
124
 
117
125
  def process_formdata(self, valuelist: list[str]) -> None:
118
126
  if valuelist:
@@ -125,8 +133,8 @@ class JSONField(fields.TextAreaField):
125
133
 
126
134
  try:
127
135
  self.data = json.loads(valuelist[0])
128
- except ValueError:
129
- raise ValueError(self.gettext("Invalid JSON"))
136
+ except ValueError as exc:
137
+ raise ValueError(self.gettext("Invalid JSON")) from exc
130
138
 
131
139
 
132
140
  class QuerySelectField(fields.SelectFieldBase):
@@ -168,7 +176,7 @@ class QuerySelectField(fields.SelectFieldBase):
168
176
  return self._data
169
177
 
170
178
  @data.setter
171
- def data(self, data: tuple) -> None:
179
+ def data(self, data: tuple | None) -> None:
172
180
  self._data = data
173
181
  self._formdata = None
174
182
 
@@ -251,7 +259,8 @@ class QuerySelectMultipleField(QuerySelectField):
251
259
  for pk, _ in self._select_data:
252
260
  if not formdata:
253
261
  break
254
- elif pk in formdata:
262
+
263
+ if pk in formdata:
255
264
  formdata.remove(pk)
256
265
  data.append(pk)
257
266
  if formdata:
@@ -260,7 +269,7 @@ class QuerySelectMultipleField(QuerySelectField):
260
269
  return self._data
261
270
 
262
271
  @data.setter
263
- def data(self, data: tuple) -> None:
272
+ def data(self, data: tuple | None) -> None:
264
273
  self._data = data
265
274
  self._formdata = None
266
275
 
@@ -280,7 +289,8 @@ class QuerySelectMultipleField(QuerySelectField):
280
289
  def pre_validate(self, form: Form) -> None:
281
290
  if self._invalid_formdata:
282
291
  raise ValidationError(self.gettext("Not a valid choice"))
283
- elif self.data:
292
+
293
+ if self.data:
284
294
  pk_list = [x[0] for x in self._select_data]
285
295
  for v in self.data:
286
296
  if v not in pk_list: # pragma: no cover
@@ -330,7 +340,7 @@ class AjaxSelectField(fields.SelectFieldBase):
330
340
 
331
341
 
332
342
  class AjaxSelectMultipleField(fields.SelectFieldBase):
333
- widget = sqladmin_widgets.AjaxSelect2Widget(multiple=True)
343
+ widget = sqladmin_widgets.AjaxSelect2Widget(multiple=True) # type: ignore[assignment]
334
344
  separator = ","
335
345
 
336
346
  def __init__(
@@ -371,10 +381,9 @@ class AjaxSelectMultipleField(fields.SelectFieldBase):
371
381
 
372
382
 
373
383
  class Select2TagsField(fields.SelectField):
374
- widget = sqladmin_widgets.Select2TagsWidget()
384
+ widget = sqladmin_widgets.Select2TagsWidget() # type: ignore[assignment]
375
385
 
376
- def pre_validate(self, form: Form) -> None:
377
- ...
386
+ def pre_validate(self, form: Form) -> None: ...
378
387
 
379
388
  def process_formdata(self, valuelist: list) -> None:
380
389
  self.data = valuelist
@@ -388,4 +397,4 @@ class FileField(fields.FileField):
388
397
  File field which is clearable.
389
398
  """
390
399
 
391
- widget = sqladmin_widgets.FileInputWidget()
400
+ widget = sqladmin_widgets.FileInputWidget() # type: ignore[assignment]