2020-02-29 05:04:35 -08:00
|
|
|
import functools
|
2018-12-06 20:24:50 +04:00
|
|
|
import re
|
2022-03-04 14:02:18 -08:00
|
|
|
import warnings
|
2019-08-30 18:12:15 -05:00
|
|
|
from dataclasses import is_dataclass
|
2020-05-23 18:56:18 +02:00
|
|
|
from enum import Enum
|
2022-03-04 14:02:18 -08:00
|
|
|
from typing import TYPE_CHECKING, Any, Dict, Optional, Set, Type, Union, cast
|
2018-12-06 20:24:50 +04:00
|
|
|
|
2020-02-29 05:04:35 -08:00
|
|
|
import fastapi
|
✨ Add support for shared/top-level parameters (dependencies, tags, etc) (#2434)
* ✨ Add Default and DefaultPlaceholder data structures
to handle defaults and overrides
* ✨ Add utils to get values by priority handling DefaultPlaceholders
* ✨ Add support for top-level parameters in FastAPI, APIRouter, include_router
including: prefix, tags, dependencies, deprecated, include_in_schema, responses, default_response_class, callbacks
* ♻️ Update openapi utils to handle DefaultPlaceholder for response_class
* 📝 Update bigger-application example code to use top-level params
and showcase them in APIRouter, FastAPI, include_router
* 📝 Update docs for Bigger Applications, include diagrams, top-level params
* 🔥 Simplify code and docs for callbacks as default_response_class is no longer required
* 📝 Add docs for top-level dependencies, in FastAPI()
* 📝 Add docs reference to top-level dependencies in docs for decorator
* ✅ Update/increase tests for Bigger Applications including shared parameters
* ✅ Add tests for top-level dependencies in FastAPI()
* ✅ Add tests for internal DefaultPlaceholder
* ✅ Update/increase tests for callbacks with top-level parameters
* ✅ Add LOTS of tests covering branches and cases for shared parameters
in top-level FastAPI, path operations, include_router, APIRouter, its path operations, nested include_router, nested APIRouter, and its path operations
* 🎨 Format/reorder parameters for consistency in FastAPI, APIRouter, include_router
2020-11-29 18:32:18 +01:00
|
|
|
from fastapi.datastructures import DefaultPlaceholder, DefaultType
|
2018-12-18 22:36:04 +04:00
|
|
|
from fastapi.openapi.constants import REF_PREFIX
|
2019-11-27 11:32:02 -08:00
|
|
|
from pydantic import BaseConfig, BaseModel, create_model
|
2020-02-29 05:04:35 -08:00
|
|
|
from pydantic.class_validators import Validator
|
2020-08-09 22:17:08 +02:00
|
|
|
from pydantic.fields import FieldInfo, ModelField, UndefinedType
|
2020-05-23 18:56:18 +02:00
|
|
|
from pydantic.schema import model_process_schema
|
2019-06-20 11:31:32 +02:00
|
|
|
from pydantic.utils import lenient_issubclass
|
2018-12-06 20:24:50 +04:00
|
|
|
|
2022-03-04 14:02:18 -08:00
|
|
|
if TYPE_CHECKING: # pragma: nocover
|
|
|
|
|
from .routing import APIRoute
|
|
|
|
|
|
2018-12-06 20:24:50 +04:00
|
|
|
|
2022-07-14 13:19:42 +02:00
|
|
|
def is_body_allowed_for_status_code(status_code: Union[int, str, None]) -> bool:
|
|
|
|
|
if status_code is None:
|
|
|
|
|
return True
|
2022-10-14 23:44:22 +03:00
|
|
|
# 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
|
2022-07-14 13:19:42 +02:00
|
|
|
current_status_code = int(status_code)
|
|
|
|
|
return not (current_status_code < 200 or current_status_code in {204, 304})
|
|
|
|
|
|
|
|
|
|
|
2018-12-06 20:24:50 +04:00
|
|
|
def get_model_definitions(
|
2020-05-23 18:56:18 +02:00
|
|
|
*,
|
|
|
|
|
flat_models: Set[Union[Type[BaseModel], Type[Enum]]],
|
|
|
|
|
model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
|
2018-12-07 19:12:16 +04:00
|
|
|
) -> Dict[str, Any]:
|
2020-12-20 19:50:00 +01:00
|
|
|
definitions: Dict[str, Dict[str, Any]] = {}
|
2018-12-06 20:24:50 +04:00
|
|
|
for model in flat_models:
|
2019-07-12 16:56:25 -07:00
|
|
|
m_schema, m_definitions, m_nested_models = model_process_schema(
|
2020-12-20 19:50:00 +01:00
|
|
|
model, model_name_map=model_name_map, ref_prefix=REF_PREFIX
|
2018-12-06 20:24:50 +04:00
|
|
|
)
|
|
|
|
|
definitions.update(m_definitions)
|
|
|
|
|
model_name = model_name_map[model]
|
2022-09-02 14:43:21 +02:00
|
|
|
if "description" in m_schema:
|
|
|
|
|
m_schema["description"] = m_schema["description"].split("\f")[0]
|
2018-12-06 20:24:50 +04:00
|
|
|
definitions[model_name] = m_schema
|
|
|
|
|
return definitions
|
|
|
|
|
|
|
|
|
|
|
2018-12-07 19:12:16 +04:00
|
|
|
def get_path_param_names(path: str) -> Set[str]:
|
2020-06-12 22:59:32 +03:00
|
|
|
return set(re.findall("{(.*?)}", path))
|
2019-06-20 11:31:32 +02:00
|
|
|
|
|
|
|
|
|
2020-02-29 05:04:35 -08:00
|
|
|
def create_response_field(
|
|
|
|
|
name: str,
|
|
|
|
|
type_: Type[Any],
|
|
|
|
|
class_validators: Optional[Dict[str, Validator]] = None,
|
|
|
|
|
default: Optional[Any] = None,
|
2022-08-22 22:17:45 +03:00
|
|
|
required: Union[bool, UndefinedType] = True,
|
2020-02-29 05:04:35 -08:00
|
|
|
model_config: Type[BaseConfig] = BaseConfig,
|
|
|
|
|
field_info: Optional[FieldInfo] = None,
|
|
|
|
|
alias: Optional[str] = None,
|
|
|
|
|
) -> ModelField:
|
|
|
|
|
"""
|
|
|
|
|
Create a new response field. Raises if type_ is invalid.
|
|
|
|
|
"""
|
|
|
|
|
class_validators = class_validators or {}
|
2022-05-13 18:38:22 -05:00
|
|
|
field_info = field_info or FieldInfo()
|
2020-02-29 05:04:35 -08:00
|
|
|
|
|
|
|
|
response_field = functools.partial(
|
|
|
|
|
ModelField,
|
|
|
|
|
name=name,
|
|
|
|
|
type_=type_,
|
|
|
|
|
class_validators=class_validators,
|
|
|
|
|
default=default,
|
|
|
|
|
required=required,
|
|
|
|
|
model_config=model_config,
|
|
|
|
|
alias=alias,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
try:
|
2020-08-09 22:17:08 +02:00
|
|
|
return response_field(field_info=field_info)
|
2020-02-29 05:04:35 -08:00
|
|
|
except RuntimeError:
|
|
|
|
|
raise fastapi.exceptions.FastAPIError(
|
2023-01-10 20:22:47 +04:00
|
|
|
"Invalid args for response field! Hint: "
|
|
|
|
|
f"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/"
|
2022-11-27 14:59:32 +01:00
|
|
|
) from None
|
2020-02-29 05:04:35 -08:00
|
|
|
|
|
|
|
|
|
2020-03-29 19:26:29 +02:00
|
|
|
def create_cloned_field(
|
2020-08-03 16:59:07 +04:30
|
|
|
field: ModelField,
|
|
|
|
|
*,
|
|
|
|
|
cloned_types: Optional[Dict[Type[BaseModel], Type[BaseModel]]] = None,
|
2020-03-29 19:26:29 +02:00
|
|
|
) -> ModelField:
|
|
|
|
|
# _cloned_types has already cloned types, to support recursive models
|
|
|
|
|
if cloned_types is None:
|
2022-08-26 17:23:25 +04:00
|
|
|
cloned_types = {}
|
2019-06-20 11:31:32 +02:00
|
|
|
original_type = field.type_
|
2019-08-30 18:12:15 -05:00
|
|
|
if is_dataclass(original_type) and hasattr(original_type, "__pydantic_model__"):
|
2020-12-20 19:50:00 +01:00
|
|
|
original_type = original_type.__pydantic_model__
|
2019-06-20 11:31:32 +02:00
|
|
|
use_type = original_type
|
|
|
|
|
if lenient_issubclass(original_type, BaseModel):
|
|
|
|
|
original_type = cast(Type[BaseModel], original_type)
|
2020-03-29 19:26:29 +02:00
|
|
|
use_type = cloned_types.get(original_type)
|
|
|
|
|
if use_type is None:
|
|
|
|
|
use_type = create_model(original_type.__name__, __base__=original_type)
|
|
|
|
|
cloned_types[original_type] = use_type
|
|
|
|
|
for f in original_type.__fields__.values():
|
|
|
|
|
use_type.__fields__[f.name] = create_cloned_field(
|
|
|
|
|
f, cloned_types=cloned_types
|
|
|
|
|
)
|
2020-02-29 05:04:35 -08:00
|
|
|
new_field = create_response_field(name=field.name, type_=use_type)
|
2019-06-20 11:31:32 +02:00
|
|
|
new_field.has_alias = field.has_alias
|
|
|
|
|
new_field.alias = field.alias
|
|
|
|
|
new_field.class_validators = field.class_validators
|
|
|
|
|
new_field.default = field.default
|
|
|
|
|
new_field.required = field.required
|
|
|
|
|
new_field.model_config = field.model_config
|
2020-08-09 22:17:08 +02:00
|
|
|
new_field.field_info = field.field_info
|
2019-06-20 11:31:32 +02:00
|
|
|
new_field.allow_none = field.allow_none
|
|
|
|
|
new_field.validate_always = field.validate_always
|
|
|
|
|
if field.sub_fields:
|
|
|
|
|
new_field.sub_fields = [
|
2020-03-29 19:26:29 +02:00
|
|
|
create_cloned_field(sub_field, cloned_types=cloned_types)
|
|
|
|
|
for sub_field in field.sub_fields
|
2019-06-20 11:31:32 +02:00
|
|
|
]
|
|
|
|
|
if field.key_field:
|
2020-03-29 19:26:29 +02:00
|
|
|
new_field.key_field = create_cloned_field(
|
|
|
|
|
field.key_field, cloned_types=cloned_types
|
|
|
|
|
)
|
2019-06-20 11:31:32 +02:00
|
|
|
new_field.validators = field.validators
|
2020-08-09 22:17:08 +02:00
|
|
|
new_field.pre_validators = field.pre_validators
|
|
|
|
|
new_field.post_validators = field.post_validators
|
2019-06-20 11:31:32 +02:00
|
|
|
new_field.parse_json = field.parse_json
|
|
|
|
|
new_field.shape = field.shape
|
2020-08-09 22:17:08 +02:00
|
|
|
new_field.populate_validators()
|
2019-06-20 11:31:32 +02:00
|
|
|
return new_field
|
2019-06-28 19:35:16 +02:00
|
|
|
|
|
|
|
|
|
2022-03-04 14:02:18 -08:00
|
|
|
def generate_operation_id_for_path(
|
|
|
|
|
*, name: str, path: str, method: str
|
|
|
|
|
) -> str: # pragma: nocover
|
|
|
|
|
warnings.warn(
|
|
|
|
|
"fastapi.utils.generate_operation_id_for_path() was deprecated, "
|
|
|
|
|
"it is not used internally, and will be removed soon",
|
|
|
|
|
DeprecationWarning,
|
|
|
|
|
stacklevel=2,
|
|
|
|
|
)
|
2019-06-28 19:35:16 +02:00
|
|
|
operation_id = name + path
|
2022-08-26 16:46:22 +03:00
|
|
|
operation_id = re.sub(r"\W", "_", operation_id)
|
2019-06-28 19:35:16 +02:00
|
|
|
operation_id = operation_id + "_" + method.lower()
|
|
|
|
|
return operation_id
|
2020-06-15 13:12:12 +02:00
|
|
|
|
|
|
|
|
|
2022-03-04 14:02:18 -08:00
|
|
|
def generate_unique_id(route: "APIRoute") -> str:
|
|
|
|
|
operation_id = route.name + route.path_format
|
2022-08-26 16:46:22 +03:00
|
|
|
operation_id = re.sub(r"\W", "_", operation_id)
|
2022-03-04 14:02:18 -08:00
|
|
|
assert route.methods
|
|
|
|
|
operation_id = operation_id + "_" + list(route.methods)[0].lower()
|
|
|
|
|
return operation_id
|
|
|
|
|
|
|
|
|
|
|
2020-12-20 19:50:00 +01:00
|
|
|
def deep_dict_update(main_dict: Dict[Any, Any], update_dict: Dict[Any, Any]) -> None:
|
2022-05-13 00:38:30 +04:00
|
|
|
for key, value in update_dict.items():
|
2020-06-15 13:12:12 +02:00
|
|
|
if (
|
|
|
|
|
key in main_dict
|
|
|
|
|
and isinstance(main_dict[key], dict)
|
2022-05-13 00:38:30 +04:00
|
|
|
and isinstance(value, dict)
|
2020-06-15 13:12:12 +02:00
|
|
|
):
|
2022-05-13 00:38:30 +04:00
|
|
|
deep_dict_update(main_dict[key], value)
|
2022-08-26 02:26:20 -07:00
|
|
|
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]
|
2020-06-15 13:12:12 +02:00
|
|
|
else:
|
2022-05-13 00:38:30 +04:00
|
|
|
main_dict[key] = value
|
✨ Add support for shared/top-level parameters (dependencies, tags, etc) (#2434)
* ✨ Add Default and DefaultPlaceholder data structures
to handle defaults and overrides
* ✨ Add utils to get values by priority handling DefaultPlaceholders
* ✨ Add support for top-level parameters in FastAPI, APIRouter, include_router
including: prefix, tags, dependencies, deprecated, include_in_schema, responses, default_response_class, callbacks
* ♻️ Update openapi utils to handle DefaultPlaceholder for response_class
* 📝 Update bigger-application example code to use top-level params
and showcase them in APIRouter, FastAPI, include_router
* 📝 Update docs for Bigger Applications, include diagrams, top-level params
* 🔥 Simplify code and docs for callbacks as default_response_class is no longer required
* 📝 Add docs for top-level dependencies, in FastAPI()
* 📝 Add docs reference to top-level dependencies in docs for decorator
* ✅ Update/increase tests for Bigger Applications including shared parameters
* ✅ Add tests for top-level dependencies in FastAPI()
* ✅ Add tests for internal DefaultPlaceholder
* ✅ Update/increase tests for callbacks with top-level parameters
* ✅ Add LOTS of tests covering branches and cases for shared parameters
in top-level FastAPI, path operations, include_router, APIRouter, its path operations, nested include_router, nested APIRouter, and its path operations
* 🎨 Format/reorder parameters for consistency in FastAPI, APIRouter, include_router
2020-11-29 18:32:18 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|