Skip to content

Commit 94594d7

Browse files
committed
feat: Upgrade sqlmodel to v0.0.14(pydantic v2 and sqlalchemy v2 are supported).
1 parent 5f2133b commit 94594d7

17 files changed

Lines changed: 241 additions & 66 deletions

File tree

demo/demo-model.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from contextlib import asynccontextmanager
2+
from typing import Optional
3+
14
from fastapi import FastAPI
25
from sqlmodel import SQLModel
36

@@ -6,17 +9,26 @@
69
from fastapi_amis_admin.admin.site import AdminSite
710
from fastapi_amis_admin.models.fields import Field
811

12+
13+
@asynccontextmanager
14+
async def lifespan(app: FastAPI):
15+
print("Starting", app)
16+
# 创建初始化数据库表
17+
await site.db.async_run_sync(SQLModel.metadata.create_all, is_session=False)
18+
yield
19+
20+
921
# 创建FastAPI应用
10-
app = FastAPI()
22+
app = FastAPI(lifespan=lifespan)
1123

1224
# 创建AdminSite实例
1325
site = AdminSite(settings=Settings(database_url_async="sqlite+aiosqlite:///amisadmin.db?check_same_thread=False"))
1426

1527

1628
# 先创建一个SQLModel模型,详细请参考: https://sqlmodel.tiangolo.com/
1729
class Category(SQLModel, table=True):
18-
id: int = Field(default=None, primary_key=True, nullable=False)
19-
name: str = Field(title="CategoryName")
30+
id: Optional[int] = Field(default=None, primary_key=True, nullable=False)
31+
name: str = Field("", title="CategoryName")
2032
description: str = Field(default="", title="Description")
2133

2234

@@ -26,18 +38,13 @@ class CategoryAdmin(admin.ModelAdmin):
2638
page_schema = "Category"
2739
# 配置管理模型
2840
model = Category
41+
display_item_action_as_column = True
2942

3043

3144
# 挂载后台管理系统
3245
site.mount_app(app)
3346

3447

35-
# 创建初始化数据库表
36-
@app.on_event("startup")
37-
async def startup():
38-
await site.db.async_run_sync(SQLModel.metadata.create_all, is_session=False)
39-
40-
4148
if __name__ == "__main__":
4249
import uvicorn
4350

fastapi_amis_admin/admin/admin.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@
2020

2121
from fastapi import Body, Depends, FastAPI, HTTPException, Request
2222
from pydantic import BaseModel
23-
from pydantic.utils import deep_update
2423
from sqlalchemy import Column, Table, delete, insert
2524
from sqlalchemy.orm import InstrumentedAttribute, RelationshipProperty
2625
from sqlalchemy.sql.elements import Label
2726
from sqlalchemy.util import md5_hex
2827
from sqlalchemy_database import AsyncDatabase, Database
2928
from starlette import status
29+
from starlette.middleware.base import BaseHTTPMiddleware
3030
from starlette.responses import HTMLResponse, JSONResponse, Response
3131
from starlette.templating import Jinja2Templates
3232
from typing_extensions import Annotated, Literal
@@ -78,7 +78,7 @@
7878
parser_str_set_list,
7979
)
8080
from fastapi_amis_admin.utils.functools import cached_property
81-
from fastapi_amis_admin.utils.pydantic import ModelField, create_model_by_model, model_fields
81+
from fastapi_amis_admin.utils.pydantic import ModelField, annotation_outer_type, create_model_by_model, deep_update, model_fields
8282
from fastapi_amis_admin.utils.translation import i18n as _
8383

8484
BaseAdminT = TypeVar("BaseAdminT", bound="BaseAdmin")
@@ -748,7 +748,9 @@ async def get_list_table_api(self, request: Request) -> AmisAPI:
748748
api.data[field.name] = f"${field.name}"
749749
else:
750750
modelfield = self.parser.get_modelfield(field)
751-
if modelfield and issubclass(modelfield.type_, (datetime.datetime, datetime.date, datetime.time)):
751+
if modelfield and issubclass(
752+
annotation_outer_type(modelfield.type_), (datetime.datetime, datetime.date, datetime.time)
753+
):
752754
api.data[modelfield.alias] = f"[-]${modelfield.alias}"
753755
return api
754756

@@ -1532,7 +1534,7 @@ def mount_app(
15321534
if enable_exception_handlers:
15331535
register_exception_handlers(self.fastapi)
15341536
if enable_db_middleware:
1535-
self.db.attach_middleware(fastapi)
1537+
fastapi.add_middleware(BaseHTTPMiddleware, dispatch=self.db.asgi_dispatch)
15361538
"""Add SQLAlchemy Session middleware to the main application, and the session object will be bound to each request.
15371539
Note:
15381540
1. The session will be automatically closed when the request ends, so you don't need to close it manually.

fastapi_amis_admin/admin/extensions/admin.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,11 @@ def __init__(self, app: "AdminApp"):
124124
def get_permission_fields(self, action: str) -> Dict[str, str]:
125125
"""获取权限字段"""
126126
info = {
127-
"list": (self.schema_list, _("List display")+'-', FieldPermEnum.LIST),
128-
"filter": (self.schema_filter, _("List filter")+'-', FieldPermEnum.FILTER),
129-
"create": (self.schema_create, _("Create")+'-', FieldPermEnum.CREATE),
130-
"read": (self.schema_read, _("Read")+'-', FieldPermEnum.READ),
131-
"update": (self.schema_update, _("Update")+'-', FieldPermEnum.UPDATE),
127+
"list": (self.schema_list, _("List display") + "-", FieldPermEnum.LIST),
128+
"filter": (self.schema_filter, _("List filter") + "-", FieldPermEnum.FILTER),
129+
"create": (self.schema_create, _("Create") + "-", FieldPermEnum.CREATE),
130+
"read": (self.schema_read, _("Read") + "-", FieldPermEnum.READ),
131+
"update": (self.schema_update, _("Update") + "-", FieldPermEnum.UPDATE),
132132
}
133133
if action not in info:
134134
return {}

fastapi_amis_admin/admin/parser.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
from fastapi._compat import Undefined, field_annotation_is_scalar_sequence, field_annotation_is_sequence
66
from pydantic import BaseModel, Json
7-
from pydantic.utils import deep_update, smart_deepcopy
87

98
from fastapi_amis_admin import amis
109
from fastapi_amis_admin.amis import AmisNode
@@ -22,12 +21,14 @@
2221
PYDANTIC_V2,
2322
ModelField,
2423
annotation_outer_type,
24+
deep_update,
2525
field_allow_none,
2626
field_json_schema_extra,
2727
field_outer_type,
2828
model_config_attr,
2929
model_fields,
3030
scalar_sequence_inner_type,
31+
smart_deepcopy,
3132
)
3233
from fastapi_amis_admin.utils.translation import i18n as _
3334

fastapi_amis_admin/amis/components.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -454,14 +454,14 @@ class App(Page):
454454
api: API = None # The page configuration interface, if you want to pull the page configuration remotely,
455455
# please configure it. Return to the configuration path json>data>pages, please refer to the pages property for
456456
# the specific format.
457-
brandName: str = None # app name
457+
brandName: Template = None # app name
458458
logo: str = None # Support image address, or svg.
459459
className: str = None # css class name
460-
header: str = None # header
461-
asideBefore: str = None # The front area on the page menu.
462-
asideAfter: str = None # The front area under the page menu.
463-
footer: str = None # The page.
464-
pages: List[PageSchema] = None # Array<page configuration> specific page configuration.
460+
header: Template = None # header
461+
asideBefore: Template = None # The front area on the page menu.
462+
asideAfter: Template = None # The front area under the page menu.
463+
footer: Template = None # The page.
464+
pages: List[Union[PageSchema, dict]] = None # Array<page configuration> specific page configuration.
465465
# Usually in an array, the first layer of the array is a group, generally you only need to configure the label set,
466466
# if you don't want to group, don't configure it directly, the real page should be configured in the second
467467
# layer, that is, in the children of the first layer.
@@ -2293,7 +2293,7 @@ class ColumnOperation(TableColumn):
22932293
"""Action column"""
22942294

22952295
type: str = "operation"
2296-
buttons: List[Union[Action, AmisNode]] = None
2296+
buttons: List[Union[Action, AmisNode, dict]] = None
22972297

22982298

22992299
class ColumnImage(Image, TableColumn):

fastapi_amis_admin/crud/schema.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,10 @@ def __call__(
5050
orderBy: str = None,
5151
orderDir: str = "asc",
5252
):
53-
self.page = page if page and page > 0 else self.perPageDefault
54-
self.perPage = perPage if perPage and perPage > 0 else self.perPageDefault
53+
page = int(page or 1)
54+
self.page = page if page > 0 else 1
55+
perPage = int(perPage or self.perPageDefault)
56+
self.perPage = perPage if perPage > 0 else self.perPageDefault
5557
if self.perPageMax:
5658
self.perPage = min(self.perPage, self.perPageMax)
5759
self.show_total = show_total

fastapi_amis_admin/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from .enums import IntegerChoices, TextChoices
22

33
try:
4+
from ._sqlmodel import SQLModel
45
from .fields import Field
56

67
# must install sqlmodel to use
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from typing import Any
2+
3+
from sqlalchemy import Column
4+
from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property
5+
from sqlalchemy.orm import ColumnProperty, declared_attr
6+
from sqlalchemy.util import classproperty, memoized_property
7+
from sqlmodel import main as sqlmodel_main
8+
from sqlmodel._compat import IS_PYDANTIC_V2, SQLModelConfig, Undefined, get_type_from_field
9+
from sqlmodel.main import SQLModel as _SQLModel
10+
from sqlmodel.main import get_column_from_field as _get_column_from_field
11+
12+
from .enums import Choices
13+
from .sqltypes import ChoiceType
14+
15+
try:
16+
from functools import cached_property
17+
except ImportError:
18+
cached_property = memoized_property
19+
20+
SaColumnTypes = (
21+
Column,
22+
ColumnProperty,
23+
hybrid_property,
24+
declared_attr,
25+
)
26+
__sqlmodel_ignored_types__ = (classproperty, cached_property, memoized_property, hybrid_method, *SaColumnTypes)
27+
28+
29+
def get_column_from_field(field: Any) -> Column: # type: ignore
30+
"""support for choices enums"""
31+
if IS_PYDANTIC_V2:
32+
field_info = field
33+
else:
34+
field_info = field.field_info
35+
sa_column = getattr(field_info, "sa_column", Undefined)
36+
if isinstance(sa_column, SaColumnTypes):
37+
return sa_column
38+
if isinstance(field_info.default, SaColumnTypes):
39+
return field_info.default
40+
type_ = get_type_from_field(field)
41+
# Support for choices enums
42+
if issubclass(type_, Choices):
43+
field_info.sa_type = ChoiceType(type_)
44+
return _get_column_from_field(field)
45+
46+
47+
class SQLModel(_SQLModel):
48+
# support cached_property,hybrid_method,hybrid_property
49+
if IS_PYDANTIC_V2:
50+
model_config = SQLModelConfig(
51+
from_attributes=True,
52+
ignored_types=__sqlmodel_ignored_types__,
53+
)
54+
else:
55+
56+
class Config:
57+
orm_mode = True
58+
keep_untouched = __sqlmodel_ignored_types__
59+
60+
61+
# patch sqlmodel
62+
sqlmodel_main.get_column_from_field = get_column_from_field
63+
sqlmodel_main.SQLModel = SQLModel

fastapi_amis_admin/models/enums.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def __new__(metacls, classname, bases, classdict, **kwds):
1010
labels = []
1111
for key in classdict._member_names:
1212
value = classdict[key]
13-
if isinstance(value, (list, tuple)) and len(value) > 1 and isinstance(value[-1], (Promise, str)):
13+
if isinstance(value, (list, tuple)) and len(value) > 1 and isinstance(value[-1], str):
1414
*value, label = value
1515
value = tuple(value)
1616
else:
@@ -75,12 +75,3 @@ class TextChoices(str, Choices):
7575

7676
def _generate_next_value_(name, start, count, last_values):
7777
return name
78-
79-
80-
class Promise:
81-
"""
82-
Base class for the proxy class created in the closure of the lazy function.
83-
It's used to recognize promises in code.
84-
"""
85-
86-
pass

fastapi_amis_admin/models/fields.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
from typing import AbstractSet, Any, Callable, Dict, Mapping, Optional, Sequence, Union
1+
from typing import AbstractSet, Any, Callable, Dict, Mapping, Optional, Sequence, Type, Union
22

3-
from pydantic.fields import Undefined, UndefinedType
4-
from pydantic.typing import NoArgAnyCallable
53
from sqlalchemy import Column
6-
from sqlmodel.main import FieldInfo
4+
from sqlmodel._compat import Undefined, UndefinedType, post_init_field_info
5+
from sqlmodel.main import FieldInfo, NoArgAnyCallable
76

87
from fastapi_amis_admin.amis import FormItem, TableColumn
98

@@ -23,17 +22,23 @@ def Field(
2322
lt: Optional[float] = None,
2423
le: Optional[float] = None,
2524
multiple_of: Optional[float] = None,
25+
max_digits: Optional[int] = None,
26+
decimal_places: Optional[int] = None,
2627
min_items: Optional[int] = None,
2728
max_items: Optional[int] = None,
29+
unique_items: Optional[bool] = None,
2830
min_length: Optional[int] = None,
2931
max_length: Optional[int] = None,
3032
allow_mutation: bool = True,
3133
regex: Optional[str] = None,
32-
primary_key: bool = False,
33-
foreign_key: Optional[Any] = None,
34-
unique: bool = False,
34+
discriminator: Optional[str] = None,
35+
repr: bool = True,
36+
primary_key: Union[bool, UndefinedType] = Undefined,
37+
foreign_key: Any = Undefined,
38+
unique: Union[bool, UndefinedType] = Undefined,
3539
nullable: Union[bool, UndefinedType] = Undefined,
3640
index: Union[bool, UndefinedType] = Undefined,
41+
sa_type: Union[Type[Any], UndefinedType] = Undefined,
3742
sa_column: Union[Column, UndefinedType] = Undefined, # type: ignore
3843
sa_column_args: Union[Sequence[Any], UndefinedType] = Undefined,
3944
sa_column_kwargs: Union[Mapping[str, Any], UndefinedType] = Undefined,
@@ -63,22 +68,27 @@ def Field(
6368
lt=lt,
6469
le=le,
6570
multiple_of=multiple_of,
71+
max_digits=max_digits,
72+
decimal_places=decimal_places,
6673
min_items=min_items,
6774
max_items=max_items,
75+
unique_items=unique_items,
6876
min_length=min_length,
6977
max_length=max_length,
7078
allow_mutation=allow_mutation,
7179
regex=regex,
80+
discriminator=discriminator,
7281
repr=repr,
7382
primary_key=primary_key,
7483
foreign_key=foreign_key,
7584
unique=unique,
7685
nullable=nullable,
7786
index=index,
87+
sa_type=sa_type,
7888
sa_column=sa_column,
7989
sa_column_args=sa_column_args,
8090
sa_column_kwargs=sa_column_kwargs,
8191
**current_schema_extra,
8292
)
83-
field_info._validate()
93+
post_init_field_info(field_info)
8494
return field_info

0 commit comments

Comments
 (0)