1
1
import inspect
2
2
from typing import Any
3
+ from typing import Callable
4
+ from typing import List
3
5
from typing import Type
6
+ from typing import Union
4
7
5
8
from fastapi import Body
6
9
from fastapi import Depends
7
10
from fastapi import Form
11
+ from fastapi import Query
8
12
from pydantic import BaseModel
9
13
from pydantic .fields import ModelField
10
14
@@ -13,12 +17,27 @@ class PydanticConverterUtils:
13
17
@classmethod
14
18
def form (cls , field : ModelField ) -> Body :
15
19
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 )
18
22
19
23
@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 (
22
41
default = default or None ,
23
42
alias = model_field .alias or None ,
24
43
title = model_field .field_info .title or None ,
@@ -32,9 +51,22 @@ def __form(cls, model_field: ModelField, default: Any):
32
51
regex = model_field .field_info .regex or None ,
33
52
)
34
53
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
+ ]
35
67
36
- class PydanticConverter :
37
68
69
+ class PydanticConverter :
38
70
@staticmethod
39
71
def body (cls : Type [BaseModel ]) -> Type [BaseModel ]:
40
72
"""
@@ -67,18 +99,13 @@ def make_form_parameter(field: ModelField) -> Any:
67
99
f"Sub-model class for { field .name } field must be decorated with"
68
100
f" `as_form` too."
69
101
)
70
- return Depends (field .type_ .body ) # noqa
102
+ return Depends (field .type_ .body ) # noqa
71
103
else :
72
104
return PydanticConverterUtils .form (field = field )
73
105
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
+ )
82
109
83
110
async def _as_form (** data ):
84
111
return cls (** data )
@@ -89,3 +116,51 @@ async def _as_form(**data):
89
116
setattr (cls , "body" , _as_form )
90
117
return cls
91
118
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