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.
- {sqladmin-0.22.0 → sqladmin-0.23.0}/PKG-INFO +15 -15
- {sqladmin-0.22.0 → sqladmin-0.23.0}/pyproject.toml +37 -67
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/__init__.py +0 -2
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/_menu.py +1 -1
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/_queries.py +22 -14
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/_types.py +6 -8
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/_validators.py +14 -8
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/ajax.py +2 -2
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/application.py +35 -20
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/fields.py +29 -20
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/filters.py +74 -45
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/formatters.py +1 -1
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/forms.py +140 -51
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/helpers.py +12 -5
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/models.py +71 -29
- sqladmin-0.23.0/sqladmin/pretty_export.py +75 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/details.html +7 -7
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/list.html +13 -2
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/templating.py +1 -1
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/widgets.py +20 -14
- sqladmin-0.22.0/.gitignore +0 -17
- sqladmin-0.22.0/LICENSE.md +0 -27
- {sqladmin-0.22.0 → sqladmin-0.23.0}/README.md +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/authentication.py +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/exceptions.py +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/pagination.py +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/py.typed +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/css/flatpickr.min.css +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/css/fontawesome.min.css +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/css/main.css +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/css/select2.min.css +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/css/tabler-icons.min.css +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/css/tabler-icons.min.css.map +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/css/tabler.min.css +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/js/bootstrap.min.js +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/js/flatpickr.min.js +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/js/jquery.min.js +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/js/main.js +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/js/popper.min.js +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/js/select2.full.min.js +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/js/tabler.min.js +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/webfonts/fa-brands-400.woff2 +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/webfonts/fa-regular-400.woff2 +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/webfonts/fa-solid-900.woff2 +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/statics/webfonts/tabler-icons.woff2 +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/_macros.html +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/base.html +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/create.html +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/edit.html +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/error.html +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/index.html +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/layout.html +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/login.html +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/modals/delete.html +0 -0
- {sqladmin-0.22.0 → sqladmin-0.23.0}/sqladmin/templates/sqladmin/modals/details_action_confirmation.html +0 -0
- {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.
|
|
3
|
+
Version: 0.23.0
|
|
4
4
|
Summary: SQLAlchemy admin for FastAPI and Starlette
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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>=
|
|
29
|
-
Requires-Dist:
|
|
30
|
-
Requires-
|
|
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 = ["
|
|
3
|
-
build-backend = "
|
|
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.
|
|
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
|
-
|
|
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
|
-
[
|
|
64
|
-
|
|
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
|
-
[
|
|
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
|
-
[
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
@@ -40,11 +40,13 @@ class Query:
|
|
|
40
40
|
conditions = []
|
|
41
41
|
for value in values:
|
|
42
42
|
conditions.append(
|
|
43
|
-
and_(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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 =
|
|
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
|
-
|
|
222
|
+
coro = self._delete_async(obj, request)
|
|
221
223
|
else:
|
|
222
|
-
|
|
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
|
-
|
|
230
|
+
coro = self._insert_async(data, request)
|
|
227
231
|
else:
|
|
228
|
-
|
|
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
|
-
|
|
238
|
+
coro = self._update_async(pk, data, request)
|
|
233
239
|
else:
|
|
234
|
-
|
|
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(
|
|
35
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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, "
|
|
186
|
-
|
|
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, "
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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] =
|
|
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)
|