fastadmin 0.7.0__tar.gz → 0.8.1__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.
- {fastadmin-0.7.0 → fastadmin-0.8.1}/PKG-INFO +1 -1
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/__init__.py +1 -0
- fastadmin-0.8.1/fastadmin/api/encoders.py +70 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/api/frameworks/flask/app.py +8 -5
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/api/helpers.py +54 -5
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/api/service.py +11 -11
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/models/base.py +9 -4
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/models/orms/ponyorm.py +16 -8
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/models/orms/sqlalchemy.py +15 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/pyproject.toml +1 -1
- {fastadmin-0.7.0 → fastadmin-0.8.1}/LICENSE.md +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/README.md +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/api/__init__.py +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/api/exceptions.py +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/api/frameworks/__init__.py +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/api/frameworks/django/__init__.py +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/api/frameworks/django/app/__init__.py +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/api/frameworks/django/app/api.py +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/api/frameworks/django/app/urls.py +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/api/frameworks/django/app/views.py +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/api/frameworks/fastapi/__init__.py +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/api/frameworks/fastapi/api.py +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/api/frameworks/fastapi/app.py +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/api/frameworks/fastapi/views.py +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/api/frameworks/flask/__init__.py +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/api/frameworks/flask/api.py +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/api/frameworks/flask/views.py +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/api/schemas.py +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/models/__init__.py +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/models/decorators.py +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/models/helpers.py +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/models/orms/__init__.py +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/models/orms/django.py +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/models/orms/tortoise.py +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/models/orms/yaraorm.py +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/models/schemas.py +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/settings.py +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/static/assets/worker-6Z7niv9l.js +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/static/assets/worker-C151k0-L.js +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/static/assets/worker-C9KPOOKD.js +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/static/assets/worker-D1fCgYB7.js +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/static/assets/worker-D3TWcJOI.js +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/static/assets/worker-DRNcaZ-V.js +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/static/assets/worker-MF2p-l5_.js +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/static/images/favicon.png +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/static/images/header-logo.svg +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/static/images/sign-in-logo.svg +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/static/index.html +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/static/index.min.css +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/static/index.min.js +0 -0
- {fastadmin-0.7.0 → fastadmin-0.8.1}/fastadmin/templates/index.html +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastadmin
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.1
|
|
4
4
|
Summary: FastAdmin is an easy-to-use Admin Dashboard App for FastAPI/Flask/Django inspired by Django Admin.
|
|
5
5
|
Keywords: admin,dashboard,fastapi,flask,django,tortoise-orm,sqlalchemy,pony,yara-orm,admin-dashboard,django-admin
|
|
6
6
|
Author: Seva D
|
|
@@ -38,6 +38,7 @@ except ModuleNotFoundError: # pragma: no cover
|
|
|
38
38
|
logging.info("Yara ORM is not installed") # pragma: no cover
|
|
39
39
|
|
|
40
40
|
# api
|
|
41
|
+
from fastadmin.api.encoders import register_encoder, unregister_encoder # noqa: F401
|
|
41
42
|
from fastadmin.api.exceptions import AdminApiException # noqa: F401
|
|
42
43
|
|
|
43
44
|
# models
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""User-registerable custom encoders for admin API serialization.
|
|
2
|
+
|
|
3
|
+
Model field values are converted to JSON-serializable representations during
|
|
4
|
+
serialization (see :meth:`fastadmin.models.base.BaseModelAdmin.serialize_obj_attributes`).
|
|
5
|
+
Register a custom encoder to control how instances of a given type are represented
|
|
6
|
+
in every admin API response — e.g. a custom datetime format, an ``Enum``'s label,
|
|
7
|
+
or a domain value object.
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
|
|
11
|
+
.. code-block:: python
|
|
12
|
+
|
|
13
|
+
import datetime
|
|
14
|
+
|
|
15
|
+
from fastadmin import register_encoder
|
|
16
|
+
|
|
17
|
+
# Render every datetime as "2024-01-31 14:05" instead of ISO 8601.
|
|
18
|
+
register_encoder(datetime.datetime, lambda dt: dt.strftime("%Y-%m-%d %H:%M"))
|
|
19
|
+
|
|
20
|
+
Encoders are matched via ``isinstance`` in registration order, so register more
|
|
21
|
+
specific types before their base types.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from collections.abc import Callable
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
# Maps a type to a function converting an instance of that type to a JSON-serializable value.
|
|
28
|
+
CUSTOM_ENCODERS: dict[type, Callable[[Any], Any]] = {}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def register_encoder(type_: type, encoder: Callable[[Any], Any]) -> None:
|
|
32
|
+
"""Register a custom encoder for a type used across all admin API responses.
|
|
33
|
+
|
|
34
|
+
:params type_: the type to encode (matched via ``isinstance``).
|
|
35
|
+
:params encoder: a callable converting an instance to a JSON-serializable value.
|
|
36
|
+
:return: None.
|
|
37
|
+
"""
|
|
38
|
+
CUSTOM_ENCODERS[type_] = encoder
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def unregister_encoder(type_: type) -> None:
|
|
42
|
+
"""Remove a previously registered encoder for a type (no-op if absent).
|
|
43
|
+
|
|
44
|
+
:params type_: the type whose encoder should be removed.
|
|
45
|
+
:return: None.
|
|
46
|
+
"""
|
|
47
|
+
CUSTOM_ENCODERS.pop(type_, None)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def clear_encoders() -> None:
|
|
51
|
+
"""Remove all registered custom encoders.
|
|
52
|
+
|
|
53
|
+
:return: None.
|
|
54
|
+
"""
|
|
55
|
+
CUSTOM_ENCODERS.clear()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def apply_custom_encoders(value: Any) -> Any:
|
|
59
|
+
"""Return ``value`` encoded by the first matching custom encoder, else unchanged.
|
|
60
|
+
|
|
61
|
+
Types are matched via ``isinstance`` in registration order, so a more specific
|
|
62
|
+
type registered before its base type wins.
|
|
63
|
+
|
|
64
|
+
:params value: the value to encode.
|
|
65
|
+
:return: the encoded value, or the original value if no encoder matches.
|
|
66
|
+
"""
|
|
67
|
+
for type_, encoder in CUSTOM_ENCODERS.items():
|
|
68
|
+
if isinstance(value, type_):
|
|
69
|
+
return encoder(value)
|
|
70
|
+
return value
|
|
@@ -33,8 +33,11 @@ app.register_blueprint(api_router)
|
|
|
33
33
|
@app.errorhandler(Exception)
|
|
34
34
|
def exception_handler(exc):
|
|
35
35
|
if isinstance(exc, HTTPException):
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
# Return API errors as JSON {"detail": ...} with the proper HTTP status
|
|
37
|
+
# so the shared React frontend can read the message (matching the Django
|
|
38
|
+
# and FastAPI integrations), instead of werkzeug's default HTML page.
|
|
39
|
+
return {"detail": exc.description}, exc.code or 500
|
|
40
|
+
# Unhandled server error: log it server-side but never leak internals to the
|
|
41
|
+
# client, and return a real HTTP 500 (a bare dict would be sent as HTTP 200).
|
|
42
|
+
logger.error("Unhandled admin error: %s", exc)
|
|
43
|
+
return {"detail": "Internal server error."}, 500
|
|
@@ -1,17 +1,41 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
from uuid import UUID
|
|
3
3
|
|
|
4
|
-
from fastadmin.models.schemas import ModelFieldWidgetSchema
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
from fastadmin.models.schemas import ModelFieldWidgetSchema, WidgetType
|
|
5
|
+
|
|
6
|
+
# Text-like filter widgets whose values are free-form strings. For these the
|
|
7
|
+
# literals "true"/"false"/"null" are legitimate content and must NOT be coerced
|
|
8
|
+
# to bool/None (otherwise a Char column can never be filtered for those words).
|
|
9
|
+
TEXT_FILTER_WIDGET_TYPES = frozenset(
|
|
10
|
+
{
|
|
11
|
+
WidgetType.Input,
|
|
12
|
+
WidgetType.TextArea,
|
|
13
|
+
WidgetType.RichTextArea,
|
|
14
|
+
WidgetType.JsonTextArea,
|
|
15
|
+
WidgetType.SlugInput,
|
|
16
|
+
WidgetType.EmailInput,
|
|
17
|
+
WidgetType.PhoneInput,
|
|
18
|
+
WidgetType.UrlInput,
|
|
19
|
+
WidgetType.PasswordInput,
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def sanitize_filter_value(
|
|
25
|
+
value: str | list,
|
|
26
|
+
field: ModelFieldWidgetSchema | None = None,
|
|
27
|
+
) -> bool | None | str | list:
|
|
8
28
|
"""Sanitize value (string or list for __in filters).
|
|
9
29
|
|
|
10
30
|
:params value: a value (str or list of str for __in).
|
|
31
|
+
:params field: the field being filtered, used to decide whether the
|
|
32
|
+
"true"/"false"/"null" literals should be coerced (skipped for text fields).
|
|
11
33
|
:return: A sanitized value.
|
|
12
34
|
"""
|
|
13
35
|
if isinstance(value, list):
|
|
14
|
-
return [sanitize_filter_value(v) for v in value]
|
|
36
|
+
return [sanitize_filter_value(v, field) for v in value]
|
|
37
|
+
if field is not None and field.filter_widget_type in TEXT_FILTER_WIDGET_TYPES:
|
|
38
|
+
return value
|
|
15
39
|
match value:
|
|
16
40
|
case "false":
|
|
17
41
|
return False
|
|
@@ -64,6 +88,31 @@ def sanitize_filter_key(key: str, fields: list[ModelFieldWidgetSchema]) -> tuple
|
|
|
64
88
|
return field_name, condition
|
|
65
89
|
|
|
66
90
|
|
|
91
|
+
def build_query_filters(
|
|
92
|
+
filters: dict,
|
|
93
|
+
fields: list[ModelFieldWidgetSchema],
|
|
94
|
+
exclude: tuple[str, ...],
|
|
95
|
+
) -> dict[tuple[str, str], bool | None | str | list]:
|
|
96
|
+
"""Build the sanitized ``{(field_name, condition): value}`` filter dict.
|
|
97
|
+
|
|
98
|
+
Resolves each key's field so value coercion (true/false/null) can be applied
|
|
99
|
+
in a type-aware way (see :func:`sanitize_filter_value`).
|
|
100
|
+
|
|
101
|
+
:param filters: raw filters mapping ``key -> value``.
|
|
102
|
+
:param fields: model fields with widget types.
|
|
103
|
+
:param exclude: keys to skip (search, sort_by, offset, limit).
|
|
104
|
+
:return: sanitized filters dict.
|
|
105
|
+
"""
|
|
106
|
+
result: dict[tuple[str, str], bool | None | str | list] = {}
|
|
107
|
+
for key, value in filters.items():
|
|
108
|
+
if key in exclude:
|
|
109
|
+
continue
|
|
110
|
+
field_name = key.partition("__")[0]
|
|
111
|
+
field = next((f for f in fields if f.name == field_name), None)
|
|
112
|
+
result[sanitize_filter_key(key, fields)] = sanitize_filter_value(value, field)
|
|
113
|
+
return result
|
|
114
|
+
|
|
115
|
+
|
|
67
116
|
def is_valid_uuid(uuid_to_test: str) -> bool:
|
|
68
117
|
"""Check if uuid_to_test is a valid uuid.
|
|
69
118
|
|
|
@@ -10,7 +10,7 @@ import jwt
|
|
|
10
10
|
from asgiref.sync import sync_to_async
|
|
11
11
|
|
|
12
12
|
from fastadmin.api.exceptions import AdminApiException
|
|
13
|
-
from fastadmin.api.helpers import
|
|
13
|
+
from fastadmin.api.helpers import build_query_filters
|
|
14
14
|
from fastadmin.api.schemas import (
|
|
15
15
|
ChangePasswordInputSchema,
|
|
16
16
|
ExportFormat,
|
|
@@ -233,11 +233,11 @@ class ApiService:
|
|
|
233
233
|
query_filters: dict[tuple[str, str], bool | str | None | list] | None = None
|
|
234
234
|
if query_params.filters:
|
|
235
235
|
self._validate_filters(admin_model, query_params.filters, exclude_filter_fields, fields)
|
|
236
|
-
query_filters =
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
236
|
+
query_filters = build_query_filters(
|
|
237
|
+
query_params.filters,
|
|
238
|
+
admin_model.get_model_fields_with_widget_types(),
|
|
239
|
+
exclude_filter_fields,
|
|
240
|
+
)
|
|
241
241
|
|
|
242
242
|
if query_params.sort_by:
|
|
243
243
|
if query_params.sort_by.strip("-") not in fields:
|
|
@@ -456,11 +456,11 @@ class ApiService:
|
|
|
456
456
|
query_filters: dict[tuple[str, str], bool | str | None | list] | None = None
|
|
457
457
|
if query_params.filters:
|
|
458
458
|
self._validate_filters(admin_model, query_params.filters, exclude_filter_fields, fields)
|
|
459
|
-
query_filters =
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
459
|
+
query_filters = build_query_filters(
|
|
460
|
+
query_params.filters,
|
|
461
|
+
admin_model.get_model_fields_with_widget_types(),
|
|
462
|
+
exclude_filter_fields,
|
|
463
|
+
)
|
|
464
464
|
|
|
465
465
|
if query_params.sort_by:
|
|
466
466
|
if query_params.sort_by.strip("-") not in fields:
|
|
@@ -11,6 +11,7 @@ from uuid import UUID
|
|
|
11
11
|
|
|
12
12
|
from asgiref.sync import sync_to_async
|
|
13
13
|
|
|
14
|
+
from fastadmin.api.encoders import apply_custom_encoders
|
|
14
15
|
from fastadmin.api.schemas import ExportFormat
|
|
15
16
|
from fastadmin.models.schemas import ModelFieldWidgetSchema, WidgetType
|
|
16
17
|
|
|
@@ -201,10 +202,9 @@ class BaseModelAdmin:
|
|
|
201
202
|
# Example of usage: show_full_result_count = True
|
|
202
203
|
show_full_result_count: bool = False
|
|
203
204
|
|
|
204
|
-
# By default, the list page allows sorting by all model fields
|
|
205
|
+
# By default (an empty collection), the list page allows sorting by all model fields.
|
|
205
206
|
# If you want to disable sorting for some columns, set sortable_by to a collection (e.g. list, tuple, or set)
|
|
206
|
-
# of the subset of list_display that you want to be sortable.
|
|
207
|
-
# An empty collection disables sorting for all columns.
|
|
207
|
+
# of the subset of list_display that you want to be sortable; columns not listed become non-sortable.
|
|
208
208
|
# Example of usage: sortable_by = ("mobile_number", "email")
|
|
209
209
|
sortable_by: Sequence[str] = ()
|
|
210
210
|
|
|
@@ -419,7 +419,12 @@ class BaseModelAdmin:
|
|
|
419
419
|
serialized_dict: dict[str, Any] = {}
|
|
420
420
|
for field in attributes_to_serizalize:
|
|
421
421
|
value = getattr(obj, field.column_name)
|
|
422
|
-
|
|
422
|
+
# Let user-registered encoders (e.g. a custom datetime format) take
|
|
423
|
+
# precedence over the built-in handling below.
|
|
424
|
+
encoded = apply_custom_encoders(value)
|
|
425
|
+
if encoded is not value:
|
|
426
|
+
value = encoded
|
|
427
|
+
elif isinstance(value, Decimal):
|
|
423
428
|
# Avoid scientific notation for Decimal values in API responses,
|
|
424
429
|
# e.g. 3.75E+3 -> "3750"
|
|
425
430
|
value = format(value, "f")
|
|
@@ -272,6 +272,7 @@ class PonyORMMixin:
|
|
|
272
272
|
|
|
273
273
|
search_fields = list(self.search_fields)
|
|
274
274
|
if search and search_fields:
|
|
275
|
+
model_pk_name = self.get_model_pk_name(self.model_cls)
|
|
275
276
|
ids = []
|
|
276
277
|
# Bind the user-supplied search term as a local so Pony resolves it
|
|
277
278
|
# as a parameter. Only the field path (from the admin's trusted
|
|
@@ -281,17 +282,24 @@ class PonyORMMixin:
|
|
|
281
282
|
pony_search_field = search_field.replace("__", ".")
|
|
282
283
|
qs_ids = qs.filter(f"search_term in m.{pony_search_field}.lower()")
|
|
283
284
|
objs = list(qs_ids)
|
|
284
|
-
ids += [o
|
|
285
|
-
|
|
285
|
+
ids += [getattr(o, model_pk_name) for o in objs]
|
|
286
|
+
# Bind the collected pks as a local so Pony resolves it as a
|
|
287
|
+
# parameter; only the trusted pk field path is interpolated.
|
|
288
|
+
pk_ids = set(ids) # noqa: F841 (referenced by Pony filter string)
|
|
289
|
+
qs = qs.filter(f"m.{model_pk_name} in pk_ids")
|
|
286
290
|
|
|
287
291
|
ordering = [sort_by] if sort_by else self.ordering
|
|
288
292
|
if ordering:
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
293
|
+
# Build a single order_by() call that preserves the declared field
|
|
294
|
+
# order. Pony prepends the fields of each separate order_by() call,
|
|
295
|
+
# so applying asc and desc fields in two calls would invert their
|
|
296
|
+
# relative priority (a later desc field wrongly outranks an earlier
|
|
297
|
+
# asc one).
|
|
298
|
+
order_exprs = [
|
|
299
|
+
desc(getattr(self.model_cls, o[1:])) if o.startswith("-") else getattr(self.model_cls, o)
|
|
300
|
+
for o in ordering
|
|
301
|
+
]
|
|
302
|
+
qs = qs.order_by(*order_exprs)
|
|
295
303
|
|
|
296
304
|
total = qs.count()
|
|
297
305
|
|
|
@@ -335,6 +335,21 @@ class SqlAlchemyMixin:
|
|
|
335
335
|
condition = field_with_condition[1]
|
|
336
336
|
model_field = getattr(self.model_cls, field)
|
|
337
337
|
|
|
338
|
+
rel_property = getattr(model_field, "property", None)
|
|
339
|
+
related_mapper = getattr(rel_property, "mapper", None)
|
|
340
|
+
if related_mapper is not None:
|
|
341
|
+
# Relationship field (e.g. m2m): SQLAlchemy rejects a
|
|
342
|
+
# scalar comparison against a collection, so match on the
|
|
343
|
+
# related row's pk via any()/has() instead.
|
|
344
|
+
related_cls = related_mapper.class_
|
|
345
|
+
related_pk = getattr(related_cls, self.get_model_pk_name(related_cls))
|
|
346
|
+
match_expr = related_pk.in_(value) if condition == "in" else related_pk == value
|
|
347
|
+
if getattr(rel_property, "uselist", True):
|
|
348
|
+
q.append(model_field.any(match_expr))
|
|
349
|
+
else:
|
|
350
|
+
q.append(model_field.has(match_expr))
|
|
351
|
+
continue
|
|
352
|
+
|
|
338
353
|
if condition != "in" and isinstance(model_field.expression.type, BIGINT | Integer):
|
|
339
354
|
with contextlib.suppress(ValueError, TypeError):
|
|
340
355
|
value = int(value)
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fastadmin"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.1"
|
|
8
8
|
description = "FastAdmin is an easy-to-use Admin Dashboard App for FastAPI/Flask/Django inspired by Django Admin."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|