Skip to content

Commit 6e75f73

Browse files
committed
feat: New feature, support custom read_fields.
1 parent 6e4f3dd commit 6e75f73

5 files changed

Lines changed: 105 additions & 14 deletions

File tree

fastapi_amis_admin/crud/_sqlmodel.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import datetime
2-
import logging
32
import re
43
from enum import Enum
54
from typing import (
@@ -19,6 +18,7 @@
1918
from fastapi import APIRouter, Body, Depends, Query
2019
from fastapi.encoders import DictIntStrAny, SetIntStr
2120
from pydantic import Extra, Json
21+
from pydantic.fields import ModelField
2222
from sqlalchemy import Column, Table, delete, func, insert
2323
from sqlalchemy.future import select
2424
from sqlalchemy.orm import InstrumentedAttribute, Session
@@ -42,6 +42,7 @@
4242
SQLModelField,
4343
SQLModelFieldParser,
4444
SQLModelListField,
45+
SQLModelPropertyField,
4546
get_python_type_parse,
4647
)
4748
from .schema import BaseApiOut, ItemListSchema
@@ -228,11 +229,15 @@ def calc_filter_clause(self, data: Dict[str, Any]) -> List[BinaryExpression]:
228229
class SQLModelCrud(BaseCrud, SQLModelSelector):
229230
engine: SqlalchemyDatabase = None
230231
create_fields: List[SQLModelField] = [] # 新增数据字段
231-
readonly_fields: List[SQLModelListField] = [] # 只读字段
232-
"""readonly fields, deprecated, not recommended, will be removed in version 0.4.0"""
233-
update_fields: List[SQLModelListField] = [] # 可编辑字段
232+
readonly_fields: List[SQLModelListField] = []
233+
"""readonly fields, priority is higher than update_fields.
234+
readonly fields, deprecated, not recommended, will be removed in version 0.4.0"""
235+
update_fields: List[SQLModelPropertyField] = []
236+
"""model update fields;support model property and relationship field."""
234237
update_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None
235-
"""update exclude fields, such as: {'id', 'key', 'name'} or {'id': True, 'category': {'id', 'name'}}"""
238+
"""update exclude fields, such as: {'id', 'key', 'name'} or {'id': True, 'category': {'id', 'name'}}."""
239+
read_fields: List[SQLModelPropertyField] = []
240+
"""Model read fields; used in route_read, note the difference between readonly_fields and read_fields."""
236241

237242
def __init__(
238243
self,
@@ -246,11 +251,11 @@ def __init__(
246251
self.db = get_engine_db(self.engine)
247252
SQLModelSelector.__init__(self, model, fields)
248253
BaseCrud.__init__(self, self.model, router)
249-
if self.readonly_fields:
250-
logging.warning(
251-
"readonly fields, deprecated, not recommended, will be removed in version 0.4.0."
252-
"Please replace them with update_fields and update_exclude."
253-
)
254+
# if self.readonly_fields:
255+
# logging.warning(
256+
# "readonly fields, deprecated, not recommended, will be removed in version 0.4.0."
257+
# "Please replace them with update_fields and update_exclude."
258+
# )
254259

255260
def _create_schema_list(self) -> Type[SchemaListT]:
256261
if self.schema_list:
@@ -296,6 +301,15 @@ def _create_schema_filter(self) -> Type[SchemaFilterT]:
296301
set_none=True,
297302
)
298303

304+
def _create_schema_read(self) -> Type[SchemaReadT]:
305+
if self.schema_read:
306+
return self.schema_read
307+
if not self.read_fields:
308+
return super()._create_schema_read()
309+
self.read_fields = self.parser.filter_insfield(self.read_fields, save_class=(ModelField,))
310+
modelfields = [self.parser.get_modelfield(ins, deepcopy=True) for ins in self.read_fields]
311+
return schema_create_by_modelfield(f"{self.schema_name_prefix}Read", modelfields, orm_mode=True)
312+
299313
def _create_schema_update(self) -> Type[SchemaUpdateT]:
300314
if self.schema_update:
301315
return self.schema_update
@@ -340,7 +354,7 @@ def _fetch_item_scalars(self, session: Session, item_id: List[str]) -> List[Mode
340354
stmt = select(self.model).where(self.pk.in_(list(map(get_python_type_parse(self.pk), item_id))))
341355
return session.scalars(stmt).all()
342356

343-
def _read_items(self, session: Session, item_id: List[str]):
357+
def _read_items(self, session: Session, item_id: List[str]) -> List[SchemaReadT]:
344358
items = self._fetch_item_scalars(session, item_id)
345359
return [self.read_item(obj) for obj in items]
346360

fastapi_amis_admin/crud/parser.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
SQLModelField = Union[str, InstrumentedAttribute]
1717
SqlField = Union[InstrumentedAttribute, Label]
1818
SQLModelListField = Union[Type[SQLModel], SQLModelField, SqlField]
19+
SQLModelPropertyField = Union[Type[SQLModel], SQLModelField, "PropertyField"]
1920

2021

2122
class SQLModelFieldParser:
@@ -161,8 +162,20 @@ def _get_label_modelfield(label: Label) -> ModelField:
161162

162163

163164
def LabelField(label: Label, field: FieldInfo) -> Label:
165+
"""Use for adding FieldInfo to sqlalchemy Label type"""
164166
modelfield = _get_label_modelfield(label)
165167
field.alias = label.key
166168
modelfield.field_info = field
167169
label.__ModelField__ = modelfield
168170
return label
171+
172+
173+
class PropertyField(ModelField):
174+
"""Use this to quickly initialize a ModelField, mainly used in schema_read and schema_update"""
175+
176+
def __init__(
177+
self, *, name: str, type_: Type[Any], required: bool = False, field_info: Optional[FieldInfo] = None, **kwargs: Any
178+
) -> None:
179+
kwargs.setdefault("class_validators", {})
180+
kwargs.setdefault("model_config", BaseConfig)
181+
super().__init__(name=name, type_=type_, required=required, field_info=field_info, **kwargs)

fastapi_amis_admin/crud/utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from fastapi.params import Path
55
from pydantic import BaseConfig, BaseModel, Extra
66
from pydantic.fields import ModelField
7+
from pydantic.main import ModelMetaclass
78
from pydantic.utils import smart_deepcopy
89
from sqlalchemy.engine import Engine
910
from sqlalchemy.ext.asyncio import AsyncEngine
@@ -50,6 +51,7 @@ def schema_create_by_modelfield(
5051
**kwargs,
5152
) -> Type[BaseModel]:
5253
namespaces = namespaces or {}
54+
namespaces["Config"] = type("Config", (BaseApiSchema.Config,), {"extra": extra, **kwargs})
5355
namespaces.update({"__fields__": {}, "__annotations__": {}})
5456
for modelfield in modelfields:
5557
if set_none:
@@ -61,7 +63,7 @@ def schema_create_by_modelfield(
6163
modelfield.pre_validators.insert(0, validator_skip_blank)
6264
namespaces["__fields__"][modelfield.name] = modelfield
6365
namespaces["__annotations__"][modelfield.name] = modelfield.type_
64-
return type(schema_name, (BaseApiSchema,), namespaces, extra=extra, **kwargs) # type: ignore
66+
return ModelMetaclass(schema_name, (BaseApiSchema,), namespaces)
6567

6668

6769
def parser_str_set_list(set_str: Union[int, str]) -> List[str]:

tests/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,7 @@ class Article(PkModelMixin, CreateTimeModelMixin, table=True):
7070
user: Optional[User] = Relationship(back_populates="articles")
7171

7272
tags: List[Tag] = Relationship(back_populates="articles", link_model=ArticleTagLink)
73+
74+
@property
75+
def content_text(self):
76+
return self.content.content if self.content else ""

tests/test_crud/test_SQLModelCrud_fields.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
from starlette.requests import Request
55

66
from fastapi_amis_admin.crud import SQLModelCrud
7-
from fastapi_amis_admin.crud.parser import LabelField
7+
from fastapi_amis_admin.crud.parser import LabelField, PropertyField
88
from fastapi_amis_admin.models import Field
99
from tests.conftest import async_db as db
10-
from tests.models import Article, User
10+
from tests.models import Article, Category, User
1111

1212

1313
async def test_pk_name(app: FastAPI, async_client: AsyncClient, fake_users):
@@ -244,3 +244,61 @@ async def get_select(self, request: Request) -> Select:
244244
assert items[0]["id"] == 2
245245
assert items[0]["user__username"] == "User_2"
246246
assert items[0]["pwd"] == "password_2"
247+
248+
249+
async def test_read_fields(app: FastAPI, async_client: AsyncClient, fake_articles):
250+
class ArticleCrud(SQLModelCrud):
251+
router_prefix = "/article"
252+
read_fields = [
253+
Article.title,
254+
Article.description,
255+
# Article.category, # Relationship
256+
# Article.user # Relationship
257+
]
258+
259+
ins = ArticleCrud(Article, db.engine).register_crud()
260+
261+
app.include_router(ins.router)
262+
263+
# test schemas
264+
assert "id" not in ins.schema_read.__fields__
265+
assert "title" in ins.schema_read.__fields__
266+
assert "description" in ins.schema_read.__fields__
267+
# test api
268+
res = await async_client.get("/article/item/1")
269+
items = res.json()["data"]
270+
print(items)
271+
assert "id" not in items
272+
assert items["title"] == "Article_1"
273+
assert items["description"] == "Description_1"
274+
275+
276+
async def test_read_fields_relationship(app: FastAPI, async_client: AsyncClient, fake_articles):
277+
class ArticleCrud(SQLModelCrud):
278+
router_prefix = "/article"
279+
read_fields = [
280+
Article.title,
281+
Article.description,
282+
PropertyField(name="category", type_=Category), # Relationship attribute
283+
# Article.category, # Relationship todo support
284+
PropertyField(name="content_text", type_=str), # property attribute
285+
]
286+
287+
ins = ArticleCrud(Article, db.engine).register_crud()
288+
289+
app.include_router(ins.router)
290+
291+
# test schemas
292+
assert "id" not in ins.schema_read.__fields__
293+
assert "title" in ins.schema_read.__fields__
294+
assert "description" in ins.schema_read.__fields__
295+
assert "category" in ins.schema_read.__fields__
296+
# test api
297+
res = await async_client.get("/article/item/1")
298+
items = res.json()["data"]
299+
print(items)
300+
assert "id" not in items
301+
assert "category" in items
302+
assert items["category"]["name"] == "Category_1"
303+
assert "content_text" in items
304+
# assert items["user"]["username"] == "User_1"

0 commit comments

Comments
 (0)