fastapi 0.97.0__py3-none-any.whl → 0.100.0b1__py3-none-any.whl
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.
Potentially problematic release.
This version of fastapi might be problematic. Click here for more details.
- fastapi/__init__.py +1 -1
- fastapi/_compat.py +597 -0
- fastapi/applications.py +21 -22
- fastapi/datastructures.py +31 -4
- fastapi/dependencies/models.py +1 -1
- fastapi/dependencies/utils.py +76 -120
- fastapi/encoders.py +91 -13
- fastapi/exceptions.py +20 -8
- fastapi/openapi/constants.py +1 -0
- fastapi/openapi/models.py +195 -52
- fastapi/openapi/utils.py +62 -39
- fastapi/param_functions.py +15 -1
- fastapi/params.py +57 -8
- fastapi/routing.py +79 -46
- fastapi/security/oauth2.py +16 -12
- fastapi/types.py +9 -1
- fastapi/utils.py +62 -60
- {fastapi-0.97.0.dist-info → fastapi-0.100.0b1.dist-info}/METADATA +3 -3
- {fastapi-0.97.0.dist-info → fastapi-0.100.0b1.dist-info}/RECORD +21 -20
- {fastapi-0.97.0.dist-info → fastapi-0.100.0b1.dist-info}/WHEEL +0 -0
- {fastapi-0.97.0.dist-info → fastapi-0.100.0b1.dist-info}/licenses/LICENSE +0 -0
fastapi/applications.py
CHANGED
|
@@ -15,7 +15,6 @@ from typing import (
|
|
|
15
15
|
|
|
16
16
|
from fastapi import routing
|
|
17
17
|
from fastapi.datastructures import Default, DefaultPlaceholder
|
|
18
|
-
from fastapi.encoders import DictIntStrAny, SetIntStr
|
|
19
18
|
from fastapi.exception_handlers import (
|
|
20
19
|
http_exception_handler,
|
|
21
20
|
request_validation_exception_handler,
|
|
@@ -31,7 +30,7 @@ from fastapi.openapi.docs import (
|
|
|
31
30
|
)
|
|
32
31
|
from fastapi.openapi.utils import get_openapi
|
|
33
32
|
from fastapi.params import Depends
|
|
34
|
-
from fastapi.types import DecoratedCallable
|
|
33
|
+
from fastapi.types import DecoratedCallable, IncEx
|
|
35
34
|
from fastapi.utils import generate_unique_id
|
|
36
35
|
from starlette.applications import Starlette
|
|
37
36
|
from starlette.datastructures import State
|
|
@@ -297,8 +296,8 @@ class FastAPI(Starlette):
|
|
|
297
296
|
deprecated: Optional[bool] = None,
|
|
298
297
|
methods: Optional[List[str]] = None,
|
|
299
298
|
operation_id: Optional[str] = None,
|
|
300
|
-
response_model_include: Optional[
|
|
301
|
-
response_model_exclude: Optional[
|
|
299
|
+
response_model_include: Optional[IncEx] = None,
|
|
300
|
+
response_model_exclude: Optional[IncEx] = None,
|
|
302
301
|
response_model_by_alias: bool = True,
|
|
303
302
|
response_model_exclude_unset: bool = False,
|
|
304
303
|
response_model_exclude_defaults: bool = False,
|
|
@@ -355,8 +354,8 @@ class FastAPI(Starlette):
|
|
|
355
354
|
deprecated: Optional[bool] = None,
|
|
356
355
|
methods: Optional[List[str]] = None,
|
|
357
356
|
operation_id: Optional[str] = None,
|
|
358
|
-
response_model_include: Optional[
|
|
359
|
-
response_model_exclude: Optional[
|
|
357
|
+
response_model_include: Optional[IncEx] = None,
|
|
358
|
+
response_model_exclude: Optional[IncEx] = None,
|
|
360
359
|
response_model_by_alias: bool = True,
|
|
361
360
|
response_model_exclude_unset: bool = False,
|
|
362
361
|
response_model_exclude_defaults: bool = False,
|
|
@@ -476,8 +475,8 @@ class FastAPI(Starlette):
|
|
|
476
475
|
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
|
477
476
|
deprecated: Optional[bool] = None,
|
|
478
477
|
operation_id: Optional[str] = None,
|
|
479
|
-
response_model_include: Optional[
|
|
480
|
-
response_model_exclude: Optional[
|
|
478
|
+
response_model_include: Optional[IncEx] = None,
|
|
479
|
+
response_model_exclude: Optional[IncEx] = None,
|
|
481
480
|
response_model_by_alias: bool = True,
|
|
482
481
|
response_model_exclude_unset: bool = False,
|
|
483
482
|
response_model_exclude_defaults: bool = False,
|
|
@@ -531,8 +530,8 @@ class FastAPI(Starlette):
|
|
|
531
530
|
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
|
532
531
|
deprecated: Optional[bool] = None,
|
|
533
532
|
operation_id: Optional[str] = None,
|
|
534
|
-
response_model_include: Optional[
|
|
535
|
-
response_model_exclude: Optional[
|
|
533
|
+
response_model_include: Optional[IncEx] = None,
|
|
534
|
+
response_model_exclude: Optional[IncEx] = None,
|
|
536
535
|
response_model_by_alias: bool = True,
|
|
537
536
|
response_model_exclude_unset: bool = False,
|
|
538
537
|
response_model_exclude_defaults: bool = False,
|
|
@@ -586,8 +585,8 @@ class FastAPI(Starlette):
|
|
|
586
585
|
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
|
587
586
|
deprecated: Optional[bool] = None,
|
|
588
587
|
operation_id: Optional[str] = None,
|
|
589
|
-
response_model_include: Optional[
|
|
590
|
-
response_model_exclude: Optional[
|
|
588
|
+
response_model_include: Optional[IncEx] = None,
|
|
589
|
+
response_model_exclude: Optional[IncEx] = None,
|
|
591
590
|
response_model_by_alias: bool = True,
|
|
592
591
|
response_model_exclude_unset: bool = False,
|
|
593
592
|
response_model_exclude_defaults: bool = False,
|
|
@@ -641,8 +640,8 @@ class FastAPI(Starlette):
|
|
|
641
640
|
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
|
642
641
|
deprecated: Optional[bool] = None,
|
|
643
642
|
operation_id: Optional[str] = None,
|
|
644
|
-
response_model_include: Optional[
|
|
645
|
-
response_model_exclude: Optional[
|
|
643
|
+
response_model_include: Optional[IncEx] = None,
|
|
644
|
+
response_model_exclude: Optional[IncEx] = None,
|
|
646
645
|
response_model_by_alias: bool = True,
|
|
647
646
|
response_model_exclude_unset: bool = False,
|
|
648
647
|
response_model_exclude_defaults: bool = False,
|
|
@@ -696,8 +695,8 @@ class FastAPI(Starlette):
|
|
|
696
695
|
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
|
697
696
|
deprecated: Optional[bool] = None,
|
|
698
697
|
operation_id: Optional[str] = None,
|
|
699
|
-
response_model_include: Optional[
|
|
700
|
-
response_model_exclude: Optional[
|
|
698
|
+
response_model_include: Optional[IncEx] = None,
|
|
699
|
+
response_model_exclude: Optional[IncEx] = None,
|
|
701
700
|
response_model_by_alias: bool = True,
|
|
702
701
|
response_model_exclude_unset: bool = False,
|
|
703
702
|
response_model_exclude_defaults: bool = False,
|
|
@@ -751,8 +750,8 @@ class FastAPI(Starlette):
|
|
|
751
750
|
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
|
752
751
|
deprecated: Optional[bool] = None,
|
|
753
752
|
operation_id: Optional[str] = None,
|
|
754
|
-
response_model_include: Optional[
|
|
755
|
-
response_model_exclude: Optional[
|
|
753
|
+
response_model_include: Optional[IncEx] = None,
|
|
754
|
+
response_model_exclude: Optional[IncEx] = None,
|
|
756
755
|
response_model_by_alias: bool = True,
|
|
757
756
|
response_model_exclude_unset: bool = False,
|
|
758
757
|
response_model_exclude_defaults: bool = False,
|
|
@@ -806,8 +805,8 @@ class FastAPI(Starlette):
|
|
|
806
805
|
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
|
807
806
|
deprecated: Optional[bool] = None,
|
|
808
807
|
operation_id: Optional[str] = None,
|
|
809
|
-
response_model_include: Optional[
|
|
810
|
-
response_model_exclude: Optional[
|
|
808
|
+
response_model_include: Optional[IncEx] = None,
|
|
809
|
+
response_model_exclude: Optional[IncEx] = None,
|
|
811
810
|
response_model_by_alias: bool = True,
|
|
812
811
|
response_model_exclude_unset: bool = False,
|
|
813
812
|
response_model_exclude_defaults: bool = False,
|
|
@@ -861,8 +860,8 @@ class FastAPI(Starlette):
|
|
|
861
860
|
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
|
|
862
861
|
deprecated: Optional[bool] = None,
|
|
863
862
|
operation_id: Optional[str] = None,
|
|
864
|
-
response_model_include: Optional[
|
|
865
|
-
response_model_exclude: Optional[
|
|
863
|
+
response_model_include: Optional[IncEx] = None,
|
|
864
|
+
response_model_exclude: Optional[IncEx] = None,
|
|
866
865
|
response_model_by_alias: bool = True,
|
|
867
866
|
response_model_exclude_unset: bool = False,
|
|
868
867
|
response_model_exclude_defaults: bool = False,
|
fastapi/datastructures.py
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
from typing import Any, Callable, Dict, Iterable, Type, TypeVar
|
|
2
|
-
|
|
1
|
+
from typing import Any, Callable, Dict, Iterable, Type, TypeVar, cast
|
|
2
|
+
|
|
3
|
+
from fastapi._compat import (
|
|
4
|
+
PYDANTIC_V2,
|
|
5
|
+
CoreSchema,
|
|
6
|
+
GetJsonSchemaHandler,
|
|
7
|
+
JsonSchemaValue,
|
|
8
|
+
general_plain_validator_function,
|
|
9
|
+
)
|
|
3
10
|
from starlette.datastructures import URL as URL # noqa: F401
|
|
4
11
|
from starlette.datastructures import Address as Address # noqa: F401
|
|
5
12
|
from starlette.datastructures import FormData as FormData # noqa: F401
|
|
@@ -21,8 +28,28 @@ class UploadFile(StarletteUploadFile):
|
|
|
21
28
|
return v
|
|
22
29
|
|
|
23
30
|
@classmethod
|
|
24
|
-
def
|
|
25
|
-
|
|
31
|
+
def _validate(cls, __input_value: Any, _: Any) -> "UploadFile":
|
|
32
|
+
if not isinstance(__input_value, StarletteUploadFile):
|
|
33
|
+
raise ValueError(f"Expected UploadFile, received: {type(__input_value)}")
|
|
34
|
+
return cast(UploadFile, __input_value)
|
|
35
|
+
|
|
36
|
+
if not PYDANTIC_V2:
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
|
|
40
|
+
field_schema.update({"type": "string", "format": "binary"})
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def __get_pydantic_json_schema__(
|
|
44
|
+
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
|
|
45
|
+
) -> JsonSchemaValue:
|
|
46
|
+
return {"type": "string", "format": "binary"}
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def __get_pydantic_core_schema__(
|
|
50
|
+
cls, source: Type[Any], handler: Callable[[Any], CoreSchema]
|
|
51
|
+
) -> CoreSchema:
|
|
52
|
+
return general_plain_validator_function(cls._validate)
|
|
26
53
|
|
|
27
54
|
|
|
28
55
|
class DefaultPlaceholder:
|
fastapi/dependencies/models.py
CHANGED
fastapi/dependencies/utils.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import dataclasses
|
|
2
1
|
import inspect
|
|
3
2
|
from contextlib import contextmanager
|
|
4
|
-
from copy import
|
|
3
|
+
from copy import deepcopy
|
|
5
4
|
from typing import (
|
|
6
5
|
Any,
|
|
7
6
|
Callable,
|
|
@@ -20,6 +19,31 @@ from typing import (
|
|
|
20
19
|
|
|
21
20
|
import anyio
|
|
22
21
|
from fastapi import params
|
|
22
|
+
from fastapi._compat import (
|
|
23
|
+
PYDANTIC_V2,
|
|
24
|
+
ErrorWrapper,
|
|
25
|
+
ModelField,
|
|
26
|
+
Required,
|
|
27
|
+
Undefined,
|
|
28
|
+
_regenerate_error_with_loc,
|
|
29
|
+
copy_field_info,
|
|
30
|
+
create_body_model,
|
|
31
|
+
evaluate_forwardref,
|
|
32
|
+
field_annotation_is_scalar,
|
|
33
|
+
get_annotation_from_field_info,
|
|
34
|
+
get_missing_field_error,
|
|
35
|
+
is_bytes_field,
|
|
36
|
+
is_bytes_sequence_field,
|
|
37
|
+
is_scalar_field,
|
|
38
|
+
is_scalar_sequence_field,
|
|
39
|
+
is_sequence_field,
|
|
40
|
+
is_uploadfile_or_nonable_uploadfile_annotation,
|
|
41
|
+
is_uploadfile_sequence_annotation,
|
|
42
|
+
lenient_issubclass,
|
|
43
|
+
sequence_types,
|
|
44
|
+
serialize_sequence_value,
|
|
45
|
+
value_is_sequence,
|
|
46
|
+
)
|
|
23
47
|
from fastapi.concurrency import (
|
|
24
48
|
AsyncExitStack,
|
|
25
49
|
asynccontextmanager,
|
|
@@ -31,50 +55,14 @@ from fastapi.security.base import SecurityBase
|
|
|
31
55
|
from fastapi.security.oauth2 import OAuth2, SecurityScopes
|
|
32
56
|
from fastapi.security.open_id_connect_url import OpenIdConnect
|
|
33
57
|
from fastapi.utils import create_response_field, get_path_param_names
|
|
34
|
-
from pydantic import
|
|
35
|
-
from pydantic.error_wrappers import ErrorWrapper
|
|
36
|
-
from pydantic.errors import MissingError
|
|
37
|
-
from pydantic.fields import (
|
|
38
|
-
SHAPE_FROZENSET,
|
|
39
|
-
SHAPE_LIST,
|
|
40
|
-
SHAPE_SEQUENCE,
|
|
41
|
-
SHAPE_SET,
|
|
42
|
-
SHAPE_SINGLETON,
|
|
43
|
-
SHAPE_TUPLE,
|
|
44
|
-
SHAPE_TUPLE_ELLIPSIS,
|
|
45
|
-
FieldInfo,
|
|
46
|
-
ModelField,
|
|
47
|
-
Required,
|
|
48
|
-
Undefined,
|
|
49
|
-
)
|
|
50
|
-
from pydantic.schema import get_annotation_from_field_info
|
|
51
|
-
from pydantic.typing import evaluate_forwardref, get_args, get_origin
|
|
52
|
-
from pydantic.utils import lenient_issubclass
|
|
58
|
+
from pydantic.fields import FieldInfo
|
|
53
59
|
from starlette.background import BackgroundTasks
|
|
54
60
|
from starlette.concurrency import run_in_threadpool
|
|
55
61
|
from starlette.datastructures import FormData, Headers, QueryParams, UploadFile
|
|
56
62
|
from starlette.requests import HTTPConnection, Request
|
|
57
63
|
from starlette.responses import Response
|
|
58
64
|
from starlette.websockets import WebSocket
|
|
59
|
-
from typing_extensions import Annotated
|
|
60
|
-
|
|
61
|
-
sequence_shapes = {
|
|
62
|
-
SHAPE_LIST,
|
|
63
|
-
SHAPE_SET,
|
|
64
|
-
SHAPE_FROZENSET,
|
|
65
|
-
SHAPE_TUPLE,
|
|
66
|
-
SHAPE_SEQUENCE,
|
|
67
|
-
SHAPE_TUPLE_ELLIPSIS,
|
|
68
|
-
}
|
|
69
|
-
sequence_types = (list, set, tuple)
|
|
70
|
-
sequence_shape_to_type = {
|
|
71
|
-
SHAPE_LIST: list,
|
|
72
|
-
SHAPE_SET: set,
|
|
73
|
-
SHAPE_TUPLE: tuple,
|
|
74
|
-
SHAPE_SEQUENCE: list,
|
|
75
|
-
SHAPE_TUPLE_ELLIPSIS: list,
|
|
76
|
-
}
|
|
77
|
-
|
|
65
|
+
from typing_extensions import Annotated, get_args, get_origin
|
|
78
66
|
|
|
79
67
|
multipart_not_installed_error = (
|
|
80
68
|
'Form data requires "python-multipart" to be installed. \n'
|
|
@@ -216,36 +204,6 @@ def get_flat_params(dependant: Dependant) -> List[ModelField]:
|
|
|
216
204
|
)
|
|
217
205
|
|
|
218
206
|
|
|
219
|
-
def is_scalar_field(field: ModelField) -> bool:
|
|
220
|
-
field_info = field.field_info
|
|
221
|
-
if not (
|
|
222
|
-
field.shape == SHAPE_SINGLETON
|
|
223
|
-
and not lenient_issubclass(field.type_, BaseModel)
|
|
224
|
-
and not lenient_issubclass(field.type_, sequence_types + (dict,))
|
|
225
|
-
and not dataclasses.is_dataclass(field.type_)
|
|
226
|
-
and not isinstance(field_info, params.Body)
|
|
227
|
-
):
|
|
228
|
-
return False
|
|
229
|
-
if field.sub_fields:
|
|
230
|
-
if not all(is_scalar_field(f) for f in field.sub_fields):
|
|
231
|
-
return False
|
|
232
|
-
return True
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
def is_scalar_sequence_field(field: ModelField) -> bool:
|
|
236
|
-
if (field.shape in sequence_shapes) and not lenient_issubclass(
|
|
237
|
-
field.type_, BaseModel
|
|
238
|
-
):
|
|
239
|
-
if field.sub_fields is not None:
|
|
240
|
-
for sub_field in field.sub_fields:
|
|
241
|
-
if not is_scalar_field(sub_field):
|
|
242
|
-
return False
|
|
243
|
-
return True
|
|
244
|
-
if lenient_issubclass(field.type_, sequence_types):
|
|
245
|
-
return True
|
|
246
|
-
return False
|
|
247
|
-
|
|
248
|
-
|
|
249
207
|
def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
|
|
250
208
|
signature = inspect.signature(call)
|
|
251
209
|
globalns = getattr(call, "__globals__", {})
|
|
@@ -364,12 +322,11 @@ def analyze_param(
|
|
|
364
322
|
is_path_param: bool,
|
|
365
323
|
) -> Tuple[Any, Optional[params.Depends], Optional[ModelField]]:
|
|
366
324
|
field_info = None
|
|
367
|
-
used_default_field_info = False
|
|
368
325
|
depends = None
|
|
369
326
|
type_annotation: Any = Any
|
|
370
327
|
if (
|
|
371
328
|
annotation is not inspect.Signature.empty
|
|
372
|
-
and get_origin(annotation) is Annotated
|
|
329
|
+
and get_origin(annotation) is Annotated
|
|
373
330
|
):
|
|
374
331
|
annotated_args = get_args(annotation)
|
|
375
332
|
type_annotation = annotated_args[0]
|
|
@@ -384,7 +341,9 @@ def analyze_param(
|
|
|
384
341
|
fastapi_annotation = next(iter(fastapi_annotations), None)
|
|
385
342
|
if isinstance(fastapi_annotation, FieldInfo):
|
|
386
343
|
# Copy `field_info` because we mutate `field_info.default` below.
|
|
387
|
-
field_info =
|
|
344
|
+
field_info = copy_field_info(
|
|
345
|
+
field_info=fastapi_annotation, annotation=annotation
|
|
346
|
+
)
|
|
388
347
|
assert field_info.default is Undefined or field_info.default is Required, (
|
|
389
348
|
f"`{field_info.__class__.__name__}` default value cannot be set in"
|
|
390
349
|
f" `Annotated` for {param_name!r}. Set the default value with `=` instead."
|
|
@@ -415,6 +374,8 @@ def analyze_param(
|
|
|
415
374
|
f" together for {param_name!r}"
|
|
416
375
|
)
|
|
417
376
|
field_info = value
|
|
377
|
+
if PYDANTIC_V2:
|
|
378
|
+
field_info.annotation = type_annotation
|
|
418
379
|
|
|
419
380
|
if depends is not None and depends.dependency is None:
|
|
420
381
|
depends.dependency = type_annotation
|
|
@@ -433,10 +394,15 @@ def analyze_param(
|
|
|
433
394
|
# We might check here that `default_value is Required`, but the fact is that the same
|
|
434
395
|
# parameter might sometimes be a path parameter and sometimes not. See
|
|
435
396
|
# `tests/test_infer_param_optionality.py` for an example.
|
|
436
|
-
field_info = params.Path()
|
|
397
|
+
field_info = params.Path(annotation=type_annotation)
|
|
398
|
+
elif is_uploadfile_or_nonable_uploadfile_annotation(
|
|
399
|
+
type_annotation
|
|
400
|
+
) or is_uploadfile_sequence_annotation(type_annotation):
|
|
401
|
+
field_info = params.File(annotation=type_annotation, default=default_value)
|
|
402
|
+
elif not field_annotation_is_scalar(annotation=type_annotation):
|
|
403
|
+
field_info = params.Body(annotation=type_annotation, default=default_value)
|
|
437
404
|
else:
|
|
438
|
-
field_info = params.Query(default=default_value)
|
|
439
|
-
used_default_field_info = True
|
|
405
|
+
field_info = params.Query(annotation=type_annotation, default=default_value)
|
|
440
406
|
|
|
441
407
|
field = None
|
|
442
408
|
if field_info is not None:
|
|
@@ -450,8 +416,8 @@ def analyze_param(
|
|
|
450
416
|
and getattr(field_info, "in_", None) is None
|
|
451
417
|
):
|
|
452
418
|
field_info.in_ = params.ParamTypes.query
|
|
453
|
-
|
|
454
|
-
|
|
419
|
+
use_annotation = get_annotation_from_field_info(
|
|
420
|
+
type_annotation,
|
|
455
421
|
field_info,
|
|
456
422
|
param_name,
|
|
457
423
|
)
|
|
@@ -459,19 +425,15 @@ def analyze_param(
|
|
|
459
425
|
alias = param_name.replace("_", "-")
|
|
460
426
|
else:
|
|
461
427
|
alias = field_info.alias or param_name
|
|
428
|
+
field_info.alias = alias
|
|
462
429
|
field = create_response_field(
|
|
463
430
|
name=param_name,
|
|
464
|
-
type_=
|
|
431
|
+
type_=use_annotation,
|
|
465
432
|
default=field_info.default,
|
|
466
433
|
alias=alias,
|
|
467
434
|
required=field_info.default in (Required, Undefined),
|
|
468
435
|
field_info=field_info,
|
|
469
436
|
)
|
|
470
|
-
if used_default_field_info:
|
|
471
|
-
if lenient_issubclass(field.type_, UploadFile):
|
|
472
|
-
field.field_info = params.File(field_info.default)
|
|
473
|
-
elif not is_scalar_field(field=field):
|
|
474
|
-
field.field_info = params.Body(field_info.default)
|
|
475
437
|
|
|
476
438
|
return type_annotation, depends, field
|
|
477
439
|
|
|
@@ -554,13 +516,13 @@ async def solve_dependencies(
|
|
|
554
516
|
dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None,
|
|
555
517
|
) -> Tuple[
|
|
556
518
|
Dict[str, Any],
|
|
557
|
-
List[
|
|
519
|
+
List[Any],
|
|
558
520
|
Optional[BackgroundTasks],
|
|
559
521
|
Response,
|
|
560
522
|
Dict[Tuple[Callable[..., Any], Tuple[str]], Any],
|
|
561
523
|
]:
|
|
562
524
|
values: Dict[str, Any] = {}
|
|
563
|
-
errors: List[
|
|
525
|
+
errors: List[Any] = []
|
|
564
526
|
if response is None:
|
|
565
527
|
response = Response()
|
|
566
528
|
del response.headers["content-length"]
|
|
@@ -674,7 +636,7 @@ async def solve_dependencies(
|
|
|
674
636
|
def request_params_to_args(
|
|
675
637
|
required_params: Sequence[ModelField],
|
|
676
638
|
received_params: Union[Mapping[str, Any], QueryParams, Headers],
|
|
677
|
-
) -> Tuple[Dict[str, Any], List[
|
|
639
|
+
) -> Tuple[Dict[str, Any], List[Any]]:
|
|
678
640
|
values = {}
|
|
679
641
|
errors = []
|
|
680
642
|
for field in required_params:
|
|
@@ -688,23 +650,19 @@ def request_params_to_args(
|
|
|
688
650
|
assert isinstance(
|
|
689
651
|
field_info, params.Param
|
|
690
652
|
), "Params must be subclasses of Param"
|
|
653
|
+
loc = (field_info.in_.value, field.alias)
|
|
691
654
|
if value is None:
|
|
692
655
|
if field.required:
|
|
693
|
-
errors.append(
|
|
694
|
-
ErrorWrapper(
|
|
695
|
-
MissingError(), loc=(field_info.in_.value, field.alias)
|
|
696
|
-
)
|
|
697
|
-
)
|
|
656
|
+
errors.append(get_missing_field_error(loc=loc))
|
|
698
657
|
else:
|
|
699
658
|
values[field.name] = deepcopy(field.default)
|
|
700
659
|
continue
|
|
701
|
-
v_, errors_ = field.validate(
|
|
702
|
-
value, values, loc=(field_info.in_.value, field.alias)
|
|
703
|
-
)
|
|
660
|
+
v_, errors_ = field.validate(value, values, loc=loc)
|
|
704
661
|
if isinstance(errors_, ErrorWrapper):
|
|
705
662
|
errors.append(errors_)
|
|
706
663
|
elif isinstance(errors_, list):
|
|
707
|
-
errors
|
|
664
|
+
new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=())
|
|
665
|
+
errors.extend(new_errors)
|
|
708
666
|
else:
|
|
709
667
|
values[field.name] = v_
|
|
710
668
|
return values, errors
|
|
@@ -713,9 +671,9 @@ def request_params_to_args(
|
|
|
713
671
|
async def request_body_to_args(
|
|
714
672
|
required_params: List[ModelField],
|
|
715
673
|
received_body: Optional[Union[Dict[str, Any], FormData]],
|
|
716
|
-
) -> Tuple[Dict[str, Any], List[
|
|
674
|
+
) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
|
|
717
675
|
values = {}
|
|
718
|
-
errors = []
|
|
676
|
+
errors: List[Dict[str, Any]] = []
|
|
719
677
|
if required_params:
|
|
720
678
|
field = required_params[0]
|
|
721
679
|
field_info = field.field_info
|
|
@@ -733,9 +691,7 @@ async def request_body_to_args(
|
|
|
733
691
|
|
|
734
692
|
value: Optional[Any] = None
|
|
735
693
|
if received_body is not None:
|
|
736
|
-
if (
|
|
737
|
-
field.shape in sequence_shapes or field.type_ in sequence_types
|
|
738
|
-
) and isinstance(received_body, FormData):
|
|
694
|
+
if (is_sequence_field(field)) and isinstance(received_body, FormData):
|
|
739
695
|
value = received_body.getlist(field.alias)
|
|
740
696
|
else:
|
|
741
697
|
try:
|
|
@@ -748,7 +704,7 @@ async def request_body_to_args(
|
|
|
748
704
|
or (isinstance(field_info, params.Form) and value == "")
|
|
749
705
|
or (
|
|
750
706
|
isinstance(field_info, params.Form)
|
|
751
|
-
and field
|
|
707
|
+
and is_sequence_field(field)
|
|
752
708
|
and len(value) == 0
|
|
753
709
|
)
|
|
754
710
|
):
|
|
@@ -759,16 +715,17 @@ async def request_body_to_args(
|
|
|
759
715
|
continue
|
|
760
716
|
if (
|
|
761
717
|
isinstance(field_info, params.File)
|
|
762
|
-
and
|
|
718
|
+
and is_bytes_field(field)
|
|
763
719
|
and isinstance(value, UploadFile)
|
|
764
720
|
):
|
|
765
721
|
value = await value.read()
|
|
766
722
|
elif (
|
|
767
|
-
field
|
|
723
|
+
is_bytes_sequence_field(field)
|
|
768
724
|
and isinstance(field_info, params.File)
|
|
769
|
-
and
|
|
770
|
-
and isinstance(value, sequence_types)
|
|
725
|
+
and value_is_sequence(value)
|
|
771
726
|
):
|
|
727
|
+
# For types
|
|
728
|
+
assert isinstance(value, sequence_types) # type: ignore[arg-type]
|
|
772
729
|
results: List[Union[bytes, str]] = []
|
|
773
730
|
|
|
774
731
|
async def process_fn(
|
|
@@ -780,24 +737,19 @@ async def request_body_to_args(
|
|
|
780
737
|
async with anyio.create_task_group() as tg:
|
|
781
738
|
for sub_value in value:
|
|
782
739
|
tg.start_soon(process_fn, sub_value.read)
|
|
783
|
-
value =
|
|
740
|
+
value = serialize_sequence_value(field=field, value=results)
|
|
784
741
|
|
|
785
742
|
v_, errors_ = field.validate(value, values, loc=loc)
|
|
786
743
|
|
|
787
|
-
if isinstance(errors_,
|
|
788
|
-
errors.append(errors_)
|
|
789
|
-
elif isinstance(errors_, list):
|
|
744
|
+
if isinstance(errors_, list):
|
|
790
745
|
errors.extend(errors_)
|
|
746
|
+
elif errors_:
|
|
747
|
+
errors.append(errors_)
|
|
791
748
|
else:
|
|
792
749
|
values[field.name] = v_
|
|
793
750
|
return values, errors
|
|
794
751
|
|
|
795
752
|
|
|
796
|
-
def get_missing_field_error(loc: Tuple[str, ...]) -> ErrorWrapper:
|
|
797
|
-
missing_field_error = ErrorWrapper(MissingError(), loc=loc)
|
|
798
|
-
return missing_field_error
|
|
799
|
-
|
|
800
|
-
|
|
801
753
|
def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]:
|
|
802
754
|
flat_dependant = get_flat_dependant(dependant)
|
|
803
755
|
if not flat_dependant.body_params:
|
|
@@ -815,12 +767,16 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]:
|
|
|
815
767
|
for param in flat_dependant.body_params:
|
|
816
768
|
setattr(param.field_info, "embed", True) # noqa: B010
|
|
817
769
|
model_name = "Body_" + name
|
|
818
|
-
BodyModel
|
|
819
|
-
|
|
820
|
-
|
|
770
|
+
BodyModel = create_body_model(
|
|
771
|
+
fields=flat_dependant.body_params, model_name=model_name
|
|
772
|
+
)
|
|
821
773
|
required = any(True for f in flat_dependant.body_params if f.required)
|
|
822
|
-
|
|
823
|
-
|
|
774
|
+
BodyFieldInfo_kwargs: Dict[str, Any] = {
|
|
775
|
+
"annotation": BodyModel,
|
|
776
|
+
"alias": "body",
|
|
777
|
+
}
|
|
778
|
+
if not required:
|
|
779
|
+
BodyFieldInfo_kwargs["default"] = None
|
|
824
780
|
if any(isinstance(f.field_info, params.File) for f in flat_dependant.body_params):
|
|
825
781
|
BodyFieldInfo: Type[params.Body] = params.File
|
|
826
782
|
elif any(isinstance(f.field_info, params.Form) for f in flat_dependant.body_params):
|
fastapi/encoders.py
CHANGED
|
@@ -1,15 +1,87 @@
|
|
|
1
1
|
import dataclasses
|
|
2
|
-
|
|
2
|
+
import datetime
|
|
3
|
+
from collections import defaultdict, deque
|
|
4
|
+
from decimal import Decimal
|
|
3
5
|
from enum import Enum
|
|
4
|
-
from
|
|
6
|
+
from ipaddress import (
|
|
7
|
+
IPv4Address,
|
|
8
|
+
IPv4Interface,
|
|
9
|
+
IPv4Network,
|
|
10
|
+
IPv6Address,
|
|
11
|
+
IPv6Interface,
|
|
12
|
+
IPv6Network,
|
|
13
|
+
)
|
|
14
|
+
from pathlib import Path, PurePath
|
|
15
|
+
from re import Pattern
|
|
5
16
|
from types import GeneratorType
|
|
6
|
-
from typing import Any, Callable, Dict, List, Optional,
|
|
17
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
|
|
18
|
+
from uuid import UUID
|
|
7
19
|
|
|
20
|
+
from fastapi.types import IncEx
|
|
8
21
|
from pydantic import BaseModel
|
|
9
|
-
from pydantic.
|
|
22
|
+
from pydantic.color import Color
|
|
23
|
+
from pydantic.networks import NameEmail
|
|
24
|
+
from pydantic.types import SecretBytes, SecretStr
|
|
10
25
|
|
|
11
|
-
|
|
12
|
-
|
|
26
|
+
from ._compat import PYDANTIC_V2, MultiHostUrl, Url, _model_dump
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Taken from Pydantic v1 as is
|
|
30
|
+
def isoformat(o: Union[datetime.date, datetime.time]) -> str:
|
|
31
|
+
return o.isoformat()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Taken from Pydantic v1 as is
|
|
35
|
+
# TODO: pv2 should this return strings instead?
|
|
36
|
+
def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
|
|
37
|
+
"""
|
|
38
|
+
Encodes a Decimal as int of there's no exponent, otherwise float
|
|
39
|
+
|
|
40
|
+
This is useful when we use ConstrainedDecimal to represent Numeric(x,0)
|
|
41
|
+
where a integer (but not int typed) is used. Encoding this as a float
|
|
42
|
+
results in failed round-tripping between encode and parse.
|
|
43
|
+
Our Id type is a prime example of this.
|
|
44
|
+
|
|
45
|
+
>>> decimal_encoder(Decimal("1.0"))
|
|
46
|
+
1.0
|
|
47
|
+
|
|
48
|
+
>>> decimal_encoder(Decimal("1"))
|
|
49
|
+
1
|
|
50
|
+
"""
|
|
51
|
+
if dec_value.as_tuple().exponent >= 0: # type: ignore[operator]
|
|
52
|
+
return int(dec_value)
|
|
53
|
+
else:
|
|
54
|
+
return float(dec_value)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = {
|
|
58
|
+
bytes: lambda o: o.decode(),
|
|
59
|
+
Color: str,
|
|
60
|
+
datetime.date: isoformat,
|
|
61
|
+
datetime.datetime: isoformat,
|
|
62
|
+
datetime.time: isoformat,
|
|
63
|
+
datetime.timedelta: lambda td: td.total_seconds(),
|
|
64
|
+
Decimal: decimal_encoder,
|
|
65
|
+
Enum: lambda o: o.value,
|
|
66
|
+
frozenset: list,
|
|
67
|
+
deque: list,
|
|
68
|
+
GeneratorType: list,
|
|
69
|
+
IPv4Address: str,
|
|
70
|
+
IPv4Interface: str,
|
|
71
|
+
IPv4Network: str,
|
|
72
|
+
IPv6Address: str,
|
|
73
|
+
IPv6Interface: str,
|
|
74
|
+
IPv6Network: str,
|
|
75
|
+
NameEmail: str,
|
|
76
|
+
Path: str,
|
|
77
|
+
Pattern: lambda o: o.pattern,
|
|
78
|
+
SecretBytes: str,
|
|
79
|
+
SecretStr: str,
|
|
80
|
+
set: list,
|
|
81
|
+
UUID: str,
|
|
82
|
+
Url: str,
|
|
83
|
+
MultiHostUrl: str,
|
|
84
|
+
}
|
|
13
85
|
|
|
14
86
|
|
|
15
87
|
def generate_encoders_by_class_tuples(
|
|
@@ -28,8 +100,8 @@ encoders_by_class_tuples = generate_encoders_by_class_tuples(ENCODERS_BY_TYPE)
|
|
|
28
100
|
|
|
29
101
|
def jsonable_encoder(
|
|
30
102
|
obj: Any,
|
|
31
|
-
include: Optional[
|
|
32
|
-
exclude: Optional[
|
|
103
|
+
include: Optional[IncEx] = None,
|
|
104
|
+
exclude: Optional[IncEx] = None,
|
|
33
105
|
by_alias: bool = True,
|
|
34
106
|
exclude_unset: bool = False,
|
|
35
107
|
exclude_defaults: bool = False,
|
|
@@ -50,10 +122,15 @@ def jsonable_encoder(
|
|
|
50
122
|
if exclude is not None and not isinstance(exclude, (set, dict)):
|
|
51
123
|
exclude = set(exclude)
|
|
52
124
|
if isinstance(obj, BaseModel):
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
125
|
+
# TODO: remove when deprecating Pydantic v1
|
|
126
|
+
encoders: Dict[Any, Any] = {}
|
|
127
|
+
if not PYDANTIC_V2:
|
|
128
|
+
encoders = getattr(obj.__config__, "json_encoders", {}) # type: ignore[attr-defined]
|
|
129
|
+
if custom_encoder:
|
|
130
|
+
encoders.update(custom_encoder)
|
|
131
|
+
obj_dict = _model_dump(
|
|
132
|
+
obj,
|
|
133
|
+
mode="json",
|
|
57
134
|
include=include,
|
|
58
135
|
exclude=exclude,
|
|
59
136
|
by_alias=by_alias,
|
|
@@ -67,7 +144,8 @@ def jsonable_encoder(
|
|
|
67
144
|
obj_dict,
|
|
68
145
|
exclude_none=exclude_none,
|
|
69
146
|
exclude_defaults=exclude_defaults,
|
|
70
|
-
|
|
147
|
+
# TODO: remove when deprecating Pydantic v1
|
|
148
|
+
custom_encoder=encoders,
|
|
71
149
|
sqlalchemy_safe=sqlalchemy_safe,
|
|
72
150
|
)
|
|
73
151
|
if dataclasses.is_dataclass(obj):
|