165 lines
5.1 KiB
Python
165 lines
5.1 KiB
Python
|
|
import re
|
||
|
|
import warnings
|
||
|
|
from collections.abc import MutableMapping
|
||
|
|
from typing import (
|
||
|
|
TYPE_CHECKING,
|
||
|
|
Any,
|
||
|
|
Optional,
|
||
|
|
Union,
|
||
|
|
)
|
||
|
|
from weakref import WeakKeyDictionary
|
||
|
|
|
||
|
|
import fastapi
|
||
|
|
from fastapi._compat import (
|
||
|
|
BaseConfig,
|
||
|
|
ModelField,
|
||
|
|
PydanticSchemaGenerationError,
|
||
|
|
Undefined,
|
||
|
|
UndefinedType,
|
||
|
|
Validator,
|
||
|
|
annotation_is_pydantic_v1,
|
||
|
|
)
|
||
|
|
from fastapi.datastructures import DefaultPlaceholder, DefaultType
|
||
|
|
from fastapi.exceptions import FastAPIDeprecationWarning, PydanticV1NotSupportedError
|
||
|
|
from pydantic import BaseModel
|
||
|
|
from pydantic.fields import FieldInfo
|
||
|
|
from typing_extensions import Literal
|
||
|
|
|
||
|
|
from ._compat import v2
|
||
|
|
|
||
|
|
if TYPE_CHECKING: # pragma: nocover
|
||
|
|
from .routing import APIRoute
|
||
|
|
|
||
|
|
# Cache for `create_cloned_field`
|
||
|
|
_CLONED_TYPES_CACHE: MutableMapping[type[BaseModel], type[BaseModel]] = (
|
||
|
|
WeakKeyDictionary()
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def is_body_allowed_for_status_code(status_code: Union[int, str, None]) -> bool:
|
||
|
|
if status_code is None:
|
||
|
|
return True
|
||
|
|
# Ref: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#patterned-fields-1
|
||
|
|
if status_code in {
|
||
|
|
"default",
|
||
|
|
"1XX",
|
||
|
|
"2XX",
|
||
|
|
"3XX",
|
||
|
|
"4XX",
|
||
|
|
"5XX",
|
||
|
|
}:
|
||
|
|
return True
|
||
|
|
current_status_code = int(status_code)
|
||
|
|
return not (current_status_code < 200 or current_status_code in {204, 205, 304})
|
||
|
|
|
||
|
|
|
||
|
|
def get_path_param_names(path: str) -> set[str]:
|
||
|
|
return set(re.findall("{(.*?)}", path))
|
||
|
|
|
||
|
|
|
||
|
|
_invalid_args_message = (
|
||
|
|
"Invalid args for response field! Hint: "
|
||
|
|
"check that {type_} is a valid Pydantic field type. "
|
||
|
|
"If you are using a return type annotation that is not a valid Pydantic "
|
||
|
|
"field (e.g. Union[Response, dict, None]) you can disable generating the "
|
||
|
|
"response model from the type annotation with the path operation decorator "
|
||
|
|
"parameter response_model=None. Read more: "
|
||
|
|
"https://fastapi.tiangolo.com/tutorial/response-model/"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def create_model_field(
|
||
|
|
name: str,
|
||
|
|
type_: Any,
|
||
|
|
class_validators: Optional[dict[str, Validator]] = None,
|
||
|
|
default: Optional[Any] = Undefined,
|
||
|
|
required: Union[bool, UndefinedType] = Undefined,
|
||
|
|
model_config: Union[type[BaseConfig], None] = None,
|
||
|
|
field_info: Optional[FieldInfo] = None,
|
||
|
|
alias: Optional[str] = None,
|
||
|
|
mode: Literal["validation", "serialization"] = "validation",
|
||
|
|
version: Literal["1", "auto"] = "auto",
|
||
|
|
) -> ModelField:
|
||
|
|
if annotation_is_pydantic_v1(type_):
|
||
|
|
raise PydanticV1NotSupportedError(
|
||
|
|
"pydantic.v1 models are no longer supported by FastAPI."
|
||
|
|
f" Please update the response model {type_!r}."
|
||
|
|
)
|
||
|
|
class_validators = class_validators or {}
|
||
|
|
|
||
|
|
field_info = field_info or FieldInfo(annotation=type_, default=default, alias=alias)
|
||
|
|
kwargs = {"mode": mode, "name": name, "field_info": field_info}
|
||
|
|
try:
|
||
|
|
return v2.ModelField(**kwargs) # type: ignore[return-value,arg-type]
|
||
|
|
except PydanticSchemaGenerationError:
|
||
|
|
raise fastapi.exceptions.FastAPIError(
|
||
|
|
_invalid_args_message.format(type_=type_)
|
||
|
|
) from None
|
||
|
|
|
||
|
|
|
||
|
|
def create_cloned_field(
|
||
|
|
field: ModelField,
|
||
|
|
*,
|
||
|
|
cloned_types: Optional[MutableMapping[type[BaseModel], type[BaseModel]]] = None,
|
||
|
|
) -> ModelField:
|
||
|
|
return field
|
||
|
|
|
||
|
|
|
||
|
|
def generate_operation_id_for_path(
|
||
|
|
*, name: str, path: str, method: str
|
||
|
|
) -> str: # pragma: nocover
|
||
|
|
warnings.warn(
|
||
|
|
message="fastapi.utils.generate_operation_id_for_path() was deprecated, "
|
||
|
|
"it is not used internally, and will be removed soon",
|
||
|
|
category=FastAPIDeprecationWarning,
|
||
|
|
stacklevel=2,
|
||
|
|
)
|
||
|
|
operation_id = f"{name}{path}"
|
||
|
|
operation_id = re.sub(r"\W", "_", operation_id)
|
||
|
|
operation_id = f"{operation_id}_{method.lower()}"
|
||
|
|
return operation_id
|
||
|
|
|
||
|
|
|
||
|
|
def generate_unique_id(route: "APIRoute") -> str:
|
||
|
|
operation_id = f"{route.name}{route.path_format}"
|
||
|
|
operation_id = re.sub(r"\W", "_", operation_id)
|
||
|
|
assert route.methods
|
||
|
|
operation_id = f"{operation_id}_{list(route.methods)[0].lower()}"
|
||
|
|
return operation_id
|
||
|
|
|
||
|
|
|
||
|
|
def deep_dict_update(main_dict: dict[Any, Any], update_dict: dict[Any, Any]) -> None:
|
||
|
|
for key, value in update_dict.items():
|
||
|
|
if (
|
||
|
|
key in main_dict
|
||
|
|
and isinstance(main_dict[key], dict)
|
||
|
|
and isinstance(value, dict)
|
||
|
|
):
|
||
|
|
deep_dict_update(main_dict[key], value)
|
||
|
|
elif (
|
||
|
|
key in main_dict
|
||
|
|
and isinstance(main_dict[key], list)
|
||
|
|
and isinstance(update_dict[key], list)
|
||
|
|
):
|
||
|
|
main_dict[key] = main_dict[key] + update_dict[key]
|
||
|
|
else:
|
||
|
|
main_dict[key] = value
|
||
|
|
|
||
|
|
|
||
|
|
def get_value_or_default(
|
||
|
|
first_item: Union[DefaultPlaceholder, DefaultType],
|
||
|
|
*extra_items: Union[DefaultPlaceholder, DefaultType],
|
||
|
|
) -> Union[DefaultPlaceholder, DefaultType]:
|
||
|
|
"""
|
||
|
|
Pass items or `DefaultPlaceholder`s by descending priority.
|
||
|
|
|
||
|
|
The first one to _not_ be a `DefaultPlaceholder` will be returned.
|
||
|
|
|
||
|
|
Otherwise, the first item (a `DefaultPlaceholder`) will be returned.
|
||
|
|
"""
|
||
|
|
items = (first_item,) + extra_items
|
||
|
|
for item in items:
|
||
|
|
if not isinstance(item, DefaultPlaceholder):
|
||
|
|
return item
|
||
|
|
return first_item
|