fastapi 0.112.2__py3-none-any.whl → 0.112.4__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 +15 -0
- fastapi/dependencies/models.py +27 -48
- fastapi/dependencies/utils.py +244 -182
- fastapi/param_functions.py +2 -2
- fastapi/params.py +1 -2
- fastapi/routing.py +47 -18
- fastapi/utils.py +3 -6
- {fastapi-0.112.2.dist-info → fastapi-0.112.4.dist-info}/METADATA +3 -4
- {fastapi-0.112.2.dist-info → fastapi-0.112.4.dist-info}/RECORD +13 -13
- {fastapi-0.112.2.dist-info → fastapi-0.112.4.dist-info}/WHEEL +0 -0
- {fastapi-0.112.2.dist-info → fastapi-0.112.4.dist-info}/entry_points.txt +0 -0
- {fastapi-0.112.2.dist-info → fastapi-0.112.4.dist-info}/licenses/LICENSE +0 -0
fastapi/__init__.py
CHANGED
fastapi/_compat.py
CHANGED
|
@@ -279,6 +279,12 @@ if PYDANTIC_V2:
|
|
|
279
279
|
BodyModel: Type[BaseModel] = create_model(model_name, **field_params) # type: ignore[call-overload]
|
|
280
280
|
return BodyModel
|
|
281
281
|
|
|
282
|
+
def get_model_fields(model: Type[BaseModel]) -> List[ModelField]:
|
|
283
|
+
return [
|
|
284
|
+
ModelField(field_info=field_info, name=name)
|
|
285
|
+
for name, field_info in model.model_fields.items()
|
|
286
|
+
]
|
|
287
|
+
|
|
282
288
|
else:
|
|
283
289
|
from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX
|
|
284
290
|
from pydantic import AnyUrl as Url # noqa: F401
|
|
@@ -513,6 +519,9 @@ else:
|
|
|
513
519
|
BodyModel.__fields__[f.name] = f # type: ignore[index]
|
|
514
520
|
return BodyModel
|
|
515
521
|
|
|
522
|
+
def get_model_fields(model: Type[BaseModel]) -> List[ModelField]:
|
|
523
|
+
return list(model.__fields__.values()) # type: ignore[attr-defined]
|
|
524
|
+
|
|
516
525
|
|
|
517
526
|
def _regenerate_error_with_loc(
|
|
518
527
|
*, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...]
|
|
@@ -532,6 +541,12 @@ def _annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool:
|
|
|
532
541
|
|
|
533
542
|
|
|
534
543
|
def field_annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool:
|
|
544
|
+
origin = get_origin(annotation)
|
|
545
|
+
if origin is Union or origin is UnionType:
|
|
546
|
+
for arg in get_args(annotation):
|
|
547
|
+
if field_annotation_is_sequence(arg):
|
|
548
|
+
return True
|
|
549
|
+
return False
|
|
535
550
|
return _annotation_is_sequence(annotation) or _annotation_is_sequence(
|
|
536
551
|
get_origin(annotation)
|
|
537
552
|
)
|
fastapi/dependencies/models.py
CHANGED
|
@@ -1,58 +1,37 @@
|
|
|
1
|
-
from
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any, Callable, List, Optional, Sequence, Tuple
|
|
2
3
|
|
|
3
4
|
from fastapi._compat import ModelField
|
|
4
5
|
from fastapi.security.base import SecurityBase
|
|
5
6
|
|
|
6
7
|
|
|
8
|
+
@dataclass
|
|
7
9
|
class SecurityRequirement:
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
):
|
|
11
|
-
self.security_scheme = security_scheme
|
|
12
|
-
self.scopes = scopes
|
|
10
|
+
security_scheme: SecurityBase
|
|
11
|
+
scopes: Optional[Sequence[str]] = None
|
|
13
12
|
|
|
14
13
|
|
|
14
|
+
@dataclass
|
|
15
15
|
class Dependant:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
) -> None:
|
|
38
|
-
self.path_params = path_params or []
|
|
39
|
-
self.query_params = query_params or []
|
|
40
|
-
self.header_params = header_params or []
|
|
41
|
-
self.cookie_params = cookie_params or []
|
|
42
|
-
self.body_params = body_params or []
|
|
43
|
-
self.dependencies = dependencies or []
|
|
44
|
-
self.security_requirements = security_schemes or []
|
|
45
|
-
self.request_param_name = request_param_name
|
|
46
|
-
self.websocket_param_name = websocket_param_name
|
|
47
|
-
self.http_connection_param_name = http_connection_param_name
|
|
48
|
-
self.response_param_name = response_param_name
|
|
49
|
-
self.background_tasks_param_name = background_tasks_param_name
|
|
50
|
-
self.security_scopes = security_scopes
|
|
51
|
-
self.security_scopes_param_name = security_scopes_param_name
|
|
52
|
-
self.name = name
|
|
53
|
-
self.call = call
|
|
54
|
-
self.use_cache = use_cache
|
|
55
|
-
# Store the path to be able to re-generate a dependable from it in overrides
|
|
56
|
-
self.path = path
|
|
57
|
-
# Save the cache key at creation to optimize performance
|
|
16
|
+
path_params: List[ModelField] = field(default_factory=list)
|
|
17
|
+
query_params: List[ModelField] = field(default_factory=list)
|
|
18
|
+
header_params: List[ModelField] = field(default_factory=list)
|
|
19
|
+
cookie_params: List[ModelField] = field(default_factory=list)
|
|
20
|
+
body_params: List[ModelField] = field(default_factory=list)
|
|
21
|
+
dependencies: List["Dependant"] = field(default_factory=list)
|
|
22
|
+
security_requirements: List[SecurityRequirement] = field(default_factory=list)
|
|
23
|
+
name: Optional[str] = None
|
|
24
|
+
call: Optional[Callable[..., Any]] = None
|
|
25
|
+
request_param_name: Optional[str] = None
|
|
26
|
+
websocket_param_name: Optional[str] = None
|
|
27
|
+
http_connection_param_name: Optional[str] = None
|
|
28
|
+
response_param_name: Optional[str] = None
|
|
29
|
+
background_tasks_param_name: Optional[str] = None
|
|
30
|
+
security_scopes_param_name: Optional[str] = None
|
|
31
|
+
security_scopes: Optional[List[str]] = None
|
|
32
|
+
use_cache: bool = True
|
|
33
|
+
path: Optional[str] = None
|
|
34
|
+
cache_key: Tuple[Optional[Callable[..., Any]], Tuple[str, ...]] = field(init=False)
|
|
35
|
+
|
|
36
|
+
def __post_init__(self) -> None:
|
|
58
37
|
self.cache_key = (self.call, tuple(sorted(set(self.security_scopes or []))))
|
fastapi/dependencies/utils.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
from contextlib import AsyncExitStack, contextmanager
|
|
3
3
|
from copy import copy, deepcopy
|
|
4
|
+
from dataclasses import dataclass
|
|
4
5
|
from typing import (
|
|
5
6
|
Any,
|
|
6
7
|
Callable,
|
|
@@ -54,11 +55,17 @@ from fastapi.logger import logger
|
|
|
54
55
|
from fastapi.security.base import SecurityBase
|
|
55
56
|
from fastapi.security.oauth2 import OAuth2, SecurityScopes
|
|
56
57
|
from fastapi.security.open_id_connect_url import OpenIdConnect
|
|
57
|
-
from fastapi.utils import
|
|
58
|
+
from fastapi.utils import create_model_field, get_path_param_names
|
|
58
59
|
from pydantic.fields import FieldInfo
|
|
59
60
|
from starlette.background import BackgroundTasks as StarletteBackgroundTasks
|
|
60
61
|
from starlette.concurrency import run_in_threadpool
|
|
61
|
-
from starlette.datastructures import
|
|
62
|
+
from starlette.datastructures import (
|
|
63
|
+
FormData,
|
|
64
|
+
Headers,
|
|
65
|
+
ImmutableMultiDict,
|
|
66
|
+
QueryParams,
|
|
67
|
+
UploadFile,
|
|
68
|
+
)
|
|
62
69
|
from starlette.requests import HTTPConnection, Request
|
|
63
70
|
from starlette.responses import Response
|
|
64
71
|
from starlette.websockets import WebSocket
|
|
@@ -79,25 +86,23 @@ multipart_incorrect_install_error = (
|
|
|
79
86
|
)
|
|
80
87
|
|
|
81
88
|
|
|
82
|
-
def
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
# __version__ is available in both multiparts, and can be mocked
|
|
87
|
-
from multipart import __version__ # type: ignore
|
|
89
|
+
def ensure_multipart_is_installed() -> None:
|
|
90
|
+
try:
|
|
91
|
+
# __version__ is available in both multiparts, and can be mocked
|
|
92
|
+
from multipart import __version__ # type: ignore
|
|
88
93
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
assert __version__
|
|
95
|
+
try:
|
|
96
|
+
# parse_options_header is only available in the right multipart
|
|
97
|
+
from multipart.multipart import parse_options_header # type: ignore
|
|
93
98
|
|
|
94
|
-
|
|
95
|
-
except ImportError:
|
|
96
|
-
logger.error(multipart_incorrect_install_error)
|
|
97
|
-
raise RuntimeError(multipart_incorrect_install_error) from None
|
|
99
|
+
assert parse_options_header
|
|
98
100
|
except ImportError:
|
|
99
|
-
logger.error(
|
|
100
|
-
raise RuntimeError(
|
|
101
|
+
logger.error(multipart_incorrect_install_error)
|
|
102
|
+
raise RuntimeError(multipart_incorrect_install_error) from None
|
|
103
|
+
except ImportError:
|
|
104
|
+
logger.error(multipart_not_installed_error)
|
|
105
|
+
raise RuntimeError(multipart_not_installed_error) from None
|
|
101
106
|
|
|
102
107
|
|
|
103
108
|
def get_param_sub_dependant(
|
|
@@ -175,7 +180,7 @@ def get_flat_dependant(
|
|
|
175
180
|
header_params=dependant.header_params.copy(),
|
|
176
181
|
cookie_params=dependant.cookie_params.copy(),
|
|
177
182
|
body_params=dependant.body_params.copy(),
|
|
178
|
-
|
|
183
|
+
security_requirements=dependant.security_requirements.copy(),
|
|
179
184
|
use_cache=dependant.use_cache,
|
|
180
185
|
path=dependant.path,
|
|
181
186
|
)
|
|
@@ -258,16 +263,16 @@ def get_dependant(
|
|
|
258
263
|
)
|
|
259
264
|
for param_name, param in signature_params.items():
|
|
260
265
|
is_path_param = param_name in path_param_names
|
|
261
|
-
|
|
266
|
+
param_details = analyze_param(
|
|
262
267
|
param_name=param_name,
|
|
263
268
|
annotation=param.annotation,
|
|
264
269
|
value=param.default,
|
|
265
270
|
is_path_param=is_path_param,
|
|
266
271
|
)
|
|
267
|
-
if depends is not None:
|
|
272
|
+
if param_details.depends is not None:
|
|
268
273
|
sub_dependant = get_param_sub_dependant(
|
|
269
274
|
param_name=param_name,
|
|
270
|
-
depends=depends,
|
|
275
|
+
depends=param_details.depends,
|
|
271
276
|
path=path,
|
|
272
277
|
security_scopes=security_scopes,
|
|
273
278
|
)
|
|
@@ -275,18 +280,18 @@ def get_dependant(
|
|
|
275
280
|
continue
|
|
276
281
|
if add_non_field_param_to_dependency(
|
|
277
282
|
param_name=param_name,
|
|
278
|
-
type_annotation=type_annotation,
|
|
283
|
+
type_annotation=param_details.type_annotation,
|
|
279
284
|
dependant=dependant,
|
|
280
285
|
):
|
|
281
286
|
assert (
|
|
282
|
-
|
|
287
|
+
param_details.field is None
|
|
283
288
|
), f"Cannot specify multiple FastAPI annotations for {param_name!r}"
|
|
284
289
|
continue
|
|
285
|
-
assert
|
|
286
|
-
if
|
|
287
|
-
dependant.body_params.append(
|
|
290
|
+
assert param_details.field is not None
|
|
291
|
+
if isinstance(param_details.field.field_info, params.Body):
|
|
292
|
+
dependant.body_params.append(param_details.field)
|
|
288
293
|
else:
|
|
289
|
-
add_param_to_fields(field=
|
|
294
|
+
add_param_to_fields(field=param_details.field, dependant=dependant)
|
|
290
295
|
return dependant
|
|
291
296
|
|
|
292
297
|
|
|
@@ -314,13 +319,20 @@ def add_non_field_param_to_dependency(
|
|
|
314
319
|
return None
|
|
315
320
|
|
|
316
321
|
|
|
322
|
+
@dataclass
|
|
323
|
+
class ParamDetails:
|
|
324
|
+
type_annotation: Any
|
|
325
|
+
depends: Optional[params.Depends]
|
|
326
|
+
field: Optional[ModelField]
|
|
327
|
+
|
|
328
|
+
|
|
317
329
|
def analyze_param(
|
|
318
330
|
*,
|
|
319
331
|
param_name: str,
|
|
320
332
|
annotation: Any,
|
|
321
333
|
value: Any,
|
|
322
334
|
is_path_param: bool,
|
|
323
|
-
) ->
|
|
335
|
+
) -> ParamDetails:
|
|
324
336
|
field_info = None
|
|
325
337
|
depends = None
|
|
326
338
|
type_annotation: Any = Any
|
|
@@ -328,6 +340,7 @@ def analyze_param(
|
|
|
328
340
|
if annotation is not inspect.Signature.empty:
|
|
329
341
|
use_annotation = annotation
|
|
330
342
|
type_annotation = annotation
|
|
343
|
+
# Extract Annotated info
|
|
331
344
|
if get_origin(use_annotation) is Annotated:
|
|
332
345
|
annotated_args = get_args(annotation)
|
|
333
346
|
type_annotation = annotated_args[0]
|
|
@@ -347,6 +360,7 @@ def analyze_param(
|
|
|
347
360
|
)
|
|
348
361
|
else:
|
|
349
362
|
fastapi_annotation = None
|
|
363
|
+
# Set default for Annotated FieldInfo
|
|
350
364
|
if isinstance(fastapi_annotation, FieldInfo):
|
|
351
365
|
# Copy `field_info` because we mutate `field_info.default` below.
|
|
352
366
|
field_info = copy_field_info(
|
|
@@ -361,9 +375,10 @@ def analyze_param(
|
|
|
361
375
|
field_info.default = value
|
|
362
376
|
else:
|
|
363
377
|
field_info.default = Required
|
|
378
|
+
# Get Annotated Depends
|
|
364
379
|
elif isinstance(fastapi_annotation, params.Depends):
|
|
365
380
|
depends = fastapi_annotation
|
|
366
|
-
|
|
381
|
+
# Get Depends from default value
|
|
367
382
|
if isinstance(value, params.Depends):
|
|
368
383
|
assert depends is None, (
|
|
369
384
|
"Cannot specify `Depends` in `Annotated` and default value"
|
|
@@ -374,6 +389,7 @@ def analyze_param(
|
|
|
374
389
|
f" default value together for {param_name!r}"
|
|
375
390
|
)
|
|
376
391
|
depends = value
|
|
392
|
+
# Get FieldInfo from default value
|
|
377
393
|
elif isinstance(value, FieldInfo):
|
|
378
394
|
assert field_info is None, (
|
|
379
395
|
"Cannot specify FastAPI annotations in `Annotated` and default value"
|
|
@@ -383,11 +399,13 @@ def analyze_param(
|
|
|
383
399
|
if PYDANTIC_V2:
|
|
384
400
|
field_info.annotation = type_annotation
|
|
385
401
|
|
|
402
|
+
# Get Depends from type annotation
|
|
386
403
|
if depends is not None and depends.dependency is None:
|
|
387
404
|
# Copy `depends` before mutating it
|
|
388
405
|
depends = copy(depends)
|
|
389
406
|
depends.dependency = type_annotation
|
|
390
407
|
|
|
408
|
+
# Handle non-param type annotations like Request
|
|
391
409
|
if lenient_issubclass(
|
|
392
410
|
type_annotation,
|
|
393
411
|
(
|
|
@@ -403,6 +421,7 @@ def analyze_param(
|
|
|
403
421
|
assert (
|
|
404
422
|
field_info is None
|
|
405
423
|
), f"Cannot specify FastAPI annotation for type {type_annotation!r}"
|
|
424
|
+
# Handle default assignations, neither field_info nor depends was not found in Annotated nor default value
|
|
406
425
|
elif field_info is None and depends is None:
|
|
407
426
|
default_value = value if value is not inspect.Signature.empty else Required
|
|
408
427
|
if is_path_param:
|
|
@@ -420,7 +439,9 @@ def analyze_param(
|
|
|
420
439
|
field_info = params.Query(annotation=use_annotation, default=default_value)
|
|
421
440
|
|
|
422
441
|
field = None
|
|
442
|
+
# It's a field_info, not a dependency
|
|
423
443
|
if field_info is not None:
|
|
444
|
+
# Handle field_info.in_
|
|
424
445
|
if is_path_param:
|
|
425
446
|
assert isinstance(field_info, params.Path), (
|
|
426
447
|
f"Cannot use `{field_info.__class__.__name__}` for path param"
|
|
@@ -436,12 +457,14 @@ def analyze_param(
|
|
|
436
457
|
field_info,
|
|
437
458
|
param_name,
|
|
438
459
|
)
|
|
460
|
+
if isinstance(field_info, params.Form):
|
|
461
|
+
ensure_multipart_is_installed()
|
|
439
462
|
if not field_info.alias and getattr(field_info, "convert_underscores", None):
|
|
440
463
|
alias = param_name.replace("_", "-")
|
|
441
464
|
else:
|
|
442
465
|
alias = field_info.alias or param_name
|
|
443
466
|
field_info.alias = alias
|
|
444
|
-
field =
|
|
467
|
+
field = create_model_field(
|
|
445
468
|
name=param_name,
|
|
446
469
|
type_=use_annotation_from_field_info,
|
|
447
470
|
default=field_info.default,
|
|
@@ -449,27 +472,14 @@ def analyze_param(
|
|
|
449
472
|
required=field_info.default in (Required, Undefined),
|
|
450
473
|
field_info=field_info,
|
|
451
474
|
)
|
|
475
|
+
if is_path_param:
|
|
476
|
+
assert is_scalar_field(
|
|
477
|
+
field=field
|
|
478
|
+
), "Path params must be of one of the supported types"
|
|
479
|
+
elif isinstance(field_info, params.Query):
|
|
480
|
+
assert is_scalar_field(field) or is_scalar_sequence_field(field)
|
|
452
481
|
|
|
453
|
-
return type_annotation, depends, field
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
def is_body_param(*, param_field: ModelField, is_path_param: bool) -> bool:
|
|
457
|
-
if is_path_param:
|
|
458
|
-
assert is_scalar_field(
|
|
459
|
-
field=param_field
|
|
460
|
-
), "Path params must be of one of the supported types"
|
|
461
|
-
return False
|
|
462
|
-
elif is_scalar_field(field=param_field):
|
|
463
|
-
return False
|
|
464
|
-
elif isinstance(
|
|
465
|
-
param_field.field_info, (params.Query, params.Header)
|
|
466
|
-
) and is_scalar_sequence_field(param_field):
|
|
467
|
-
return False
|
|
468
|
-
else:
|
|
469
|
-
assert isinstance(
|
|
470
|
-
param_field.field_info, params.Body
|
|
471
|
-
), f"Param: {param_field.name} can only be a request body, using Body()"
|
|
472
|
-
return True
|
|
482
|
+
return ParamDetails(type_annotation=type_annotation, depends=depends, field=field)
|
|
473
483
|
|
|
474
484
|
|
|
475
485
|
def add_param_to_fields(*, field: ModelField, dependant: Dependant) -> None:
|
|
@@ -521,6 +531,15 @@ async def solve_generator(
|
|
|
521
531
|
return await stack.enter_async_context(cm)
|
|
522
532
|
|
|
523
533
|
|
|
534
|
+
@dataclass
|
|
535
|
+
class SolvedDependency:
|
|
536
|
+
values: Dict[str, Any]
|
|
537
|
+
errors: List[Any]
|
|
538
|
+
background_tasks: Optional[StarletteBackgroundTasks]
|
|
539
|
+
response: Response
|
|
540
|
+
dependency_cache: Dict[Tuple[Callable[..., Any], Tuple[str]], Any]
|
|
541
|
+
|
|
542
|
+
|
|
524
543
|
async def solve_dependencies(
|
|
525
544
|
*,
|
|
526
545
|
request: Union[Request, WebSocket],
|
|
@@ -531,13 +550,8 @@ async def solve_dependencies(
|
|
|
531
550
|
dependency_overrides_provider: Optional[Any] = None,
|
|
532
551
|
dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None,
|
|
533
552
|
async_exit_stack: AsyncExitStack,
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
List[Any],
|
|
537
|
-
Optional[StarletteBackgroundTasks],
|
|
538
|
-
Response,
|
|
539
|
-
Dict[Tuple[Callable[..., Any], Tuple[str]], Any],
|
|
540
|
-
]:
|
|
553
|
+
embed_body_fields: bool,
|
|
554
|
+
) -> SolvedDependency:
|
|
541
555
|
values: Dict[str, Any] = {}
|
|
542
556
|
errors: List[Any] = []
|
|
543
557
|
if response is None:
|
|
@@ -578,28 +592,23 @@ async def solve_dependencies(
|
|
|
578
592
|
dependency_overrides_provider=dependency_overrides_provider,
|
|
579
593
|
dependency_cache=dependency_cache,
|
|
580
594
|
async_exit_stack=async_exit_stack,
|
|
595
|
+
embed_body_fields=embed_body_fields,
|
|
581
596
|
)
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
_, # the subdependency returns the same response we have
|
|
587
|
-
sub_dependency_cache,
|
|
588
|
-
) = solved_result
|
|
589
|
-
dependency_cache.update(sub_dependency_cache)
|
|
590
|
-
if sub_errors:
|
|
591
|
-
errors.extend(sub_errors)
|
|
597
|
+
background_tasks = solved_result.background_tasks
|
|
598
|
+
dependency_cache.update(solved_result.dependency_cache)
|
|
599
|
+
if solved_result.errors:
|
|
600
|
+
errors.extend(solved_result.errors)
|
|
592
601
|
continue
|
|
593
602
|
if sub_dependant.use_cache and sub_dependant.cache_key in dependency_cache:
|
|
594
603
|
solved = dependency_cache[sub_dependant.cache_key]
|
|
595
604
|
elif is_gen_callable(call) or is_async_gen_callable(call):
|
|
596
605
|
solved = await solve_generator(
|
|
597
|
-
call=call, stack=async_exit_stack, sub_values=
|
|
606
|
+
call=call, stack=async_exit_stack, sub_values=solved_result.values
|
|
598
607
|
)
|
|
599
608
|
elif is_coroutine_callable(call):
|
|
600
|
-
solved = await call(**
|
|
609
|
+
solved = await call(**solved_result.values)
|
|
601
610
|
else:
|
|
602
|
-
solved = await run_in_threadpool(call, **
|
|
611
|
+
solved = await run_in_threadpool(call, **solved_result.values)
|
|
603
612
|
if sub_dependant.name is not None:
|
|
604
613
|
values[sub_dependant.name] = solved
|
|
605
614
|
if sub_dependant.cache_key not in dependency_cache:
|
|
@@ -626,7 +635,9 @@ async def solve_dependencies(
|
|
|
626
635
|
body_values,
|
|
627
636
|
body_errors,
|
|
628
637
|
) = await request_body_to_args( # body_params checked above
|
|
629
|
-
|
|
638
|
+
body_fields=dependant.body_params,
|
|
639
|
+
received_body=body,
|
|
640
|
+
embed_body_fields=embed_body_fields,
|
|
630
641
|
)
|
|
631
642
|
values.update(body_values)
|
|
632
643
|
errors.extend(body_errors)
|
|
@@ -646,142 +657,194 @@ async def solve_dependencies(
|
|
|
646
657
|
values[dependant.security_scopes_param_name] = SecurityScopes(
|
|
647
658
|
scopes=dependant.security_scopes
|
|
648
659
|
)
|
|
649
|
-
return
|
|
660
|
+
return SolvedDependency(
|
|
661
|
+
values=values,
|
|
662
|
+
errors=errors,
|
|
663
|
+
background_tasks=background_tasks,
|
|
664
|
+
response=response,
|
|
665
|
+
dependency_cache=dependency_cache,
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def _validate_value_with_model_field(
|
|
670
|
+
*, field: ModelField, value: Any, values: Dict[str, Any], loc: Tuple[str, ...]
|
|
671
|
+
) -> Tuple[Any, List[Any]]:
|
|
672
|
+
if value is None:
|
|
673
|
+
if field.required:
|
|
674
|
+
return None, [get_missing_field_error(loc=loc)]
|
|
675
|
+
else:
|
|
676
|
+
return deepcopy(field.default), []
|
|
677
|
+
v_, errors_ = field.validate(value, values, loc=loc)
|
|
678
|
+
if isinstance(errors_, ErrorWrapper):
|
|
679
|
+
return None, [errors_]
|
|
680
|
+
elif isinstance(errors_, list):
|
|
681
|
+
new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=())
|
|
682
|
+
return None, new_errors
|
|
683
|
+
else:
|
|
684
|
+
return v_, []
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def _get_multidict_value(field: ModelField, values: Mapping[str, Any]) -> Any:
|
|
688
|
+
if is_sequence_field(field) and isinstance(values, (ImmutableMultiDict, Headers)):
|
|
689
|
+
value = values.getlist(field.alias)
|
|
690
|
+
else:
|
|
691
|
+
value = values.get(field.alias, None)
|
|
692
|
+
if (
|
|
693
|
+
value is None
|
|
694
|
+
or (
|
|
695
|
+
isinstance(field.field_info, params.Form)
|
|
696
|
+
and isinstance(value, str) # For type checks
|
|
697
|
+
and value == ""
|
|
698
|
+
)
|
|
699
|
+
or (is_sequence_field(field) and len(value) == 0)
|
|
700
|
+
):
|
|
701
|
+
if field.required:
|
|
702
|
+
return
|
|
703
|
+
else:
|
|
704
|
+
return deepcopy(field.default)
|
|
705
|
+
return value
|
|
650
706
|
|
|
651
707
|
|
|
652
708
|
def request_params_to_args(
|
|
653
|
-
|
|
709
|
+
fields: Sequence[ModelField],
|
|
654
710
|
received_params: Union[Mapping[str, Any], QueryParams, Headers],
|
|
655
711
|
) -> Tuple[Dict[str, Any], List[Any]]:
|
|
656
|
-
values = {}
|
|
712
|
+
values: Dict[str, Any] = {}
|
|
657
713
|
errors = []
|
|
658
|
-
for field in
|
|
659
|
-
|
|
660
|
-
received_params, (QueryParams, Headers)
|
|
661
|
-
):
|
|
662
|
-
value = received_params.getlist(field.alias) or field.default
|
|
663
|
-
else:
|
|
664
|
-
value = received_params.get(field.alias)
|
|
714
|
+
for field in fields:
|
|
715
|
+
value = _get_multidict_value(field, received_params)
|
|
665
716
|
field_info = field.field_info
|
|
666
717
|
assert isinstance(
|
|
667
718
|
field_info, params.Param
|
|
668
719
|
), "Params must be subclasses of Param"
|
|
669
720
|
loc = (field_info.in_.value, field.alias)
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
continue
|
|
676
|
-
v_, errors_ = field.validate(value, values, loc=loc)
|
|
677
|
-
if isinstance(errors_, ErrorWrapper):
|
|
678
|
-
errors.append(errors_)
|
|
679
|
-
elif isinstance(errors_, list):
|
|
680
|
-
new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=())
|
|
681
|
-
errors.extend(new_errors)
|
|
721
|
+
v_, errors_ = _validate_value_with_model_field(
|
|
722
|
+
field=field, value=value, values=values, loc=loc
|
|
723
|
+
)
|
|
724
|
+
if errors_:
|
|
725
|
+
errors.extend(errors_)
|
|
682
726
|
else:
|
|
683
727
|
values[field.name] = v_
|
|
684
728
|
return values, errors
|
|
685
729
|
|
|
686
730
|
|
|
731
|
+
def _should_embed_body_fields(fields: List[ModelField]) -> bool:
|
|
732
|
+
if not fields:
|
|
733
|
+
return False
|
|
734
|
+
# More than one dependency could have the same field, it would show up as multiple
|
|
735
|
+
# fields but it's the same one, so count them by name
|
|
736
|
+
body_param_names_set = {field.name for field in fields}
|
|
737
|
+
# A top level field has to be a single field, not multiple
|
|
738
|
+
if len(body_param_names_set) > 1:
|
|
739
|
+
return True
|
|
740
|
+
first_field = fields[0]
|
|
741
|
+
# If it explicitly specifies it is embedded, it has to be embedded
|
|
742
|
+
if getattr(first_field.field_info, "embed", None):
|
|
743
|
+
return True
|
|
744
|
+
# If it's a Form (or File) field, it has to be a BaseModel to be top level
|
|
745
|
+
# otherwise it has to be embedded, so that the key value pair can be extracted
|
|
746
|
+
if isinstance(first_field.field_info, params.Form):
|
|
747
|
+
return True
|
|
748
|
+
return False
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
async def _extract_form_body(
|
|
752
|
+
body_fields: List[ModelField],
|
|
753
|
+
received_body: FormData,
|
|
754
|
+
) -> Dict[str, Any]:
|
|
755
|
+
values = {}
|
|
756
|
+
first_field = body_fields[0]
|
|
757
|
+
first_field_info = first_field.field_info
|
|
758
|
+
|
|
759
|
+
for field in body_fields:
|
|
760
|
+
value = _get_multidict_value(field, received_body)
|
|
761
|
+
if (
|
|
762
|
+
isinstance(first_field_info, params.File)
|
|
763
|
+
and is_bytes_field(field)
|
|
764
|
+
and isinstance(value, UploadFile)
|
|
765
|
+
):
|
|
766
|
+
value = await value.read()
|
|
767
|
+
elif (
|
|
768
|
+
is_bytes_sequence_field(field)
|
|
769
|
+
and isinstance(first_field_info, params.File)
|
|
770
|
+
and value_is_sequence(value)
|
|
771
|
+
):
|
|
772
|
+
# For types
|
|
773
|
+
assert isinstance(value, sequence_types) # type: ignore[arg-type]
|
|
774
|
+
results: List[Union[bytes, str]] = []
|
|
775
|
+
|
|
776
|
+
async def process_fn(
|
|
777
|
+
fn: Callable[[], Coroutine[Any, Any, Any]],
|
|
778
|
+
) -> None:
|
|
779
|
+
result = await fn()
|
|
780
|
+
results.append(result) # noqa: B023
|
|
781
|
+
|
|
782
|
+
async with anyio.create_task_group() as tg:
|
|
783
|
+
for sub_value in value:
|
|
784
|
+
tg.start_soon(process_fn, sub_value.read)
|
|
785
|
+
value = serialize_sequence_value(field=field, value=results)
|
|
786
|
+
values[field.name] = value
|
|
787
|
+
return values
|
|
788
|
+
|
|
789
|
+
|
|
687
790
|
async def request_body_to_args(
|
|
688
|
-
|
|
791
|
+
body_fields: List[ModelField],
|
|
689
792
|
received_body: Optional[Union[Dict[str, Any], FormData]],
|
|
793
|
+
embed_body_fields: bool,
|
|
690
794
|
) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
|
|
691
|
-
values = {}
|
|
795
|
+
values: Dict[str, Any] = {}
|
|
692
796
|
errors: List[Dict[str, Any]] = []
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
except AttributeError:
|
|
716
|
-
errors.append(get_missing_field_error(loc))
|
|
717
|
-
continue
|
|
718
|
-
if (
|
|
719
|
-
value is None
|
|
720
|
-
or (isinstance(field_info, params.Form) and value == "")
|
|
721
|
-
or (
|
|
722
|
-
isinstance(field_info, params.Form)
|
|
723
|
-
and is_sequence_field(field)
|
|
724
|
-
and len(value) == 0
|
|
725
|
-
)
|
|
726
|
-
):
|
|
727
|
-
if field.required:
|
|
728
|
-
errors.append(get_missing_field_error(loc))
|
|
729
|
-
else:
|
|
730
|
-
values[field.name] = deepcopy(field.default)
|
|
797
|
+
assert body_fields, "request_body_to_args() should be called with fields"
|
|
798
|
+
single_not_embedded_field = len(body_fields) == 1 and not embed_body_fields
|
|
799
|
+
first_field = body_fields[0]
|
|
800
|
+
body_to_process = received_body
|
|
801
|
+
if isinstance(received_body, FormData):
|
|
802
|
+
body_to_process = await _extract_form_body(body_fields, received_body)
|
|
803
|
+
|
|
804
|
+
if single_not_embedded_field:
|
|
805
|
+
loc: Tuple[str, ...] = ("body",)
|
|
806
|
+
v_, errors_ = _validate_value_with_model_field(
|
|
807
|
+
field=first_field, value=body_to_process, values=values, loc=loc
|
|
808
|
+
)
|
|
809
|
+
return {first_field.name: v_}, errors_
|
|
810
|
+
for field in body_fields:
|
|
811
|
+
loc = ("body", field.alias)
|
|
812
|
+
value: Optional[Any] = None
|
|
813
|
+
if body_to_process is not None:
|
|
814
|
+
try:
|
|
815
|
+
value = body_to_process.get(field.alias)
|
|
816
|
+
# If the received body is a list, not a dict
|
|
817
|
+
except AttributeError:
|
|
818
|
+
errors.append(get_missing_field_error(loc))
|
|
731
819
|
continue
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
)
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
is_bytes_sequence_field(field)
|
|
740
|
-
and isinstance(field_info, params.File)
|
|
741
|
-
and value_is_sequence(value)
|
|
742
|
-
):
|
|
743
|
-
# For types
|
|
744
|
-
assert isinstance(value, sequence_types) # type: ignore[arg-type]
|
|
745
|
-
results: List[Union[bytes, str]] = []
|
|
746
|
-
|
|
747
|
-
async def process_fn(
|
|
748
|
-
fn: Callable[[], Coroutine[Any, Any, Any]],
|
|
749
|
-
) -> None:
|
|
750
|
-
result = await fn()
|
|
751
|
-
results.append(result) # noqa: B023
|
|
752
|
-
|
|
753
|
-
async with anyio.create_task_group() as tg:
|
|
754
|
-
for sub_value in value:
|
|
755
|
-
tg.start_soon(process_fn, sub_value.read)
|
|
756
|
-
value = serialize_sequence_value(field=field, value=results)
|
|
757
|
-
|
|
758
|
-
v_, errors_ = field.validate(value, values, loc=loc)
|
|
759
|
-
|
|
760
|
-
if isinstance(errors_, list):
|
|
761
|
-
errors.extend(errors_)
|
|
762
|
-
elif errors_:
|
|
763
|
-
errors.append(errors_)
|
|
764
|
-
else:
|
|
765
|
-
values[field.name] = v_
|
|
820
|
+
v_, errors_ = _validate_value_with_model_field(
|
|
821
|
+
field=field, value=value, values=values, loc=loc
|
|
822
|
+
)
|
|
823
|
+
if errors_:
|
|
824
|
+
errors.extend(errors_)
|
|
825
|
+
else:
|
|
826
|
+
values[field.name] = v_
|
|
766
827
|
return values, errors
|
|
767
828
|
|
|
768
829
|
|
|
769
|
-
def get_body_field(
|
|
770
|
-
flat_dependant
|
|
830
|
+
def get_body_field(
|
|
831
|
+
*, flat_dependant: Dependant, name: str, embed_body_fields: bool
|
|
832
|
+
) -> Optional[ModelField]:
|
|
833
|
+
"""
|
|
834
|
+
Get a ModelField representing the request body for a path operation, combining
|
|
835
|
+
all body parameters into a single field if necessary.
|
|
836
|
+
|
|
837
|
+
Used to check if it's form data (with `isinstance(body_field, params.Form)`)
|
|
838
|
+
or JSON and to generate the JSON Schema for a request body.
|
|
839
|
+
|
|
840
|
+
This is **not** used to validate/parse the request body, that's done with each
|
|
841
|
+
individual body parameter.
|
|
842
|
+
"""
|
|
771
843
|
if not flat_dependant.body_params:
|
|
772
844
|
return None
|
|
773
845
|
first_param = flat_dependant.body_params[0]
|
|
774
|
-
|
|
775
|
-
embed = getattr(field_info, "embed", None)
|
|
776
|
-
body_param_names_set = {param.name for param in flat_dependant.body_params}
|
|
777
|
-
if len(body_param_names_set) == 1 and not embed:
|
|
778
|
-
check_file_field(first_param)
|
|
846
|
+
if not embed_body_fields:
|
|
779
847
|
return first_param
|
|
780
|
-
# If one field requires to embed, all have to be embedded
|
|
781
|
-
# in case a sub-dependency is evaluated with a single unique body field
|
|
782
|
-
# That is combined (embedded) with other body fields
|
|
783
|
-
for param in flat_dependant.body_params:
|
|
784
|
-
setattr(param.field_info, "embed", True) # noqa: B010
|
|
785
848
|
model_name = "Body_" + name
|
|
786
849
|
BodyModel = create_body_model(
|
|
787
850
|
fields=flat_dependant.body_params, model_name=model_name
|
|
@@ -807,12 +870,11 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]:
|
|
|
807
870
|
]
|
|
808
871
|
if len(set(body_param_media_types)) == 1:
|
|
809
872
|
BodyFieldInfo_kwargs["media_type"] = body_param_media_types[0]
|
|
810
|
-
final_field =
|
|
873
|
+
final_field = create_model_field(
|
|
811
874
|
name="body",
|
|
812
875
|
type_=BodyModel,
|
|
813
876
|
required=required,
|
|
814
877
|
alias="body",
|
|
815
878
|
field_info=BodyFieldInfo(**BodyFieldInfo_kwargs),
|
|
816
879
|
)
|
|
817
|
-
check_file_field(final_field)
|
|
818
880
|
return final_field
|
fastapi/param_functions.py
CHANGED
|
@@ -1282,7 +1282,7 @@ def Body( # noqa: N802
|
|
|
1282
1282
|
),
|
|
1283
1283
|
] = _Unset,
|
|
1284
1284
|
embed: Annotated[
|
|
1285
|
-
bool,
|
|
1285
|
+
Union[bool, None],
|
|
1286
1286
|
Doc(
|
|
1287
1287
|
"""
|
|
1288
1288
|
When `embed` is `True`, the parameter will be expected in a JSON body as a
|
|
@@ -1294,7 +1294,7 @@ def Body( # noqa: N802
|
|
|
1294
1294
|
[FastAPI docs for Body - Multiple Parameters](https://fastapi.tiangolo.com/tutorial/body-multiple-params/#embed-a-single-body-parameter).
|
|
1295
1295
|
"""
|
|
1296
1296
|
),
|
|
1297
|
-
] =
|
|
1297
|
+
] = None,
|
|
1298
1298
|
media_type: Annotated[
|
|
1299
1299
|
str,
|
|
1300
1300
|
Doc(
|
fastapi/params.py
CHANGED
|
@@ -479,7 +479,7 @@ class Body(FieldInfo):
|
|
|
479
479
|
*,
|
|
480
480
|
default_factory: Union[Callable[[], Any], None] = _Unset,
|
|
481
481
|
annotation: Optional[Any] = None,
|
|
482
|
-
embed: bool =
|
|
482
|
+
embed: Union[bool, None] = None,
|
|
483
483
|
media_type: str = "application/json",
|
|
484
484
|
alias: Optional[str] = None,
|
|
485
485
|
alias_priority: Union[int, None] = _Unset,
|
|
@@ -642,7 +642,6 @@ class Form(Body):
|
|
|
642
642
|
default=default,
|
|
643
643
|
default_factory=default_factory,
|
|
644
644
|
annotation=annotation,
|
|
645
|
-
embed=True,
|
|
646
645
|
media_type=media_type,
|
|
647
646
|
alias=alias,
|
|
648
647
|
alias_priority=alias_priority,
|
fastapi/routing.py
CHANGED
|
@@ -33,8 +33,10 @@ from fastapi._compat import (
|
|
|
33
33
|
from fastapi.datastructures import Default, DefaultPlaceholder
|
|
34
34
|
from fastapi.dependencies.models import Dependant
|
|
35
35
|
from fastapi.dependencies.utils import (
|
|
36
|
+
_should_embed_body_fields,
|
|
36
37
|
get_body_field,
|
|
37
38
|
get_dependant,
|
|
39
|
+
get_flat_dependant,
|
|
38
40
|
get_parameterless_sub_dependant,
|
|
39
41
|
get_typed_return_annotation,
|
|
40
42
|
solve_dependencies,
|
|
@@ -49,7 +51,7 @@ from fastapi.exceptions import (
|
|
|
49
51
|
from fastapi.types import DecoratedCallable, IncEx
|
|
50
52
|
from fastapi.utils import (
|
|
51
53
|
create_cloned_field,
|
|
52
|
-
|
|
54
|
+
create_model_field,
|
|
53
55
|
generate_unique_id,
|
|
54
56
|
get_value_or_default,
|
|
55
57
|
is_body_allowed_for_status_code,
|
|
@@ -225,6 +227,7 @@ def get_request_handler(
|
|
|
225
227
|
response_model_exclude_defaults: bool = False,
|
|
226
228
|
response_model_exclude_none: bool = False,
|
|
227
229
|
dependency_overrides_provider: Optional[Any] = None,
|
|
230
|
+
embed_body_fields: bool = False,
|
|
228
231
|
) -> Callable[[Request], Coroutine[Any, Any, Response]]:
|
|
229
232
|
assert dependant.call is not None, "dependant.call must be a function"
|
|
230
233
|
is_coroutine = asyncio.iscoroutinefunction(dependant.call)
|
|
@@ -291,27 +294,36 @@ def get_request_handler(
|
|
|
291
294
|
body=body,
|
|
292
295
|
dependency_overrides_provider=dependency_overrides_provider,
|
|
293
296
|
async_exit_stack=async_exit_stack,
|
|
297
|
+
embed_body_fields=embed_body_fields,
|
|
294
298
|
)
|
|
295
|
-
|
|
299
|
+
errors = solved_result.errors
|
|
296
300
|
if not errors:
|
|
297
301
|
raw_response = await run_endpoint_function(
|
|
298
|
-
dependant=dependant,
|
|
302
|
+
dependant=dependant,
|
|
303
|
+
values=solved_result.values,
|
|
304
|
+
is_coroutine=is_coroutine,
|
|
299
305
|
)
|
|
300
306
|
if isinstance(raw_response, Response):
|
|
301
307
|
if raw_response.background is None:
|
|
302
|
-
raw_response.background = background_tasks
|
|
308
|
+
raw_response.background = solved_result.background_tasks
|
|
303
309
|
response = raw_response
|
|
304
310
|
else:
|
|
305
|
-
response_args: Dict[str, Any] = {
|
|
311
|
+
response_args: Dict[str, Any] = {
|
|
312
|
+
"background": solved_result.background_tasks
|
|
313
|
+
}
|
|
306
314
|
# If status_code was set, use it, otherwise use the default from the
|
|
307
315
|
# response class, in the case of redirect it's 307
|
|
308
316
|
current_status_code = (
|
|
309
|
-
status_code
|
|
317
|
+
status_code
|
|
318
|
+
if status_code
|
|
319
|
+
else solved_result.response.status_code
|
|
310
320
|
)
|
|
311
321
|
if current_status_code is not None:
|
|
312
322
|
response_args["status_code"] = current_status_code
|
|
313
|
-
if
|
|
314
|
-
response_args["status_code"] =
|
|
323
|
+
if solved_result.response.status_code:
|
|
324
|
+
response_args["status_code"] = (
|
|
325
|
+
solved_result.response.status_code
|
|
326
|
+
)
|
|
315
327
|
content = await serialize_response(
|
|
316
328
|
field=response_field,
|
|
317
329
|
response_content=raw_response,
|
|
@@ -326,7 +338,7 @@ def get_request_handler(
|
|
|
326
338
|
response = actual_response_class(content, **response_args)
|
|
327
339
|
if not is_body_allowed_for_status_code(response.status_code):
|
|
328
340
|
response.body = b""
|
|
329
|
-
response.headers.raw.extend(
|
|
341
|
+
response.headers.raw.extend(solved_result.response.headers.raw)
|
|
330
342
|
if errors:
|
|
331
343
|
validation_error = RequestValidationError(
|
|
332
344
|
_normalize_errors(errors), body=body
|
|
@@ -346,7 +358,9 @@ def get_request_handler(
|
|
|
346
358
|
|
|
347
359
|
|
|
348
360
|
def get_websocket_app(
|
|
349
|
-
dependant: Dependant,
|
|
361
|
+
dependant: Dependant,
|
|
362
|
+
dependency_overrides_provider: Optional[Any] = None,
|
|
363
|
+
embed_body_fields: bool = False,
|
|
350
364
|
) -> Callable[[WebSocket], Coroutine[Any, Any, Any]]:
|
|
351
365
|
async def app(websocket: WebSocket) -> None:
|
|
352
366
|
async with AsyncExitStack() as async_exit_stack:
|
|
@@ -359,12 +373,14 @@ def get_websocket_app(
|
|
|
359
373
|
dependant=dependant,
|
|
360
374
|
dependency_overrides_provider=dependency_overrides_provider,
|
|
361
375
|
async_exit_stack=async_exit_stack,
|
|
376
|
+
embed_body_fields=embed_body_fields,
|
|
362
377
|
)
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
378
|
+
if solved_result.errors:
|
|
379
|
+
raise WebSocketRequestValidationError(
|
|
380
|
+
_normalize_errors(solved_result.errors)
|
|
381
|
+
)
|
|
366
382
|
assert dependant.call is not None, "dependant.call must be a function"
|
|
367
|
-
await dependant.call(**values)
|
|
383
|
+
await dependant.call(**solved_result.values)
|
|
368
384
|
|
|
369
385
|
return app
|
|
370
386
|
|
|
@@ -390,11 +406,15 @@ class APIWebSocketRoute(routing.WebSocketRoute):
|
|
|
390
406
|
0,
|
|
391
407
|
get_parameterless_sub_dependant(depends=depends, path=self.path_format),
|
|
392
408
|
)
|
|
393
|
-
|
|
409
|
+
self._flat_dependant = get_flat_dependant(self.dependant)
|
|
410
|
+
self._embed_body_fields = _should_embed_body_fields(
|
|
411
|
+
self._flat_dependant.body_params
|
|
412
|
+
)
|
|
394
413
|
self.app = websocket_session(
|
|
395
414
|
get_websocket_app(
|
|
396
415
|
dependant=self.dependant,
|
|
397
416
|
dependency_overrides_provider=dependency_overrides_provider,
|
|
417
|
+
embed_body_fields=self._embed_body_fields,
|
|
398
418
|
)
|
|
399
419
|
)
|
|
400
420
|
|
|
@@ -488,7 +508,7 @@ class APIRoute(routing.Route):
|
|
|
488
508
|
status_code
|
|
489
509
|
), f"Status code {status_code} must not have a response body"
|
|
490
510
|
response_name = "Response_" + self.unique_id
|
|
491
|
-
self.response_field =
|
|
511
|
+
self.response_field = create_model_field(
|
|
492
512
|
name=response_name,
|
|
493
513
|
type_=self.response_model,
|
|
494
514
|
mode="serialization",
|
|
@@ -521,7 +541,7 @@ class APIRoute(routing.Route):
|
|
|
521
541
|
additional_status_code
|
|
522
542
|
), f"Status code {additional_status_code} must not have a response body"
|
|
523
543
|
response_name = f"Response_{additional_status_code}_{self.unique_id}"
|
|
524
|
-
response_field =
|
|
544
|
+
response_field = create_model_field(name=response_name, type_=model)
|
|
525
545
|
response_fields[additional_status_code] = response_field
|
|
526
546
|
if response_fields:
|
|
527
547
|
self.response_fields: Dict[Union[int, str], ModelField] = response_fields
|
|
@@ -535,7 +555,15 @@ class APIRoute(routing.Route):
|
|
|
535
555
|
0,
|
|
536
556
|
get_parameterless_sub_dependant(depends=depends, path=self.path_format),
|
|
537
557
|
)
|
|
538
|
-
self.
|
|
558
|
+
self._flat_dependant = get_flat_dependant(self.dependant)
|
|
559
|
+
self._embed_body_fields = _should_embed_body_fields(
|
|
560
|
+
self._flat_dependant.body_params
|
|
561
|
+
)
|
|
562
|
+
self.body_field = get_body_field(
|
|
563
|
+
flat_dependant=self._flat_dependant,
|
|
564
|
+
name=self.unique_id,
|
|
565
|
+
embed_body_fields=self._embed_body_fields,
|
|
566
|
+
)
|
|
539
567
|
self.app = request_response(self.get_route_handler())
|
|
540
568
|
|
|
541
569
|
def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]]:
|
|
@@ -552,6 +580,7 @@ class APIRoute(routing.Route):
|
|
|
552
580
|
response_model_exclude_defaults=self.response_model_exclude_defaults,
|
|
553
581
|
response_model_exclude_none=self.response_model_exclude_none,
|
|
554
582
|
dependency_overrides_provider=self.dependency_overrides_provider,
|
|
583
|
+
embed_body_fields=self._embed_body_fields,
|
|
555
584
|
)
|
|
556
585
|
|
|
557
586
|
def matches(self, scope: Scope) -> Tuple[Match, Scope]:
|
fastapi/utils.py
CHANGED
|
@@ -60,9 +60,9 @@ def get_path_param_names(path: str) -> Set[str]:
|
|
|
60
60
|
return set(re.findall("{(.*?)}", path))
|
|
61
61
|
|
|
62
62
|
|
|
63
|
-
def
|
|
63
|
+
def create_model_field(
|
|
64
64
|
name: str,
|
|
65
|
-
type_:
|
|
65
|
+
type_: Any,
|
|
66
66
|
class_validators: Optional[Dict[str, Validator]] = None,
|
|
67
67
|
default: Optional[Any] = Undefined,
|
|
68
68
|
required: Union[bool, UndefinedType] = Undefined,
|
|
@@ -71,9 +71,6 @@ def create_response_field(
|
|
|
71
71
|
alias: Optional[str] = None,
|
|
72
72
|
mode: Literal["validation", "serialization"] = "validation",
|
|
73
73
|
) -> ModelField:
|
|
74
|
-
"""
|
|
75
|
-
Create a new response field. Raises if type_ is invalid.
|
|
76
|
-
"""
|
|
77
74
|
class_validators = class_validators or {}
|
|
78
75
|
if PYDANTIC_V2:
|
|
79
76
|
field_info = field_info or FieldInfo(
|
|
@@ -135,7 +132,7 @@ def create_cloned_field(
|
|
|
135
132
|
use_type.__fields__[f.name] = create_cloned_field(
|
|
136
133
|
f, cloned_types=cloned_types
|
|
137
134
|
)
|
|
138
|
-
new_field =
|
|
135
|
+
new_field = create_model_field(name=field.name, type_=use_type)
|
|
139
136
|
new_field.has_alias = field.has_alias # type: ignore[attr-defined]
|
|
140
137
|
new_field.alias = field.alias # type: ignore[misc]
|
|
141
138
|
new_field.class_validators = field.class_validators # type: ignore[attr-defined]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fastapi
|
|
3
|
-
Version: 0.112.
|
|
3
|
+
Version: 0.112.4
|
|
4
4
|
Summary: FastAPI framework, high performance, easy to learn, fast to code, ready for production
|
|
5
5
|
Author-Email: =?utf-8?q?Sebasti=C3=A1n_Ram=C3=ADrez?= <tiangolo@gmail.com>
|
|
6
6
|
Classifier: Intended Audience :: Information Technology
|
|
@@ -115,9 +115,8 @@ The key features are:
|
|
|
115
115
|
<a href="https://bump.sh/fastapi?utm_source=fastapi&utm_medium=referral&utm_campaign=sponsor" target="_blank" title="Automate FastAPI documentation generation with Bump.sh"><img src="https://fastapi.tiangolo.com/img/sponsors/bump-sh.svg"></a>
|
|
116
116
|
<a href="https://github.com/scalar/scalar/?utm_source=fastapi&utm_medium=website&utm_campaign=main-badge" target="_blank" title="Scalar: Beautiful Open-Source API References from Swagger/OpenAPI files"><img src="https://fastapi.tiangolo.com/img/sponsors/scalar.svg"></a>
|
|
117
117
|
<a href="https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=mainbadge" target="_blank" title="Auth, user management and more for your B2B product"><img src="https://fastapi.tiangolo.com/img/sponsors/propelauth.png"></a>
|
|
118
|
-
<a href="https://docs.withcoherence.com/
|
|
118
|
+
<a href="https://docs.withcoherence.com/coherence-templates/full-stack-template/#fastapi?utm_medium=advertising&utm_source=fastapi&utm_campaign=docs" target="_blank" title="Coherence"><img src="https://fastapi.tiangolo.com/img/sponsors/coherence.png"></a>
|
|
119
119
|
<a href="https://www.mongodb.com/developer/languages/python/python-quickstart-fastapi/?utm_campaign=fastapi_framework&utm_source=fastapi_sponsorship&utm_medium=web_referral" target="_blank" title="Simplify Full Stack Development with FastAPI & MongoDB"><img src="https://fastapi.tiangolo.com/img/sponsors/mongodb.png"></a>
|
|
120
|
-
<a href="https://konghq.com/products/kong-konnect?utm_medium=referral&utm_source=github&utm_campaign=platform&utm_content=fast-api" target="_blank" title="Kong Konnect - API management platform"><img src="https://fastapi.tiangolo.com/img/sponsors/kong.png"></a>
|
|
121
120
|
<a href="https://zuplo.link/fastapi-gh" target="_blank" title="Zuplo: Scale, Protect, Document, and Monetize your FastAPI"><img src="https://fastapi.tiangolo.com/img/sponsors/zuplo.png"></a>
|
|
122
121
|
<a href="https://fine.dev?ref=fastapibadge" target="_blank" title="Fine's AI FastAPI Workflow: Effortlessly Deploy and Integrate FastAPI into Your Project"><img src="https://fastapi.tiangolo.com/img/sponsors/fine.png"></a>
|
|
123
122
|
<a href="https://liblab.com?utm_source=fastapi" target="_blank" title="liblab - Generate SDKs from FastAPI"><img src="https://fastapi.tiangolo.com/img/sponsors/liblab.png"></a>
|
|
@@ -457,7 +456,7 @@ Coming back to the previous code example, **FastAPI** will:
|
|
|
457
456
|
* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests.
|
|
458
457
|
* As the `q` parameter is declared with `= None`, it is optional.
|
|
459
458
|
* Without the `None` it would be required (as is the body in the case with `PUT`).
|
|
460
|
-
* For `PUT` requests to `/items/{item_id}`,
|
|
459
|
+
* For `PUT` requests to `/items/{item_id}`, read the body as JSON:
|
|
461
460
|
* Check that it has a required attribute `name` that should be a `str`.
|
|
462
461
|
* Check that it has a required attribute `price` that has to be a `float`.
|
|
463
462
|
* Check that it has an optional attribute `is_offer`, that should be a `bool`, if present.
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
fastapi-0.112.
|
|
2
|
-
fastapi-0.112.
|
|
3
|
-
fastapi-0.112.
|
|
4
|
-
fastapi-0.112.
|
|
5
|
-
fastapi/__init__.py,sha256=
|
|
1
|
+
fastapi-0.112.4.dist-info/METADATA,sha256=5HI7E6Ktp7E6DHqe-EAOrbdi9cOuiJqLfxqBCeJGmcQ,27273
|
|
2
|
+
fastapi-0.112.4.dist-info/WHEEL,sha256=rSwsxJWe3vzyR5HCwjWXQruDgschpei4h_giTm0dJVE,90
|
|
3
|
+
fastapi-0.112.4.dist-info/entry_points.txt,sha256=Nn2-rs4A5_lQZko2b9QqCKQx9Irx0agGbxq3QLgjBxQ,46
|
|
4
|
+
fastapi-0.112.4.dist-info/licenses/LICENSE,sha256=Tsif_IFIW5f-xYSy1KlhAy7v_oNEU4lP2cEnSQbMdE4,1086
|
|
5
|
+
fastapi/__init__.py,sha256=6SxOneGIhP_aHxYYRO2T6iyZXYjC_rjss7xd3XsWl7s,1081
|
|
6
6
|
fastapi/__main__.py,sha256=bKePXLdO4SsVSM6r9SVoLickJDcR2c0cTOxZRKq26YQ,37
|
|
7
|
-
fastapi/_compat.py,sha256=
|
|
7
|
+
fastapi/_compat.py,sha256=9AipVKxefipp4ymECk3VhrryBi-Hbh8A7zsljaWdGAk,23723
|
|
8
8
|
fastapi/applications.py,sha256=Ix-o9pQAWhEDf9J0Q1hZ0nBB1uP72c-Y3oiYzvrwqiM,176316
|
|
9
9
|
fastapi/background.py,sha256=rouLirxUANrcYC824MSMypXL_Qb2HYg2YZqaiEqbEKI,1768
|
|
10
10
|
fastapi/cli.py,sha256=OYhZb0NR_deuT5ofyPF2NoNBzZDNOP8Salef2nk-HqA,418
|
|
11
11
|
fastapi/concurrency.py,sha256=AYLnS4judDUmXsNRICtoKSP0prfYDcS8ehBtYW9JhQQ,1403
|
|
12
12
|
fastapi/datastructures.py,sha256=b2PEz77XGq-u3Ur1Inwk0AGjOsQZO49yF9C7IPJ15cY,5766
|
|
13
13
|
fastapi/dependencies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
-
fastapi/dependencies/models.py,sha256
|
|
15
|
-
fastapi/dependencies/utils.py,sha256=
|
|
14
|
+
fastapi/dependencies/models.py,sha256=Pjl6vx-4nZ5Tta9kJa3-RfQKkXtCpS09-FhMgs9eWNs,1507
|
|
15
|
+
fastapi/dependencies/utils.py,sha256=k3bs2iNotFYcwO_RihkdJjGrBAr2SNOxf6j5u7vu9gM,31980
|
|
16
16
|
fastapi/encoders.py,sha256=LvwYmFeOz4tVwvgBoC5rvZnbr7hZr73KGrU8O7zSptU,11068
|
|
17
17
|
fastapi/exception_handlers.py,sha256=MBrIOA-ugjJDivIi4rSsUJBdTsjuzN76q4yh0q1COKw,1332
|
|
18
18
|
fastapi/exceptions.py,sha256=taNixuFEXb67lI1bnX1ubq8y8TseJ4yoPlWjyP0fTzk,4969
|
|
@@ -28,12 +28,12 @@ fastapi/openapi/constants.py,sha256=adGzmis1L1HJRTE3kJ5fmHS_Noq6tIY6pWv_SFzoFDU,
|
|
|
28
28
|
fastapi/openapi/docs.py,sha256=XcQq-ZbQdC5sI0gIGu5MoHK1q-OFaqws7-ORTo6sjY4,10348
|
|
29
29
|
fastapi/openapi/models.py,sha256=PqkxQiqcEgjKuhfUIWPZPQcyTcubtUCB3vcObLsB7VE,15397
|
|
30
30
|
fastapi/openapi/utils.py,sha256=asSbOKDuagDfpByNQvPy7OM0sqOBdUmqh64BH-n-5f0,22286
|
|
31
|
-
fastapi/param_functions.py,sha256=
|
|
32
|
-
fastapi/params.py,sha256=
|
|
31
|
+
fastapi/param_functions.py,sha256=uQzNlihlhM80u4Xbstz__D3L3yxpTqLmsC-Hra1WfqE,64018
|
|
32
|
+
fastapi/params.py,sha256=e_ZHoUohqWWbquiBL5AHqdsKJmTMLZzAPii6eCYR3tk,28187
|
|
33
33
|
fastapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
34
|
fastapi/requests.py,sha256=zayepKFcienBllv3snmWI20Gk0oHNVLU4DDhqXBb4LU,142
|
|
35
35
|
fastapi/responses.py,sha256=QNQQlwpKhQoIPZTTWkpc9d_QGeGZ_aVQPaDV3nQ8m7c,1761
|
|
36
|
-
fastapi/routing.py,sha256=
|
|
36
|
+
fastapi/routing.py,sha256=LnYhRsafzHporfz4aDUmEk8qDU2tarLZNjHaKy3RB7w,176148
|
|
37
37
|
fastapi/security/__init__.py,sha256=bO8pNmxqVRXUjfl2mOKiVZLn0FpBQ61VUYVjmppnbJw,881
|
|
38
38
|
fastapi/security/api_key.py,sha256=_OqUUjEHG5_MT1IPAhXIGJRCPldTBdSww_DegFy_W8Y,9368
|
|
39
39
|
fastapi/security/base.py,sha256=dl4pvbC-RxjfbWgPtCWd8MVU-7CB2SZ22rJDXVCXO6c,141
|
|
@@ -45,6 +45,6 @@ fastapi/staticfiles.py,sha256=iirGIt3sdY2QZXd36ijs3Cj-T0FuGFda3cd90kM9Ikw,69
|
|
|
45
45
|
fastapi/templating.py,sha256=4zsuTWgcjcEainMJFAlW6-gnslm6AgOS1SiiDWfmQxk,76
|
|
46
46
|
fastapi/testclient.py,sha256=nBvaAmX66YldReJNZXPOk1sfuo2Q6hs8bOvIaCep6LQ,66
|
|
47
47
|
fastapi/types.py,sha256=nFb36sK3DSoqoyo7Miwy3meKK5UdFBgkAgLSzQlUVyI,383
|
|
48
|
-
fastapi/utils.py,sha256=
|
|
48
|
+
fastapi/utils.py,sha256=y8Bj5ttMaI9tS4D60OUgXqKnktBr99NdYUnHHV9LgoY,7948
|
|
49
49
|
fastapi/websockets.py,sha256=419uncYObEKZG0YcrXscfQQYLSWoE10jqxVMetGdR98,222
|
|
50
|
-
fastapi-0.112.
|
|
50
|
+
fastapi-0.112.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|