784 lines
28 KiB
Python
784 lines
28 KiB
Python
|
import asyncio
|
||
|
import inspect
|
||
|
from contextlib import contextmanager
|
||
|
from copy import deepcopy
|
||
|
from typing import (
|
||
|
Any,
|
||
|
Callable,
|
||
|
Dict,
|
||
|
List,
|
||
|
Mapping,
|
||
|
Optional,
|
||
|
Sequence,
|
||
|
Tuple,
|
||
|
Type,
|
||
|
Union,
|
||
|
cast,
|
||
|
)
|
||
|
|
||
|
from fastapi import params
|
||
|
from fastapi.concurrency import (
|
||
|
AsyncExitStack,
|
||
|
_fake_asynccontextmanager,
|
||
|
asynccontextmanager,
|
||
|
contextmanager_in_threadpool,
|
||
|
)
|
||
|
from fastapi.dependencies.models import Dependant, SecurityRequirement
|
||
|
from fastapi.logger import logger
|
||
|
from fastapi.security.base import SecurityBase
|
||
|
from fastapi.security.oauth2 import OAuth2, SecurityScopes
|
||
|
from fastapi.security.open_id_connect_url import OpenIdConnect
|
||
|
from fastapi.utils import create_response_field, get_path_param_names
|
||
|
from pydantic import BaseModel, create_model
|
||
|
from pydantic.error_wrappers import ErrorWrapper
|
||
|
from pydantic.errors import MissingError
|
||
|
from pydantic.fields import (
|
||
|
SHAPE_LIST,
|
||
|
SHAPE_SEQUENCE,
|
||
|
SHAPE_SET,
|
||
|
SHAPE_SINGLETON,
|
||
|
SHAPE_TUPLE,
|
||
|
SHAPE_TUPLE_ELLIPSIS,
|
||
|
FieldInfo,
|
||
|
ModelField,
|
||
|
Required,
|
||
|
)
|
||
|
from pydantic.schema import get_annotation_from_field_info
|
||
|
from pydantic.typing import ForwardRef, evaluate_forwardref
|
||
|
from pydantic.utils import lenient_issubclass
|
||
|
from starlette.background import BackgroundTasks
|
||
|
from starlette.concurrency import run_in_threadpool
|
||
|
from starlette.datastructures import FormData, Headers, QueryParams, UploadFile
|
||
|
from starlette.requests import HTTPConnection, Request
|
||
|
from starlette.responses import Response
|
||
|
from starlette.websockets import WebSocket
|
||
|
|
||
|
sequence_shapes = {
|
||
|
SHAPE_LIST,
|
||
|
SHAPE_SET,
|
||
|
SHAPE_TUPLE,
|
||
|
SHAPE_SEQUENCE,
|
||
|
SHAPE_TUPLE_ELLIPSIS,
|
||
|
}
|
||
|
sequence_types = (list, set, tuple)
|
||
|
sequence_shape_to_type = {
|
||
|
SHAPE_LIST: list,
|
||
|
SHAPE_SET: set,
|
||
|
SHAPE_TUPLE: tuple,
|
||
|
SHAPE_SEQUENCE: list,
|
||
|
SHAPE_TUPLE_ELLIPSIS: list,
|
||
|
}
|
||
|
|
||
|
|
||
|
multipart_not_installed_error = (
|
||
|
'Form data requires "python-multipart" to be installed. \n'
|
||
|
'You can install "python-multipart" with: \n\n'
|
||
|
"pip install python-multipart\n"
|
||
|
)
|
||
|
multipart_incorrect_install_error = (
|
||
|
'Form data requires "python-multipart" to be installed. '
|
||
|
'It seems you installed "multipart" instead. \n'
|
||
|
'You can remove "multipart" with: \n\n'
|
||
|
"pip uninstall multipart\n\n"
|
||
|
'And then install "python-multipart" with: \n\n'
|
||
|
"pip install python-multipart\n"
|
||
|
)
|
||
|
|
||
|
|
||
|
def check_file_field(field: ModelField) -> None:
|
||
|
field_info = field.field_info
|
||
|
if isinstance(field_info, params.Form):
|
||
|
try:
|
||
|
# __version__ is available in both multiparts, and can be mocked
|
||
|
from multipart import __version__ # type: ignore
|
||
|
|
||
|
assert __version__
|
||
|
try:
|
||
|
# parse_options_header is only available in the right multipart
|
||
|
from multipart.multipart import parse_options_header # type: ignore
|
||
|
|
||
|
assert parse_options_header
|
||
|
except ImportError:
|
||
|
logger.error(multipart_incorrect_install_error)
|
||
|
raise RuntimeError(multipart_incorrect_install_error)
|
||
|
except ImportError:
|
||
|
logger.error(multipart_not_installed_error)
|
||
|
raise RuntimeError(multipart_not_installed_error)
|
||
|
|
||
|
|
||
|
def get_param_sub_dependant(
|
||
|
*, param: inspect.Parameter, path: str, security_scopes: Optional[List[str]] = None
|
||
|
) -> Dependant:
|
||
|
depends: params.Depends = param.default
|
||
|
if depends.dependency:
|
||
|
dependency = depends.dependency
|
||
|
else:
|
||
|
dependency = param.annotation
|
||
|
return get_sub_dependant(
|
||
|
depends=depends,
|
||
|
dependency=dependency,
|
||
|
path=path,
|
||
|
name=param.name,
|
||
|
security_scopes=security_scopes,
|
||
|
)
|
||
|
|
||
|
|
||
|
def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> Dependant:
|
||
|
assert callable(
|
||
|
depends.dependency
|
||
|
), "A parameter-less dependency must have a callable dependency"
|
||
|
return get_sub_dependant(depends=depends, dependency=depends.dependency, path=path)
|
||
|
|
||
|
|
||
|
def get_sub_dependant(
|
||
|
*,
|
||
|
depends: params.Depends,
|
||
|
dependency: Callable[..., Any],
|
||
|
path: str,
|
||
|
name: Optional[str] = None,
|
||
|
security_scopes: Optional[List[str]] = None,
|
||
|
) -> Dependant:
|
||
|
security_requirement = None
|
||
|
security_scopes = security_scopes or []
|
||
|
if isinstance(depends, params.Security):
|
||
|
dependency_scopes = depends.scopes
|
||
|
security_scopes.extend(dependency_scopes)
|
||
|
if isinstance(dependency, SecurityBase):
|
||
|
use_scopes: List[str] = []
|
||
|
if isinstance(dependency, (OAuth2, OpenIdConnect)):
|
||
|
use_scopes = security_scopes
|
||
|
security_requirement = SecurityRequirement(
|
||
|
security_scheme=dependency, scopes=use_scopes
|
||
|
)
|
||
|
sub_dependant = get_dependant(
|
||
|
path=path,
|
||
|
call=dependency,
|
||
|
name=name,
|
||
|
security_scopes=security_scopes,
|
||
|
use_cache=depends.use_cache,
|
||
|
)
|
||
|
if security_requirement:
|
||
|
sub_dependant.security_requirements.append(security_requirement)
|
||
|
sub_dependant.security_scopes = security_scopes
|
||
|
return sub_dependant
|
||
|
|
||
|
|
||
|
CacheKey = Tuple[Optional[Callable[..., Any]], Tuple[str, ...]]
|
||
|
|
||
|
|
||
|
def get_flat_dependant(
|
||
|
dependant: Dependant,
|
||
|
*,
|
||
|
skip_repeats: bool = False,
|
||
|
visited: Optional[List[CacheKey]] = None,
|
||
|
) -> Dependant:
|
||
|
if visited is None:
|
||
|
visited = []
|
||
|
visited.append(dependant.cache_key)
|
||
|
|
||
|
flat_dependant = Dependant(
|
||
|
path_params=dependant.path_params.copy(),
|
||
|
query_params=dependant.query_params.copy(),
|
||
|
header_params=dependant.header_params.copy(),
|
||
|
cookie_params=dependant.cookie_params.copy(),
|
||
|
body_params=dependant.body_params.copy(),
|
||
|
security_schemes=dependant.security_requirements.copy(),
|
||
|
use_cache=dependant.use_cache,
|
||
|
path=dependant.path,
|
||
|
)
|
||
|
for sub_dependant in dependant.dependencies:
|
||
|
if skip_repeats and sub_dependant.cache_key in visited:
|
||
|
continue
|
||
|
flat_sub = get_flat_dependant(
|
||
|
sub_dependant, skip_repeats=skip_repeats, visited=visited
|
||
|
)
|
||
|
flat_dependant.path_params.extend(flat_sub.path_params)
|
||
|
flat_dependant.query_params.extend(flat_sub.query_params)
|
||
|
flat_dependant.header_params.extend(flat_sub.header_params)
|
||
|
flat_dependant.cookie_params.extend(flat_sub.cookie_params)
|
||
|
flat_dependant.body_params.extend(flat_sub.body_params)
|
||
|
flat_dependant.security_requirements.extend(flat_sub.security_requirements)
|
||
|
return flat_dependant
|
||
|
|
||
|
|
||
|
def get_flat_params(dependant: Dependant) -> List[ModelField]:
|
||
|
flat_dependant = get_flat_dependant(dependant, skip_repeats=True)
|
||
|
return (
|
||
|
flat_dependant.path_params
|
||
|
+ flat_dependant.query_params
|
||
|
+ flat_dependant.header_params
|
||
|
+ flat_dependant.cookie_params
|
||
|
)
|
||
|
|
||
|
|
||
|
def is_scalar_field(field: ModelField) -> bool:
|
||
|
field_info = field.field_info
|
||
|
if not (
|
||
|
field.shape == SHAPE_SINGLETON
|
||
|
and not lenient_issubclass(field.type_, BaseModel)
|
||
|
and not lenient_issubclass(field.type_, sequence_types + (dict,))
|
||
|
and not isinstance(field_info, params.Body)
|
||
|
):
|
||
|
return False
|
||
|
if field.sub_fields:
|
||
|
if not all(is_scalar_field(f) for f in field.sub_fields):
|
||
|
return False
|
||
|
return True
|
||
|
|
||
|
|
||
|
def is_scalar_sequence_field(field: ModelField) -> bool:
|
||
|
if (field.shape in sequence_shapes) and not lenient_issubclass(
|
||
|
field.type_, BaseModel
|
||
|
):
|
||
|
if field.sub_fields is not None:
|
||
|
for sub_field in field.sub_fields:
|
||
|
if not is_scalar_field(sub_field):
|
||
|
return False
|
||
|
return True
|
||
|
if lenient_issubclass(field.type_, sequence_types):
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
|
||
|
def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
|
||
|
signature = inspect.signature(call)
|
||
|
globalns = getattr(call, "__globals__", {})
|
||
|
typed_params = [
|
||
|
inspect.Parameter(
|
||
|
name=param.name,
|
||
|
kind=param.kind,
|
||
|
default=param.default,
|
||
|
annotation=get_typed_annotation(param, globalns),
|
||
|
)
|
||
|
for param in signature.parameters.values()
|
||
|
]
|
||
|
typed_signature = inspect.Signature(typed_params)
|
||
|
return typed_signature
|
||
|
|
||
|
|
||
|
def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) -> Any:
|
||
|
annotation = param.annotation
|
||
|
if isinstance(annotation, str):
|
||
|
annotation = ForwardRef(annotation)
|
||
|
annotation = evaluate_forwardref(annotation, globalns, globalns)
|
||
|
return annotation
|
||
|
|
||
|
|
||
|
async_contextmanager_dependencies_error = """
|
||
|
FastAPI dependencies with yield require Python 3.7 or above,
|
||
|
or the backports for Python 3.6, installed with:
|
||
|
pip install async-exit-stack async-generator
|
||
|
"""
|
||
|
|
||
|
|
||
|
def check_dependency_contextmanagers() -> None:
|
||
|
if AsyncExitStack is None or asynccontextmanager == _fake_asynccontextmanager:
|
||
|
raise RuntimeError(async_contextmanager_dependencies_error) # pragma: no cover
|
||
|
|
||
|
|
||
|
def get_dependant(
|
||
|
*,
|
||
|
path: str,
|
||
|
call: Callable[..., Any],
|
||
|
name: Optional[str] = None,
|
||
|
security_scopes: Optional[List[str]] = None,
|
||
|
use_cache: bool = True,
|
||
|
) -> Dependant:
|
||
|
path_param_names = get_path_param_names(path)
|
||
|
endpoint_signature = get_typed_signature(call)
|
||
|
signature_params = endpoint_signature.parameters
|
||
|
if is_gen_callable(call) or is_async_gen_callable(call):
|
||
|
check_dependency_contextmanagers()
|
||
|
dependant = Dependant(call=call, name=name, path=path, use_cache=use_cache)
|
||
|
for param_name, param in signature_params.items():
|
||
|
if isinstance(param.default, params.Depends):
|
||
|
sub_dependant = get_param_sub_dependant(
|
||
|
param=param, path=path, security_scopes=security_scopes
|
||
|
)
|
||
|
dependant.dependencies.append(sub_dependant)
|
||
|
continue
|
||
|
if add_non_field_param_to_dependency(param=param, dependant=dependant):
|
||
|
continue
|
||
|
param_field = get_param_field(
|
||
|
param=param, default_field_info=params.Query, param_name=param_name
|
||
|
)
|
||
|
if param_name in path_param_names:
|
||
|
assert is_scalar_field(
|
||
|
field=param_field
|
||
|
), "Path params must be of one of the supported types"
|
||
|
if isinstance(param.default, params.Path):
|
||
|
ignore_default = False
|
||
|
else:
|
||
|
ignore_default = True
|
||
|
param_field = get_param_field(
|
||
|
param=param,
|
||
|
param_name=param_name,
|
||
|
default_field_info=params.Path,
|
||
|
force_type=params.ParamTypes.path,
|
||
|
ignore_default=ignore_default,
|
||
|
)
|
||
|
add_param_to_fields(field=param_field, dependant=dependant)
|
||
|
elif is_scalar_field(field=param_field):
|
||
|
add_param_to_fields(field=param_field, dependant=dependant)
|
||
|
elif isinstance(
|
||
|
param.default, (params.Query, params.Header)
|
||
|
) and is_scalar_sequence_field(param_field):
|
||
|
add_param_to_fields(field=param_field, dependant=dependant)
|
||
|
else:
|
||
|
field_info = param_field.field_info
|
||
|
assert isinstance(
|
||
|
field_info, params.Body
|
||
|
), f"Param: {param_field.name} can only be a request body, using Body(...)"
|
||
|
dependant.body_params.append(param_field)
|
||
|
return dependant
|
||
|
|
||
|
|
||
|
def add_non_field_param_to_dependency(
|
||
|
*, param: inspect.Parameter, dependant: Dependant
|
||
|
) -> Optional[bool]:
|
||
|
if lenient_issubclass(param.annotation, Request):
|
||
|
dependant.request_param_name = param.name
|
||
|
return True
|
||
|
elif lenient_issubclass(param.annotation, WebSocket):
|
||
|
dependant.websocket_param_name = param.name
|
||
|
return True
|
||
|
elif lenient_issubclass(param.annotation, HTTPConnection):
|
||
|
dependant.http_connection_param_name = param.name
|
||
|
return True
|
||
|
elif lenient_issubclass(param.annotation, Response):
|
||
|
dependant.response_param_name = param.name
|
||
|
return True
|
||
|
elif lenient_issubclass(param.annotation, BackgroundTasks):
|
||
|
dependant.background_tasks_param_name = param.name
|
||
|
return True
|
||
|
elif lenient_issubclass(param.annotation, SecurityScopes):
|
||
|
dependant.security_scopes_param_name = param.name
|
||
|
return True
|
||
|
return None
|
||
|
|
||
|
|
||
|
def get_param_field(
|
||
|
*,
|
||
|
param: inspect.Parameter,
|
||
|
param_name: str,
|
||
|
default_field_info: Type[params.Param] = params.Param,
|
||
|
force_type: Optional[params.ParamTypes] = None,
|
||
|
ignore_default: bool = False,
|
||
|
) -> ModelField:
|
||
|
default_value = Required
|
||
|
had_schema = False
|
||
|
if not param.default == param.empty and ignore_default is False:
|
||
|
default_value = param.default
|
||
|
if isinstance(default_value, FieldInfo):
|
||
|
had_schema = True
|
||
|
field_info = default_value
|
||
|
default_value = field_info.default
|
||
|
if (
|
||
|
isinstance(field_info, params.Param)
|
||
|
and getattr(field_info, "in_", None) is None
|
||
|
):
|
||
|
field_info.in_ = default_field_info.in_
|
||
|
if force_type:
|
||
|
field_info.in_ = force_type # type: ignore
|
||
|
else:
|
||
|
field_info = default_field_info(default_value)
|
||
|
required = default_value == Required
|
||
|
annotation: Any = Any
|
||
|
if not param.annotation == param.empty:
|
||
|
annotation = param.annotation
|
||
|
annotation = get_annotation_from_field_info(annotation, field_info, param_name)
|
||
|
if not field_info.alias and getattr(field_info, "convert_underscores", None):
|
||
|
alias = param.name.replace("_", "-")
|
||
|
else:
|
||
|
alias = field_info.alias or param.name
|
||
|
field = create_response_field(
|
||
|
name=param.name,
|
||
|
type_=annotation,
|
||
|
default=None if required else default_value,
|
||
|
alias=alias,
|
||
|
required=required,
|
||
|
field_info=field_info,
|
||
|
)
|
||
|
field.required = required
|
||
|
if not had_schema and not is_scalar_field(field=field):
|
||
|
field.field_info = params.Body(field_info.default)
|
||
|
|
||
|
return field
|
||
|
|
||
|
|
||
|
def add_param_to_fields(*, field: ModelField, dependant: Dependant) -> None:
|
||
|
field_info = cast(params.Param, field.field_info)
|
||
|
if field_info.in_ == params.ParamTypes.path:
|
||
|
dependant.path_params.append(field)
|
||
|
elif field_info.in_ == params.ParamTypes.query:
|
||
|
dependant.query_params.append(field)
|
||
|
elif field_info.in_ == params.ParamTypes.header:
|
||
|
dependant.header_params.append(field)
|
||
|
else:
|
||
|
assert (
|
||
|
field_info.in_ == params.ParamTypes.cookie
|
||
|
), f"non-body parameters must be in path, query, header or cookie: {field.name}"
|
||
|
dependant.cookie_params.append(field)
|
||
|
|
||
|
|
||
|
def is_coroutine_callable(call: Callable[..., Any]) -> bool:
|
||
|
if inspect.isroutine(call):
|
||
|
return inspect.iscoroutinefunction(call)
|
||
|
if inspect.isclass(call):
|
||
|
return False
|
||
|
call = getattr(call, "__call__", None)
|
||
|
return inspect.iscoroutinefunction(call)
|
||
|
|
||
|
|
||
|
def is_async_gen_callable(call: Callable[..., Any]) -> bool:
|
||
|
if inspect.isasyncgenfunction(call):
|
||
|
return True
|
||
|
call = getattr(call, "__call__", None)
|
||
|
return inspect.isasyncgenfunction(call)
|
||
|
|
||
|
|
||
|
def is_gen_callable(call: Callable[..., Any]) -> bool:
|
||
|
if inspect.isgeneratorfunction(call):
|
||
|
return True
|
||
|
call = getattr(call, "__call__", None)
|
||
|
return inspect.isgeneratorfunction(call)
|
||
|
|
||
|
|
||
|
async def solve_generator(
|
||
|
*, call: Callable[..., Any], stack: AsyncExitStack, sub_values: Dict[str, Any]
|
||
|
) -> Any:
|
||
|
if is_gen_callable(call):
|
||
|
cm = contextmanager_in_threadpool(contextmanager(call)(**sub_values))
|
||
|
elif is_async_gen_callable(call):
|
||
|
if not inspect.isasyncgenfunction(call):
|
||
|
# asynccontextmanager from the async_generator backfill pre python3.7
|
||
|
# does not support callables that are not functions or methods.
|
||
|
# See https://github.com/python-trio/async_generator/issues/32
|
||
|
#
|
||
|
# Expand the callable class into its __call__ method before decorating it.
|
||
|
# This approach will work on newer python versions as well.
|
||
|
call = getattr(call, "__call__", None)
|
||
|
cm = asynccontextmanager(call)(**sub_values)
|
||
|
return await stack.enter_async_context(cm)
|
||
|
|
||
|
|
||
|
async def solve_dependencies(
|
||
|
*,
|
||
|
request: Union[Request, WebSocket],
|
||
|
dependant: Dependant,
|
||
|
body: Optional[Union[Dict[str, Any], FormData]] = None,
|
||
|
background_tasks: Optional[BackgroundTasks] = None,
|
||
|
response: Optional[Response] = None,
|
||
|
dependency_overrides_provider: Optional[Any] = None,
|
||
|
dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None,
|
||
|
) -> Tuple[
|
||
|
Dict[str, Any],
|
||
|
List[ErrorWrapper],
|
||
|
Optional[BackgroundTasks],
|
||
|
Response,
|
||
|
Dict[Tuple[Callable[..., Any], Tuple[str]], Any],
|
||
|
]:
|
||
|
values: Dict[str, Any] = {}
|
||
|
errors: List[ErrorWrapper] = []
|
||
|
response = response or Response(
|
||
|
content=None,
|
||
|
status_code=None, # type: ignore
|
||
|
headers=None, # type: ignore # in Starlette
|
||
|
media_type=None, # type: ignore # in Starlette
|
||
|
background=None, # type: ignore # in Starlette
|
||
|
)
|
||
|
dependency_cache = dependency_cache or {}
|
||
|
sub_dependant: Dependant
|
||
|
for sub_dependant in dependant.dependencies:
|
||
|
sub_dependant.call = cast(Callable[..., Any], sub_dependant.call)
|
||
|
sub_dependant.cache_key = cast(
|
||
|
Tuple[Callable[..., Any], Tuple[str]], sub_dependant.cache_key
|
||
|
)
|
||
|
call = sub_dependant.call
|
||
|
use_sub_dependant = sub_dependant
|
||
|
if (
|
||
|
dependency_overrides_provider
|
||
|
and dependency_overrides_provider.dependency_overrides
|
||
|
):
|
||
|
original_call = sub_dependant.call
|
||
|
call = getattr(
|
||
|
dependency_overrides_provider, "dependency_overrides", {}
|
||
|
).get(original_call, original_call)
|
||
|
use_path: str = sub_dependant.path # type: ignore
|
||
|
use_sub_dependant = get_dependant(
|
||
|
path=use_path,
|
||
|
call=call,
|
||
|
name=sub_dependant.name,
|
||
|
security_scopes=sub_dependant.security_scopes,
|
||
|
)
|
||
|
use_sub_dependant.security_scopes = sub_dependant.security_scopes
|
||
|
|
||
|
solved_result = await solve_dependencies(
|
||
|
request=request,
|
||
|
dependant=use_sub_dependant,
|
||
|
body=body,
|
||
|
background_tasks=background_tasks,
|
||
|
response=response,
|
||
|
dependency_overrides_provider=dependency_overrides_provider,
|
||
|
dependency_cache=dependency_cache,
|
||
|
)
|
||
|
(
|
||
|
sub_values,
|
||
|
sub_errors,
|
||
|
background_tasks,
|
||
|
_, # the subdependency returns the same response we have
|
||
|
sub_dependency_cache,
|
||
|
) = solved_result
|
||
|
dependency_cache.update(sub_dependency_cache)
|
||
|
if sub_errors:
|
||
|
errors.extend(sub_errors)
|
||
|
continue
|
||
|
if sub_dependant.use_cache and sub_dependant.cache_key in dependency_cache:
|
||
|
solved = dependency_cache[sub_dependant.cache_key]
|
||
|
elif is_gen_callable(call) or is_async_gen_callable(call):
|
||
|
stack = request.scope.get("fastapi_astack")
|
||
|
if stack is None:
|
||
|
raise RuntimeError(
|
||
|
async_contextmanager_dependencies_error
|
||
|
) # pragma: no cover
|
||
|
solved = await solve_generator(
|
||
|
call=call, stack=stack, sub_values=sub_values
|
||
|
)
|
||
|
elif is_coroutine_callable(call):
|
||
|
solved = await call(**sub_values)
|
||
|
else:
|
||
|
solved = await run_in_threadpool(call, **sub_values)
|
||
|
if sub_dependant.name is not None:
|
||
|
values[sub_dependant.name] = solved
|
||
|
if sub_dependant.cache_key not in dependency_cache:
|
||
|
dependency_cache[sub_dependant.cache_key] = solved
|
||
|
path_values, path_errors = request_params_to_args(
|
||
|
dependant.path_params, request.path_params
|
||
|
)
|
||
|
query_values, query_errors = request_params_to_args(
|
||
|
dependant.query_params, request.query_params
|
||
|
)
|
||
|
header_values, header_errors = request_params_to_args(
|
||
|
dependant.header_params, request.headers
|
||
|
)
|
||
|
cookie_values, cookie_errors = request_params_to_args(
|
||
|
dependant.cookie_params, request.cookies
|
||
|
)
|
||
|
values.update(path_values)
|
||
|
values.update(query_values)
|
||
|
values.update(header_values)
|
||
|
values.update(cookie_values)
|
||
|
errors += path_errors + query_errors + header_errors + cookie_errors
|
||
|
if dependant.body_params:
|
||
|
(
|
||
|
body_values,
|
||
|
body_errors,
|
||
|
) = await request_body_to_args( # body_params checked above
|
||
|
required_params=dependant.body_params, received_body=body
|
||
|
)
|
||
|
values.update(body_values)
|
||
|
errors.extend(body_errors)
|
||
|
if dependant.http_connection_param_name:
|
||
|
values[dependant.http_connection_param_name] = request
|
||
|
if dependant.request_param_name and isinstance(request, Request):
|
||
|
values[dependant.request_param_name] = request
|
||
|
elif dependant.websocket_param_name and isinstance(request, WebSocket):
|
||
|
values[dependant.websocket_param_name] = request
|
||
|
if dependant.background_tasks_param_name:
|
||
|
if background_tasks is None:
|
||
|
background_tasks = BackgroundTasks()
|
||
|
values[dependant.background_tasks_param_name] = background_tasks
|
||
|
if dependant.response_param_name:
|
||
|
values[dependant.response_param_name] = response
|
||
|
if dependant.security_scopes_param_name:
|
||
|
values[dependant.security_scopes_param_name] = SecurityScopes(
|
||
|
scopes=dependant.security_scopes
|
||
|
)
|
||
|
return values, errors, background_tasks, response, dependency_cache
|
||
|
|
||
|
|
||
|
def request_params_to_args(
|
||
|
required_params: Sequence[ModelField],
|
||
|
received_params: Union[Mapping[str, Any], QueryParams, Headers],
|
||
|
) -> Tuple[Dict[str, Any], List[ErrorWrapper]]:
|
||
|
values = {}
|
||
|
errors = []
|
||
|
for field in required_params:
|
||
|
if is_scalar_sequence_field(field) and isinstance(
|
||
|
received_params, (QueryParams, Headers)
|
||
|
):
|
||
|
value = received_params.getlist(field.alias) or field.default
|
||
|
else:
|
||
|
value = received_params.get(field.alias)
|
||
|
field_info = field.field_info
|
||
|
assert isinstance(
|
||
|
field_info, params.Param
|
||
|
), "Params must be subclasses of Param"
|
||
|
if value is None:
|
||
|
if field.required:
|
||
|
errors.append(
|
||
|
ErrorWrapper(
|
||
|
MissingError(), loc=(field_info.in_.value, field.alias)
|
||
|
)
|
||
|
)
|
||
|
else:
|
||
|
values[field.name] = deepcopy(field.default)
|
||
|
continue
|
||
|
v_, errors_ = field.validate(
|
||
|
value, values, loc=(field_info.in_.value, field.alias)
|
||
|
)
|
||
|
if isinstance(errors_, ErrorWrapper):
|
||
|
errors.append(errors_)
|
||
|
elif isinstance(errors_, list):
|
||
|
errors.extend(errors_)
|
||
|
else:
|
||
|
values[field.name] = v_
|
||
|
return values, errors
|
||
|
|
||
|
|
||
|
async def request_body_to_args(
|
||
|
required_params: List[ModelField],
|
||
|
received_body: Optional[Union[Dict[str, Any], FormData]],
|
||
|
) -> Tuple[Dict[str, Any], List[ErrorWrapper]]:
|
||
|
values = {}
|
||
|
errors = []
|
||
|
if required_params:
|
||
|
field = required_params[0]
|
||
|
field_info = field.field_info
|
||
|
embed = getattr(field_info, "embed", None)
|
||
|
field_alias_omitted = len(required_params) == 1 and not embed
|
||
|
if field_alias_omitted:
|
||
|
received_body = {field.alias: received_body}
|
||
|
|
||
|
for field in required_params:
|
||
|
loc: Tuple[str, ...]
|
||
|
if field_alias_omitted:
|
||
|
loc = ("body",)
|
||
|
else:
|
||
|
loc = ("body", field.alias)
|
||
|
|
||
|
value: Optional[Any] = None
|
||
|
if received_body is not None:
|
||
|
if (
|
||
|
field.shape in sequence_shapes or field.type_ in sequence_types
|
||
|
) and isinstance(received_body, FormData):
|
||
|
value = received_body.getlist(field.alias)
|
||
|
else:
|
||
|
try:
|
||
|
value = received_body.get(field.alias)
|
||
|
except AttributeError:
|
||
|
errors.append(get_missing_field_error(loc))
|
||
|
continue
|
||
|
if (
|
||
|
value is None
|
||
|
or (isinstance(field_info, params.Form) and value == "")
|
||
|
or (
|
||
|
isinstance(field_info, params.Form)
|
||
|
and field.shape in sequence_shapes
|
||
|
and len(value) == 0
|
||
|
)
|
||
|
):
|
||
|
if field.required:
|
||
|
errors.append(get_missing_field_error(loc))
|
||
|
else:
|
||
|
values[field.name] = deepcopy(field.default)
|
||
|
continue
|
||
|
if (
|
||
|
isinstance(field_info, params.File)
|
||
|
and lenient_issubclass(field.type_, bytes)
|
||
|
and isinstance(value, UploadFile)
|
||
|
):
|
||
|
value = await value.read()
|
||
|
elif (
|
||
|
field.shape in sequence_shapes
|
||
|
and isinstance(field_info, params.File)
|
||
|
and lenient_issubclass(field.type_, bytes)
|
||
|
and isinstance(value, sequence_types)
|
||
|
):
|
||
|
awaitables = [sub_value.read() for sub_value in value]
|
||
|
contents = await asyncio.gather(*awaitables)
|
||
|
value = sequence_shape_to_type[field.shape](contents)
|
||
|
|
||
|
v_, errors_ = field.validate(value, values, loc=loc)
|
||
|
|
||
|
if isinstance(errors_, ErrorWrapper):
|
||
|
errors.append(errors_)
|
||
|
elif isinstance(errors_, list):
|
||
|
errors.extend(errors_)
|
||
|
else:
|
||
|
values[field.name] = v_
|
||
|
return values, errors
|
||
|
|
||
|
|
||
|
def get_missing_field_error(loc: Tuple[str, ...]) -> ErrorWrapper:
|
||
|
missing_field_error = ErrorWrapper(MissingError(), loc=loc)
|
||
|
return missing_field_error
|
||
|
|
||
|
|
||
|
def get_schema_compatible_field(*, field: ModelField) -> ModelField:
|
||
|
out_field = field
|
||
|
if lenient_issubclass(field.type_, UploadFile):
|
||
|
use_type: type = bytes
|
||
|
if field.shape in sequence_shapes:
|
||
|
use_type = List[bytes]
|
||
|
out_field = create_response_field(
|
||
|
name=field.name,
|
||
|
type_=use_type,
|
||
|
class_validators=field.class_validators,
|
||
|
model_config=field.model_config,
|
||
|
default=field.default,
|
||
|
required=field.required,
|
||
|
alias=field.alias,
|
||
|
field_info=field.field_info,
|
||
|
)
|
||
|
return out_field
|
||
|
|
||
|
|
||
|
def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]:
|
||
|
flat_dependant = get_flat_dependant(dependant)
|
||
|
if not flat_dependant.body_params:
|
||
|
return None
|
||
|
first_param = flat_dependant.body_params[0]
|
||
|
field_info = first_param.field_info
|
||
|
embed = getattr(field_info, "embed", None)
|
||
|
body_param_names_set = {param.name for param in flat_dependant.body_params}
|
||
|
if len(body_param_names_set) == 1 and not embed:
|
||
|
final_field = get_schema_compatible_field(field=first_param)
|
||
|
check_file_field(final_field)
|
||
|
return final_field
|
||
|
# If one field requires to embed, all have to be embedded
|
||
|
# in case a sub-dependency is evaluated with a single unique body field
|
||
|
# That is combined (embedded) with other body fields
|
||
|
for param in flat_dependant.body_params:
|
||
|
setattr(param.field_info, "embed", True)
|
||
|
model_name = "Body_" + name
|
||
|
BodyModel = create_model(model_name)
|
||
|
for f in flat_dependant.body_params:
|
||
|
BodyModel.__fields__[f.name] = get_schema_compatible_field(field=f)
|
||
|
required = any(True for f in flat_dependant.body_params if f.required)
|
||
|
|
||
|
BodyFieldInfo_kwargs: Dict[str, Any] = dict(default=None)
|
||
|
if any(isinstance(f.field_info, params.File) for f in flat_dependant.body_params):
|
||
|
BodyFieldInfo: Type[params.Body] = params.File
|
||
|
elif any(isinstance(f.field_info, params.Form) for f in flat_dependant.body_params):
|
||
|
BodyFieldInfo = params.Form
|
||
|
else:
|
||
|
BodyFieldInfo = params.Body
|
||
|
|
||
|
body_param_media_types = [
|
||
|
getattr(f.field_info, "media_type")
|
||
|
for f in flat_dependant.body_params
|
||
|
if isinstance(f.field_info, params.Body)
|
||
|
]
|
||
|
if len(set(body_param_media_types)) == 1:
|
||
|
BodyFieldInfo_kwargs["media_type"] = body_param_media_types[0]
|
||
|
final_field = create_response_field(
|
||
|
name="body",
|
||
|
type_=BodyModel,
|
||
|
required=required,
|
||
|
alias="body",
|
||
|
field_info=BodyFieldInfo(**BodyFieldInfo_kwargs),
|
||
|
)
|
||
|
check_file_field(final_field)
|
||
|
return final_field
|