sqladmin 0.22.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 (56) hide show
  1. {sqladmin-0.22.0 → sqladmin-0.23.0}/PKG-INFO +15 -15
  2. {sqladmin-0.22.0 → sqladmin-0.23.0}/pyproject.toml +37 -67
  3. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/__init__.py +0 -2
  4. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/_menu.py +1 -1
  5. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/_queries.py +22 -14
  6. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/_types.py +6 -8
  7. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/_validators.py +14 -8
  8. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/ajax.py +2 -2
  9. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/application.py +35 -20
  10. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/fields.py +29 -20
  11. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/filters.py +74 -45
  12. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/formatters.py +1 -1
  13. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/forms.py +140 -51
  14. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/helpers.py +12 -5
  15. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/models.py +71 -29
  16. sqladmin-0.23.0/sqladmin/pretty_export.py +75 -0
  17. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/details.html +7 -7
  18. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/list.html +13 -2
  19. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/templating.py +1 -1
  20. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/widgets.py +20 -14
  21. sqladmin-0.22.0/.gitignore +0 -17
  22. sqladmin-0.22.0/LICENSE.md +0 -27
  23. {sqladmin-0.22.0 → sqladmin-0.23.0}/README.md +0 -0
  24. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/authentication.py +0 -0
  25. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/exceptions.py +0 -0
  26. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/pagination.py +0 -0
  27. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/py.typed +0 -0
  28. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/css/flatpickr.min.css +0 -0
  29. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/css/fontawesome.min.css +0 -0
  30. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/css/main.css +0 -0
  31. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/css/select2.min.css +0 -0
  32. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/css/tabler-icons.min.css +0 -0
  33. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/css/tabler-icons.min.css.map +0 -0
  34. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/css/tabler.min.css +0 -0
  35. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/js/bootstrap.min.js +0 -0
  36. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/js/flatpickr.min.js +0 -0
  37. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/js/jquery.min.js +0 -0
  38. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/js/main.js +0 -0
  39. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/js/popper.min.js +0 -0
  40. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/js/select2.full.min.js +0 -0
  41. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/js/tabler.min.js +0 -0
  42. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/webfonts/fa-brands-400.woff2 +0 -0
  43. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/webfonts/fa-regular-400.woff2 +0 -0
  44. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/webfonts/fa-solid-900.woff2 +0 -0
  45. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/webfonts/tabler-icons.woff2 +0 -0
  46. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/_macros.html +0 -0
  47. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/base.html +0 -0
  48. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/create.html +0 -0
  49. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/edit.html +0 -0
  50. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/error.html +0 -0
  51. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/index.html +0 -0
  52. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/layout.html +0 -0
  53. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/login.html +0 -0
  54. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/modals/delete.html +0 -0
  55. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/modals/details_action_confirmation.html +0 -0
  56. {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/modals/list_action_confirmation.html +0 -0
@@ -1,19 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqladmin
3
- Version: 0.22.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
11
  Classifier: Programming Language :: Python :: 3.9
19
12
  Classifier: Programming Language :: Python :: 3.10
@@ -21,15 +14,22 @@ Classifier: Programming Language :: Python :: 3.11
21
14
  Classifier: Programming Language :: Python :: 3.12
22
15
  Classifier: Programming Language :: Python :: 3.13
23
16
  Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Environment :: Web Environment
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: License :: OSI Approved :: BSD License
24
20
  Classifier: Topic :: Internet :: WWW/HTTP
25
- Requires-Python: >=3.10
21
+ Classifier: Operating System :: OS Independent
22
+ Requires-Dist: starlette
23
+ Requires-Dist: wtforms>=3.1,<3.2
26
24
  Requires-Dist: jinja2
27
25
  Requires-Dist: python-multipart
28
- Requires-Dist: sqlalchemy>=1.4
29
- Requires-Dist: starlette
30
- 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
31
32
  Provides-Extra: full
32
- Requires-Dist: itsdangerous; extra == 'full'
33
33
  Description-Content-Type: text/markdown
34
34
 
35
35
  <p align="center">
@@ -1,17 +1,20 @@
1
1
  [build-system]
2
- requires = ["hatchling"]
3
- build-backend = "hatchling.build"
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 = ""
4
8
 
5
9
  [project]
6
10
  name = "sqladmin"
11
+ version = "0.23.0"
7
12
  description = 'SQLAlchemy admin for FastAPI and Starlette'
8
13
  readme = "README.md"
9
- requires-python = ">=3.10"
14
+ requires-python = ">=3.9"
10
15
  license = "BSD-3-Clause"
11
16
  keywords = ["sqlalchemy", "fastapi", "starlette", "admin"]
12
- authors = [
13
- { name = "Amin Alaee", email = "me@aminalaee.dev" },
14
- ]
17
+ authors = [{ name = "Amin Alaee", email = "me@aminalaee.dev" }]
15
18
  classifiers = [
16
19
  "Development Status :: 4 - Beta",
17
20
  "Programming Language :: Python",
@@ -29,39 +32,24 @@ classifiers = [
29
32
  ]
30
33
  dependencies = [
31
34
  "starlette",
32
- "sqlalchemy >=1.4",
33
35
  "wtforms >=3.1, <3.2",
34
36
  "jinja2",
35
37
  "python-multipart",
38
+ "sqlalchemy >= 2.0",
36
39
  ]
37
- dynamic = ["version"]
40
+
38
41
 
39
42
  [project.optional-dependencies]
40
- full = [
41
- "itsdangerous",
42
- ]
43
+ full = ["itsdangerous"]
43
44
 
44
45
  [project.urls]
45
46
  Documentation = "https://aminalaee.github.io/sqladmin/"
46
47
  Issues = "https://github.com/aminalaee/sqladmin/issues"
47
48
  Source = "https://github.com/aminalaee/sqladmin"
48
49
 
49
- [tool.hatch.version]
50
- path = "sqladmin/__init__.py"
51
-
52
- [tool.hatch.build.targets.wheel]
53
- [tool.hatch.build.targets.sdist]
54
- include = [
55
- "/sqladmin",
56
- ]
57
-
58
- [tool.hatch.build]
59
- exclude = [
60
- "tests/*",
61
- ]
62
50
 
63
- [tool.hatch.envs.test]
64
- dependencies = [
51
+ [dependency-groups]
52
+ dev = [
65
53
  "aiosqlite",
66
54
  "arrow",
67
55
  "asyncpg",
@@ -76,68 +64,50 @@ dependencies = [
76
64
  "itsdangerous",
77
65
  "phonenumbers",
78
66
  "pillow",
67
+ "psycopg2-binary",
79
68
  "psycopg[binary]",
80
69
  "pytest",
81
70
  "python-dateutil",
82
71
  "sqlalchemy_utils",
72
+ "sqlmodel",
73
+ "bandit",
83
74
  ]
84
75
 
85
- [tool.hatch.envs.lint]
86
- dependencies = [
87
- "mypy==1.8.0",
88
- "ruff==0.1.5",
89
- "sqlalchemy~=1.4", # MyPy issues with SQLAlchemy V2
90
- ]
76
+ lint = ["mypy==1.18.1", "ruff==0.14.10", "types-wtforms>=3.2.1.20250809"]
91
77
 
92
- [tool.hatch.envs.docs]
93
- dependencies = [
78
+ docs = [
94
79
  "mkdocs-material==9.6.14",
95
80
  "mkdocs==1.6.1",
96
81
  "mkdocstrings[python]==0.26.1",
97
82
  ]
98
83
 
99
- [tool.hatch.envs.test.scripts]
100
- cov = [
101
- "coverage report --show-missing --skip-covered --fail-under=99",
102
- "coverage xml",
103
- ]
104
- test = "coverage run -a --concurrency=thread,greenlet -m pytest {args}"
105
-
106
- [tool.hatch.envs.lint.scripts]
107
- check = [
108
- "ruff .",
109
- "ruff format --check .",
110
- "mypy sqladmin",
111
- ]
112
- format = [
113
- "ruff format .",
114
- "ruff --fix .",
115
- ]
116
-
117
- [tool.hatch.envs.docs.scripts]
118
- build = "mkdocs build"
119
- serve = "mkdocs serve --dev-addr localhost:8080"
120
- deploy = "mkdocs gh-deploy --force"
121
-
122
84
  [tool.mypy]
123
85
  disallow_untyped_defs = true
124
86
  ignore_missing_imports = true
125
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
126
96
 
127
97
  [tool.ruff]
128
- select = ["E", "F", "I"]
98
+ lint.select = ["E", "F", "I"]
129
99
 
130
100
  [tool.coverage.run]
131
- source_pkgs = [
132
- "sqladmin",
133
- "tests",
134
- ]
101
+ source_pkgs = ["sqladmin", "tests"]
135
102
 
136
103
  [tool.coverage.report]
137
104
  exclude_lines = [
138
- "pragma: no cover",
139
- "pragma: nocover",
140
- "except NotImplementedError",
141
- "raise NotImplementedError",
142
- "if TYPE_CHECKING:",
105
+ "pragma: no cover",
106
+ "pragma: nocover",
107
+ "except NotImplementedError",
108
+ "raise NotImplementedError",
109
+ "if TYPE_CHECKING:",
143
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.22.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
@@ -28,11 +28,11 @@ class SimpleColumnFilter(Protocol):
28
28
 
29
29
  async def lookups(
30
30
  self, request: Request, model: Any, run_query: Callable[[Select], Any]
31
- ) -> List[Tuple[str, str]]:
32
- ... # pragma: no cover
31
+ ) -> List[Tuple[str, str]]: ... # pragma: no cover
33
32
 
34
- async def get_filtered_query(self, query: Select, value: Any, model: Any) -> Select:
35
- ... # pragma: no cover
33
+ async def get_filtered_query(
34
+ self, query: Select, value: Any, model: Any
35
+ ) -> Select: ... # pragma: no cover
36
36
 
37
37
 
38
38
  @runtime_checkable
@@ -45,13 +45,11 @@ class OperationColumnFilter(Protocol):
45
45
 
46
46
  async def lookups(
47
47
  self, request: Request, model: Any, run_query: Callable[[Select], Any]
48
- ) -> List[Tuple[str, str]]:
49
- ... # pragma: no cover
48
+ ) -> List[Tuple[str, str]]: ... # pragma: no cover
50
49
 
51
50
  async def get_filtered_query(
52
51
  self, query: Select, operation: str, value: Any, model: Any
53
- ) -> Select:
54
- ... # pragma: no cover
52
+ ) -> Select: ... # pragma: no cover
55
53
 
56
54
 
57
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)