Skip to content

Commit a579840

Browse files
authored
Merge pull request #3 from dotX12/dev_query
Added support query model.
2 parents 660b843 + 5371a54 commit a579840

File tree

5 files changed

+126
-31
lines changed

5 files changed

+126
-31
lines changed

examples/main.py

+19-13
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
from fastapi import File
33
from fastapi import UploadFile
44

5-
from examples.models import PostContractJSONSchema, PostContractBodySchema
5+
from examples.models import PostContractBodySchema
6+
from examples.models import PostContractJSONSchema
67
from examples.models import PostContractSmallDoubleBodySchema
8+
from examples.models import PostContractSmallDoubleQuerySchema
79
from pyfa_converter import FormBody
10+
from pyfa_converter.depends import QueryBody
811

912
app = FastAPI()
1013

@@ -24,27 +27,30 @@ async def example_foo_body_handler(
2427
data: PostContractBodySchema = FormBody(PostContractBodySchema),
2528
document: UploadFile = File(...),
2629
):
27-
return {
28-
"title": data.title,
29-
"date": data.date,
30-
"file_name": document.filename
31-
}
30+
return {"title": data.title, "date": data.date, "file_name": document.filename}
3231

3332

3433
@app.post("/form-data-body-two")
3534
async def example_foo_body_handler(
3635
data: PostContractBodySchema = FormBody(PostContractBodySchema),
3736
document: UploadFile = File(...),
3837
):
39-
return {
40-
"title": data.title,
41-
"date": data.date,
42-
"file_name": document.filename
43-
}
38+
return {"title": data.title, "date": data.date, "file_name": document.filename}
4439

4540

4641
@app.post("/test")
4742
async def foo(
48-
data: PostContractSmallDoubleBodySchema = FormBody(PostContractSmallDoubleBodySchema),
43+
data: PostContractSmallDoubleBodySchema = FormBody(
44+
PostContractSmallDoubleBodySchema
45+
),
46+
):
47+
return {"bar": "bar"}
48+
49+
50+
@app.post("/test_query_list")
51+
async def test_list_form(
52+
data: PostContractSmallDoubleQuerySchema = QueryBody(
53+
PostContractSmallDoubleQuerySchema
54+
),
4955
):
50-
return {'bar': 'bar'}
56+
return {"data": data}

examples/models.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from datetime import datetime
22
from decimal import Decimal
3+
from typing import List
34
from typing import Optional
45

56
from pydantic import BaseModel
@@ -17,7 +18,7 @@ class PostContractJSONSchema(BaseModel):
1718
amount: Optional[Decimal] = Field(None, description="Description amount")
1819
unit_price: Optional[Decimal] = Field(None, description="Description unit_price")
1920

20-
@validator('date', each_item=True)
21+
@validator("date", each_item=True)
2122
def date_validator(cls, v: datetime):
2223
return v.date()
2324

@@ -41,5 +42,13 @@ class PostContractSmallBodySchema(PostContractSmallJSONSchema):
4142

4243
@PydanticConverter.body
4344
class PostContractSmallDoubleBodySchema(BaseModel):
44-
id: Optional[int] = Field(None, description='gwa')
45+
id: Optional[int] = Field(None, description="gwa")
4546
title: Optional[str] = Field(None)
47+
data: Optional[List[int]]
48+
49+
50+
@PydanticConverter.query
51+
class PostContractSmallDoubleQuerySchema(BaseModel):
52+
id: Optional[int] = Field(None, description="gwa")
53+
title: Optional[str] = Field(None)
54+
data: Optional[List[int]] = Field(default=[1, 2, 3])

pyfa_converter/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
from .depends import FormBody
33

44
__all__ = (
5-
'PydanticConverter',
6-
'FormBody',
5+
"PydanticConverter",
6+
"FormBody",
77
)

pyfa_converter/depends.py

+5
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,8 @@
88
class FormBody:
99
def __new__(cls, model_type: Type[BaseModel | PydanticConverter]):
1010
return Depends(model_type.body)
11+
12+
13+
class QueryBody:
14+
def __new__(cls, model_type: Type[BaseModel | PydanticConverter]):
15+
return Depends(model_type.query)

pyfa_converter/utils.py

+89-14
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import inspect
22
from typing import Any
3+
from typing import Callable
4+
from typing import List
35
from typing import Type
6+
from typing import Union
47

58
from fastapi import Body
69
from fastapi import Depends
710
from fastapi import Form
11+
from fastapi import Query
812
from pydantic import BaseModel
913
from pydantic.fields import ModelField
1014

@@ -13,12 +17,27 @@ class PydanticConverterUtils:
1317
@classmethod
1418
def form(cls, field: ModelField) -> Body:
1519
if field.required is True:
16-
return cls.__form(model_field=field, default=...)
17-
return cls.__form(model_field=field, default=field.default)
20+
return cls.__fill_params(param=Form, model_field=field, default=...)
21+
return cls.__fill_params(param=Form, model_field=field, default=field.default)
1822

1923
@classmethod
20-
def __form(cls, model_field: ModelField, default: Any):
21-
return Form(
24+
def query(cls, field: ModelField) -> Body:
25+
if field.required is True:
26+
return cls.__fill_params(param=Query, model_field=field, default=...)
27+
return cls.__fill_params(param=Query, model_field=field, default=field.default)
28+
29+
@classmethod
30+
def __base_param(cls, field: ModelField, param: Union[Form, Query]):
31+
if type(param) is Form:
32+
return cls.form(field=field)
33+
if type(param) is Query:
34+
return cls.query(field=field)
35+
36+
@classmethod
37+
def __fill_params(
38+
cls, param: Union[Form, Query], model_field: ModelField, default: Any
39+
):
40+
return param(
2241
default=default or None,
2342
alias=model_field.alias or None,
2443
title=model_field.field_info.title or None,
@@ -32,9 +51,22 @@ def __form(cls, model_field: ModelField, default: Any):
3251
regex=model_field.field_info.regex or None,
3352
)
3453

54+
@classmethod
55+
def override_signature_parameters(
56+
cls, model: Type[BaseModel], param_maker: Callable[[ModelField], Any]
57+
):
58+
return [
59+
inspect.Parameter(
60+
field.alias,
61+
inspect.Parameter.POSITIONAL_ONLY,
62+
default=param_maker(field),
63+
annotation=field.outer_type_,
64+
)
65+
for field in model.__fields__.values()
66+
]
3567

36-
class PydanticConverter:
3768

69+
class PydanticConverter:
3870
@staticmethod
3971
def body(cls: Type[BaseModel]) -> Type[BaseModel]:
4072
"""
@@ -67,18 +99,13 @@ def make_form_parameter(field: ModelField) -> Any:
6799
f"Sub-model class for {field.name} field must be decorated with"
68100
f" `as_form` too."
69101
)
70-
return Depends(field.type_.body) # noqa
102+
return Depends(field.type_.body) # noqa
71103
else:
72104
return PydanticConverterUtils.form(field=field)
73105

74-
new_params = [
75-
inspect.Parameter(
76-
field.alias,
77-
inspect.Parameter.POSITIONAL_ONLY,
78-
default=make_form_parameter(field),
79-
)
80-
for field in cls.__fields__.values()
81-
]
106+
new_params = PydanticConverterUtils.override_signature_parameters(
107+
model=cls, param_maker=make_form_parameter
108+
)
82109

83110
async def _as_form(**data):
84111
return cls(**data)
@@ -89,3 +116,51 @@ async def _as_form(**data):
89116
setattr(cls, "body", _as_form)
90117
return cls
91118

119+
@staticmethod
120+
def query(cls: Type[BaseModel]) -> Type[BaseModel]:
121+
"""
122+
Adds an `body` class method to decorated models. The `query` class
123+
method can be used with `FastAPI` endpoints.
124+
125+
Args:
126+
cls: The model class to decorate.
127+
128+
Returns:
129+
The decorated class.
130+
"""
131+
132+
def make_form_parameter(field: ModelField) -> Any:
133+
"""
134+
Converts a field from a `Pydantic` model to the appropriate `FastAPI`
135+
parameter type.
136+
137+
Args:
138+
field: The field to convert.
139+
140+
Returns:
141+
Either the result of `Form`, if the field is not a sub-model, or
142+
the result of `Depends` if it is.
143+
144+
"""
145+
if issubclass(field.type_, BaseModel):
146+
# This is a sub-model.
147+
assert hasattr(field.type_, "query"), (
148+
f"Sub-model class for {field.name} field must be decorated with"
149+
f" `as_form` too."
150+
)
151+
return Depends(field.type_.query) # noqa
152+
else:
153+
return PydanticConverterUtils.query(field=field)
154+
155+
new_params = PydanticConverterUtils.override_signature_parameters(
156+
model=cls, param_maker=make_form_parameter
157+
)
158+
159+
async def _as_form(**data):
160+
return cls(**data)
161+
162+
sig = inspect.signature(_as_form)
163+
sig = sig.replace(parameters=new_params)
164+
_as_form.__signature__ = sig
165+
setattr(cls, "query", _as_form)
166+
return cls

0 commit comments

Comments
 (0)