Update 2025-04-13_16:26:34
This commit is contained in:
25
venv/lib/python3.11/site-packages/fastapi/__init__.py
Normal file
25
venv/lib/python3.11/site-packages/fastapi/__init__.py
Normal file
@ -0,0 +1,25 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.115.12"
|
||||
|
||||
from starlette import status as status
|
||||
|
||||
from .applications import FastAPI as FastAPI
|
||||
from .background import BackgroundTasks as BackgroundTasks
|
||||
from .datastructures import UploadFile as UploadFile
|
||||
from .exceptions import HTTPException as HTTPException
|
||||
from .exceptions import WebSocketException as WebSocketException
|
||||
from .param_functions import Body as Body
|
||||
from .param_functions import Cookie as Cookie
|
||||
from .param_functions import Depends as Depends
|
||||
from .param_functions import File as File
|
||||
from .param_functions import Form as Form
|
||||
from .param_functions import Header as Header
|
||||
from .param_functions import Path as Path
|
||||
from .param_functions import Query as Query
|
||||
from .param_functions import Security as Security
|
||||
from .requests import Request as Request
|
||||
from .responses import Response as Response
|
||||
from .routing import APIRouter as APIRouter
|
||||
from .websockets import WebSocket as WebSocket
|
||||
from .websockets import WebSocketDisconnect as WebSocketDisconnect
|
3
venv/lib/python3.11/site-packages/fastapi/__main__.py
Normal file
3
venv/lib/python3.11/site-packages/fastapi/__main__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from fastapi.cli import main
|
||||
|
||||
main()
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
659
venv/lib/python3.11/site-packages/fastapi/_compat.py
Normal file
659
venv/lib/python3.11/site-packages/fastapi/_compat.py
Normal file
@ -0,0 +1,659 @@
|
||||
from collections import deque
|
||||
from copy import copy
|
||||
from dataclasses import dataclass, is_dataclass
|
||||
from enum import Enum
|
||||
from functools import lru_cache
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Deque,
|
||||
Dict,
|
||||
FrozenSet,
|
||||
List,
|
||||
Mapping,
|
||||
Sequence,
|
||||
Set,
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
|
||||
from fastapi.exceptions import RequestErrorModel
|
||||
from fastapi.types import IncEx, ModelNameMap, UnionType
|
||||
from pydantic import BaseModel, create_model
|
||||
from pydantic.version import VERSION as PYDANTIC_VERSION
|
||||
from starlette.datastructures import UploadFile
|
||||
from typing_extensions import Annotated, Literal, get_args, get_origin
|
||||
|
||||
PYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2])
|
||||
PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2
|
||||
|
||||
|
||||
sequence_annotation_to_type = {
|
||||
Sequence: list,
|
||||
List: list,
|
||||
list: list,
|
||||
Tuple: tuple,
|
||||
tuple: tuple,
|
||||
Set: set,
|
||||
set: set,
|
||||
FrozenSet: frozenset,
|
||||
frozenset: frozenset,
|
||||
Deque: deque,
|
||||
deque: deque,
|
||||
}
|
||||
|
||||
sequence_types = tuple(sequence_annotation_to_type.keys())
|
||||
|
||||
Url: Type[Any]
|
||||
|
||||
if PYDANTIC_V2:
|
||||
from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError
|
||||
from pydantic import TypeAdapter
|
||||
from pydantic import ValidationError as ValidationError
|
||||
from pydantic._internal._schema_generation_shared import ( # type: ignore[attr-defined]
|
||||
GetJsonSchemaHandler as GetJsonSchemaHandler,
|
||||
)
|
||||
from pydantic._internal._typing_extra import eval_type_lenient
|
||||
from pydantic._internal._utils import lenient_issubclass as lenient_issubclass
|
||||
from pydantic.fields import FieldInfo
|
||||
from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema
|
||||
from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue
|
||||
from pydantic_core import CoreSchema as CoreSchema
|
||||
from pydantic_core import PydanticUndefined, PydanticUndefinedType
|
||||
from pydantic_core import Url as Url
|
||||
|
||||
try:
|
||||
from pydantic_core.core_schema import (
|
||||
with_info_plain_validator_function as with_info_plain_validator_function,
|
||||
)
|
||||
except ImportError: # pragma: no cover
|
||||
from pydantic_core.core_schema import (
|
||||
general_plain_validator_function as with_info_plain_validator_function, # noqa: F401
|
||||
)
|
||||
|
||||
RequiredParam = PydanticUndefined
|
||||
Undefined = PydanticUndefined
|
||||
UndefinedType = PydanticUndefinedType
|
||||
evaluate_forwardref = eval_type_lenient
|
||||
Validator = Any
|
||||
|
||||
class BaseConfig:
|
||||
pass
|
||||
|
||||
class ErrorWrapper(Exception):
|
||||
pass
|
||||
|
||||
@dataclass
|
||||
class ModelField:
|
||||
field_info: FieldInfo
|
||||
name: str
|
||||
mode: Literal["validation", "serialization"] = "validation"
|
||||
|
||||
@property
|
||||
def alias(self) -> str:
|
||||
a = self.field_info.alias
|
||||
return a if a is not None else self.name
|
||||
|
||||
@property
|
||||
def required(self) -> bool:
|
||||
return self.field_info.is_required()
|
||||
|
||||
@property
|
||||
def default(self) -> Any:
|
||||
return self.get_default()
|
||||
|
||||
@property
|
||||
def type_(self) -> Any:
|
||||
return self.field_info.annotation
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self._type_adapter: TypeAdapter[Any] = TypeAdapter(
|
||||
Annotated[self.field_info.annotation, self.field_info]
|
||||
)
|
||||
|
||||
def get_default(self) -> Any:
|
||||
if self.field_info.is_required():
|
||||
return Undefined
|
||||
return self.field_info.get_default(call_default_factory=True)
|
||||
|
||||
def validate(
|
||||
self,
|
||||
value: Any,
|
||||
values: Dict[str, Any] = {}, # noqa: B006
|
||||
*,
|
||||
loc: Tuple[Union[int, str], ...] = (),
|
||||
) -> Tuple[Any, Union[List[Dict[str, Any]], None]]:
|
||||
try:
|
||||
return (
|
||||
self._type_adapter.validate_python(value, from_attributes=True),
|
||||
None,
|
||||
)
|
||||
except ValidationError as exc:
|
||||
return None, _regenerate_error_with_loc(
|
||||
errors=exc.errors(include_url=False), loc_prefix=loc
|
||||
)
|
||||
|
||||
def serialize(
|
||||
self,
|
||||
value: Any,
|
||||
*,
|
||||
mode: Literal["json", "python"] = "json",
|
||||
include: Union[IncEx, None] = None,
|
||||
exclude: Union[IncEx, None] = None,
|
||||
by_alias: bool = True,
|
||||
exclude_unset: bool = False,
|
||||
exclude_defaults: bool = False,
|
||||
exclude_none: bool = False,
|
||||
) -> Any:
|
||||
# What calls this code passes a value that already called
|
||||
# self._type_adapter.validate_python(value)
|
||||
return self._type_adapter.dump_python(
|
||||
value,
|
||||
mode=mode,
|
||||
include=include,
|
||||
exclude=exclude,
|
||||
by_alias=by_alias,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_defaults=exclude_defaults,
|
||||
exclude_none=exclude_none,
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
# Each ModelField is unique for our purposes, to allow making a dict from
|
||||
# ModelField to its JSON Schema.
|
||||
return id(self)
|
||||
|
||||
def get_annotation_from_field_info(
|
||||
annotation: Any, field_info: FieldInfo, field_name: str
|
||||
) -> Any:
|
||||
return annotation
|
||||
|
||||
def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]:
|
||||
return errors # type: ignore[return-value]
|
||||
|
||||
def _model_rebuild(model: Type[BaseModel]) -> None:
|
||||
model.model_rebuild()
|
||||
|
||||
def _model_dump(
|
||||
model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
|
||||
) -> Any:
|
||||
return model.model_dump(mode=mode, **kwargs)
|
||||
|
||||
def _get_model_config(model: BaseModel) -> Any:
|
||||
return model.model_config
|
||||
|
||||
def get_schema_from_model_field(
|
||||
*,
|
||||
field: ModelField,
|
||||
schema_generator: GenerateJsonSchema,
|
||||
model_name_map: ModelNameMap,
|
||||
field_mapping: Dict[
|
||||
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
|
||||
],
|
||||
separate_input_output_schemas: bool = True,
|
||||
) -> Dict[str, Any]:
|
||||
override_mode: Union[Literal["validation"], None] = (
|
||||
None if separate_input_output_schemas else "validation"
|
||||
)
|
||||
# This expects that GenerateJsonSchema was already used to generate the definitions
|
||||
json_schema = field_mapping[(field, override_mode or field.mode)]
|
||||
if "$ref" not in json_schema:
|
||||
# TODO remove when deprecating Pydantic v1
|
||||
# Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207
|
||||
json_schema["title"] = (
|
||||
field.field_info.title or field.alias.title().replace("_", " ")
|
||||
)
|
||||
return json_schema
|
||||
|
||||
def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap:
|
||||
return {}
|
||||
|
||||
def get_definitions(
|
||||
*,
|
||||
fields: List[ModelField],
|
||||
schema_generator: GenerateJsonSchema,
|
||||
model_name_map: ModelNameMap,
|
||||
separate_input_output_schemas: bool = True,
|
||||
) -> Tuple[
|
||||
Dict[
|
||||
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
|
||||
],
|
||||
Dict[str, Dict[str, Any]],
|
||||
]:
|
||||
override_mode: Union[Literal["validation"], None] = (
|
||||
None if separate_input_output_schemas else "validation"
|
||||
)
|
||||
inputs = [
|
||||
(field, override_mode or field.mode, field._type_adapter.core_schema)
|
||||
for field in fields
|
||||
]
|
||||
field_mapping, definitions = schema_generator.generate_definitions(
|
||||
inputs=inputs
|
||||
)
|
||||
return field_mapping, definitions # type: ignore[return-value]
|
||||
|
||||
def is_scalar_field(field: ModelField) -> bool:
|
||||
from fastapi import params
|
||||
|
||||
return field_annotation_is_scalar(
|
||||
field.field_info.annotation
|
||||
) and not isinstance(field.field_info, params.Body)
|
||||
|
||||
def is_sequence_field(field: ModelField) -> bool:
|
||||
return field_annotation_is_sequence(field.field_info.annotation)
|
||||
|
||||
def is_scalar_sequence_field(field: ModelField) -> bool:
|
||||
return field_annotation_is_scalar_sequence(field.field_info.annotation)
|
||||
|
||||
def is_bytes_field(field: ModelField) -> bool:
|
||||
return is_bytes_or_nonable_bytes_annotation(field.type_)
|
||||
|
||||
def is_bytes_sequence_field(field: ModelField) -> bool:
|
||||
return is_bytes_sequence_annotation(field.type_)
|
||||
|
||||
def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
|
||||
cls = type(field_info)
|
||||
merged_field_info = cls.from_annotation(annotation)
|
||||
new_field_info = copy(field_info)
|
||||
new_field_info.metadata = merged_field_info.metadata
|
||||
new_field_info.annotation = merged_field_info.annotation
|
||||
return new_field_info
|
||||
|
||||
def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
|
||||
origin_type = (
|
||||
get_origin(field.field_info.annotation) or field.field_info.annotation
|
||||
)
|
||||
assert issubclass(origin_type, sequence_types) # type: ignore[arg-type]
|
||||
return sequence_annotation_to_type[origin_type](value) # type: ignore[no-any-return]
|
||||
|
||||
def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]:
|
||||
error = ValidationError.from_exception_data(
|
||||
"Field required", [{"type": "missing", "loc": loc, "input": {}}]
|
||||
).errors(include_url=False)[0]
|
||||
error["input"] = None
|
||||
return error # type: ignore[return-value]
|
||||
|
||||
def create_body_model(
|
||||
*, fields: Sequence[ModelField], model_name: str
|
||||
) -> Type[BaseModel]:
|
||||
field_params = {f.name: (f.field_info.annotation, f.field_info) for f in fields}
|
||||
BodyModel: Type[BaseModel] = create_model(model_name, **field_params) # type: ignore[call-overload]
|
||||
return BodyModel
|
||||
|
||||
def get_model_fields(model: Type[BaseModel]) -> List[ModelField]:
|
||||
return [
|
||||
ModelField(field_info=field_info, name=name)
|
||||
for name, field_info in model.model_fields.items()
|
||||
]
|
||||
|
||||
else:
|
||||
from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX
|
||||
from pydantic import AnyUrl as Url # noqa: F401
|
||||
from pydantic import ( # type: ignore[assignment]
|
||||
BaseConfig as BaseConfig, # noqa: F401
|
||||
)
|
||||
from pydantic import ValidationError as ValidationError # noqa: F401
|
||||
from pydantic.class_validators import ( # type: ignore[no-redef]
|
||||
Validator as Validator, # noqa: F401
|
||||
)
|
||||
from pydantic.error_wrappers import ( # type: ignore[no-redef]
|
||||
ErrorWrapper as ErrorWrapper, # noqa: F401
|
||||
)
|
||||
from pydantic.errors import MissingError
|
||||
from pydantic.fields import ( # type: ignore[attr-defined]
|
||||
SHAPE_FROZENSET,
|
||||
SHAPE_LIST,
|
||||
SHAPE_SEQUENCE,
|
||||
SHAPE_SET,
|
||||
SHAPE_SINGLETON,
|
||||
SHAPE_TUPLE,
|
||||
SHAPE_TUPLE_ELLIPSIS,
|
||||
)
|
||||
from pydantic.fields import FieldInfo as FieldInfo
|
||||
from pydantic.fields import ( # type: ignore[no-redef,attr-defined]
|
||||
ModelField as ModelField, # noqa: F401
|
||||
)
|
||||
|
||||
# Keeping old "Required" functionality from Pydantic V1, without
|
||||
# shadowing typing.Required.
|
||||
RequiredParam: Any = Ellipsis # type: ignore[no-redef]
|
||||
from pydantic.fields import ( # type: ignore[no-redef,attr-defined]
|
||||
Undefined as Undefined,
|
||||
)
|
||||
from pydantic.fields import ( # type: ignore[no-redef, attr-defined]
|
||||
UndefinedType as UndefinedType, # noqa: F401
|
||||
)
|
||||
from pydantic.schema import (
|
||||
field_schema,
|
||||
get_flat_models_from_fields,
|
||||
get_model_name_map,
|
||||
model_process_schema,
|
||||
)
|
||||
from pydantic.schema import ( # type: ignore[no-redef] # noqa: F401
|
||||
get_annotation_from_field_info as get_annotation_from_field_info,
|
||||
)
|
||||
from pydantic.typing import ( # type: ignore[no-redef]
|
||||
evaluate_forwardref as evaluate_forwardref, # noqa: F401
|
||||
)
|
||||
from pydantic.utils import ( # type: ignore[no-redef]
|
||||
lenient_issubclass as lenient_issubclass, # noqa: F401
|
||||
)
|
||||
|
||||
GetJsonSchemaHandler = Any # type: ignore[assignment,misc]
|
||||
JsonSchemaValue = Dict[str, Any] # type: ignore[misc]
|
||||
CoreSchema = Any # type: ignore[assignment,misc]
|
||||
|
||||
sequence_shapes = {
|
||||
SHAPE_LIST,
|
||||
SHAPE_SET,
|
||||
SHAPE_FROZENSET,
|
||||
SHAPE_TUPLE,
|
||||
SHAPE_SEQUENCE,
|
||||
SHAPE_TUPLE_ELLIPSIS,
|
||||
}
|
||||
sequence_shape_to_type = {
|
||||
SHAPE_LIST: list,
|
||||
SHAPE_SET: set,
|
||||
SHAPE_TUPLE: tuple,
|
||||
SHAPE_SEQUENCE: list,
|
||||
SHAPE_TUPLE_ELLIPSIS: list,
|
||||
}
|
||||
|
||||
@dataclass
|
||||
class GenerateJsonSchema: # type: ignore[no-redef]
|
||||
ref_template: str
|
||||
|
||||
class PydanticSchemaGenerationError(Exception): # type: ignore[no-redef]
|
||||
pass
|
||||
|
||||
def with_info_plain_validator_function( # type: ignore[misc]
|
||||
function: Callable[..., Any],
|
||||
*,
|
||||
ref: Union[str, None] = None,
|
||||
metadata: Any = None,
|
||||
serialization: Any = None,
|
||||
) -> Any:
|
||||
return {}
|
||||
|
||||
def get_model_definitions(
|
||||
*,
|
||||
flat_models: Set[Union[Type[BaseModel], Type[Enum]]],
|
||||
model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
|
||||
) -> Dict[str, Any]:
|
||||
definitions: Dict[str, Dict[str, Any]] = {}
|
||||
for model in flat_models:
|
||||
m_schema, m_definitions, m_nested_models = model_process_schema(
|
||||
model, model_name_map=model_name_map, ref_prefix=REF_PREFIX
|
||||
)
|
||||
definitions.update(m_definitions)
|
||||
model_name = model_name_map[model]
|
||||
if "description" in m_schema:
|
||||
m_schema["description"] = m_schema["description"].split("\f")[0]
|
||||
definitions[model_name] = m_schema
|
||||
return definitions
|
||||
|
||||
def is_pv1_scalar_field(field: ModelField) -> bool:
|
||||
from fastapi import params
|
||||
|
||||
field_info = field.field_info
|
||||
if not (
|
||||
field.shape == SHAPE_SINGLETON # type: ignore[attr-defined]
|
||||
and not lenient_issubclass(field.type_, BaseModel)
|
||||
and not lenient_issubclass(field.type_, dict)
|
||||
and not field_annotation_is_sequence(field.type_)
|
||||
and not is_dataclass(field.type_)
|
||||
and not isinstance(field_info, params.Body)
|
||||
):
|
||||
return False
|
||||
if field.sub_fields: # type: ignore[attr-defined]
|
||||
if not all(
|
||||
is_pv1_scalar_field(f)
|
||||
for f in field.sub_fields # type: ignore[attr-defined]
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_pv1_scalar_sequence_field(field: ModelField) -> bool:
|
||||
if (field.shape in sequence_shapes) and not lenient_issubclass( # type: ignore[attr-defined]
|
||||
field.type_, BaseModel
|
||||
):
|
||||
if field.sub_fields is not None: # type: ignore[attr-defined]
|
||||
for sub_field in field.sub_fields: # type: ignore[attr-defined]
|
||||
if not is_pv1_scalar_field(sub_field):
|
||||
return False
|
||||
return True
|
||||
if _annotation_is_sequence(field.type_):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]:
|
||||
use_errors: List[Any] = []
|
||||
for error in errors:
|
||||
if isinstance(error, ErrorWrapper):
|
||||
new_errors = ValidationError( # type: ignore[call-arg]
|
||||
errors=[error], model=RequestErrorModel
|
||||
).errors()
|
||||
use_errors.extend(new_errors)
|
||||
elif isinstance(error, list):
|
||||
use_errors.extend(_normalize_errors(error))
|
||||
else:
|
||||
use_errors.append(error)
|
||||
return use_errors
|
||||
|
||||
def _model_rebuild(model: Type[BaseModel]) -> None:
|
||||
model.update_forward_refs()
|
||||
|
||||
def _model_dump(
|
||||
model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
|
||||
) -> Any:
|
||||
return model.dict(**kwargs)
|
||||
|
||||
def _get_model_config(model: BaseModel) -> Any:
|
||||
return model.__config__ # type: ignore[attr-defined]
|
||||
|
||||
def get_schema_from_model_field(
|
||||
*,
|
||||
field: ModelField,
|
||||
schema_generator: GenerateJsonSchema,
|
||||
model_name_map: ModelNameMap,
|
||||
field_mapping: Dict[
|
||||
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
|
||||
],
|
||||
separate_input_output_schemas: bool = True,
|
||||
) -> Dict[str, Any]:
|
||||
# This expects that GenerateJsonSchema was already used to generate the definitions
|
||||
return field_schema( # type: ignore[no-any-return]
|
||||
field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
|
||||
)[0]
|
||||
|
||||
def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap:
|
||||
models = get_flat_models_from_fields(fields, known_models=set())
|
||||
return get_model_name_map(models) # type: ignore[no-any-return]
|
||||
|
||||
def get_definitions(
|
||||
*,
|
||||
fields: List[ModelField],
|
||||
schema_generator: GenerateJsonSchema,
|
||||
model_name_map: ModelNameMap,
|
||||
separate_input_output_schemas: bool = True,
|
||||
) -> Tuple[
|
||||
Dict[
|
||||
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
|
||||
],
|
||||
Dict[str, Dict[str, Any]],
|
||||
]:
|
||||
models = get_flat_models_from_fields(fields, known_models=set())
|
||||
return {}, get_model_definitions(
|
||||
flat_models=models, model_name_map=model_name_map
|
||||
)
|
||||
|
||||
def is_scalar_field(field: ModelField) -> bool:
|
||||
return is_pv1_scalar_field(field)
|
||||
|
||||
def is_sequence_field(field: ModelField) -> bool:
|
||||
return field.shape in sequence_shapes or _annotation_is_sequence(field.type_) # type: ignore[attr-defined]
|
||||
|
||||
def is_scalar_sequence_field(field: ModelField) -> bool:
|
||||
return is_pv1_scalar_sequence_field(field)
|
||||
|
||||
def is_bytes_field(field: ModelField) -> bool:
|
||||
return lenient_issubclass(field.type_, bytes)
|
||||
|
||||
def is_bytes_sequence_field(field: ModelField) -> bool:
|
||||
return field.shape in sequence_shapes and lenient_issubclass(field.type_, bytes) # type: ignore[attr-defined]
|
||||
|
||||
def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
|
||||
return copy(field_info)
|
||||
|
||||
def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
|
||||
return sequence_shape_to_type[field.shape](value) # type: ignore[no-any-return,attr-defined]
|
||||
|
||||
def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]:
|
||||
missing_field_error = ErrorWrapper(MissingError(), loc=loc) # type: ignore[call-arg]
|
||||
new_error = ValidationError([missing_field_error], RequestErrorModel)
|
||||
return new_error.errors()[0] # type: ignore[return-value]
|
||||
|
||||
def create_body_model(
|
||||
*, fields: Sequence[ModelField], model_name: str
|
||||
) -> Type[BaseModel]:
|
||||
BodyModel = create_model(model_name)
|
||||
for f in fields:
|
||||
BodyModel.__fields__[f.name] = f # type: ignore[index]
|
||||
return BodyModel
|
||||
|
||||
def get_model_fields(model: Type[BaseModel]) -> List[ModelField]:
|
||||
return list(model.__fields__.values()) # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def _regenerate_error_with_loc(
|
||||
*, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...]
|
||||
) -> List[Dict[str, Any]]:
|
||||
updated_loc_errors: List[Any] = [
|
||||
{**err, "loc": loc_prefix + err.get("loc", ())}
|
||||
for err in _normalize_errors(errors)
|
||||
]
|
||||
|
||||
return updated_loc_errors
|
||||
|
||||
|
||||
def _annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool:
|
||||
if lenient_issubclass(annotation, (str, bytes)):
|
||||
return False
|
||||
return lenient_issubclass(annotation, sequence_types)
|
||||
|
||||
|
||||
def field_annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool:
|
||||
origin = get_origin(annotation)
|
||||
if origin is Union or origin is UnionType:
|
||||
for arg in get_args(annotation):
|
||||
if field_annotation_is_sequence(arg):
|
||||
return True
|
||||
return False
|
||||
return _annotation_is_sequence(annotation) or _annotation_is_sequence(
|
||||
get_origin(annotation)
|
||||
)
|
||||
|
||||
|
||||
def value_is_sequence(value: Any) -> bool:
|
||||
return isinstance(value, sequence_types) and not isinstance(value, (str, bytes)) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def _annotation_is_complex(annotation: Union[Type[Any], None]) -> bool:
|
||||
return (
|
||||
lenient_issubclass(annotation, (BaseModel, Mapping, UploadFile))
|
||||
or _annotation_is_sequence(annotation)
|
||||
or is_dataclass(annotation)
|
||||
)
|
||||
|
||||
|
||||
def field_annotation_is_complex(annotation: Union[Type[Any], None]) -> bool:
|
||||
origin = get_origin(annotation)
|
||||
if origin is Union or origin is UnionType:
|
||||
return any(field_annotation_is_complex(arg) for arg in get_args(annotation))
|
||||
|
||||
return (
|
||||
_annotation_is_complex(annotation)
|
||||
or _annotation_is_complex(origin)
|
||||
or hasattr(origin, "__pydantic_core_schema__")
|
||||
or hasattr(origin, "__get_pydantic_core_schema__")
|
||||
)
|
||||
|
||||
|
||||
def field_annotation_is_scalar(annotation: Any) -> bool:
|
||||
# handle Ellipsis here to make tuple[int, ...] work nicely
|
||||
return annotation is Ellipsis or not field_annotation_is_complex(annotation)
|
||||
|
||||
|
||||
def field_annotation_is_scalar_sequence(annotation: Union[Type[Any], None]) -> bool:
|
||||
origin = get_origin(annotation)
|
||||
if origin is Union or origin is UnionType:
|
||||
at_least_one_scalar_sequence = False
|
||||
for arg in get_args(annotation):
|
||||
if field_annotation_is_scalar_sequence(arg):
|
||||
at_least_one_scalar_sequence = True
|
||||
continue
|
||||
elif not field_annotation_is_scalar(arg):
|
||||
return False
|
||||
return at_least_one_scalar_sequence
|
||||
return field_annotation_is_sequence(annotation) and all(
|
||||
field_annotation_is_scalar(sub_annotation)
|
||||
for sub_annotation in get_args(annotation)
|
||||
)
|
||||
|
||||
|
||||
def is_bytes_or_nonable_bytes_annotation(annotation: Any) -> bool:
|
||||
if lenient_issubclass(annotation, bytes):
|
||||
return True
|
||||
origin = get_origin(annotation)
|
||||
if origin is Union or origin is UnionType:
|
||||
for arg in get_args(annotation):
|
||||
if lenient_issubclass(arg, bytes):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_uploadfile_or_nonable_uploadfile_annotation(annotation: Any) -> bool:
|
||||
if lenient_issubclass(annotation, UploadFile):
|
||||
return True
|
||||
origin = get_origin(annotation)
|
||||
if origin is Union or origin is UnionType:
|
||||
for arg in get_args(annotation):
|
||||
if lenient_issubclass(arg, UploadFile):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_bytes_sequence_annotation(annotation: Any) -> bool:
|
||||
origin = get_origin(annotation)
|
||||
if origin is Union or origin is UnionType:
|
||||
at_least_one = False
|
||||
for arg in get_args(annotation):
|
||||
if is_bytes_sequence_annotation(arg):
|
||||
at_least_one = True
|
||||
continue
|
||||
return at_least_one
|
||||
return field_annotation_is_sequence(annotation) and all(
|
||||
is_bytes_or_nonable_bytes_annotation(sub_annotation)
|
||||
for sub_annotation in get_args(annotation)
|
||||
)
|
||||
|
||||
|
||||
def is_uploadfile_sequence_annotation(annotation: Any) -> bool:
|
||||
origin = get_origin(annotation)
|
||||
if origin is Union or origin is UnionType:
|
||||
at_least_one = False
|
||||
for arg in get_args(annotation):
|
||||
if is_uploadfile_sequence_annotation(arg):
|
||||
at_least_one = True
|
||||
continue
|
||||
return at_least_one
|
||||
return field_annotation_is_sequence(annotation) and all(
|
||||
is_uploadfile_or_nonable_uploadfile_annotation(sub_annotation)
|
||||
for sub_annotation in get_args(annotation)
|
||||
)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_cached_model_fields(model: Type[BaseModel]) -> List[ModelField]:
|
||||
return get_model_fields(model)
|
4585
venv/lib/python3.11/site-packages/fastapi/applications.py
Normal file
4585
venv/lib/python3.11/site-packages/fastapi/applications.py
Normal file
File diff suppressed because it is too large
Load Diff
59
venv/lib/python3.11/site-packages/fastapi/background.py
Normal file
59
venv/lib/python3.11/site-packages/fastapi/background.py
Normal file
@ -0,0 +1,59 @@
|
||||
from typing import Any, Callable
|
||||
|
||||
from starlette.background import BackgroundTasks as StarletteBackgroundTasks
|
||||
from typing_extensions import Annotated, Doc, ParamSpec
|
||||
|
||||
P = ParamSpec("P")
|
||||
|
||||
|
||||
class BackgroundTasks(StarletteBackgroundTasks):
|
||||
"""
|
||||
A collection of background tasks that will be called after a response has been
|
||||
sent to the client.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for Background Tasks](https://fastapi.tiangolo.com/tutorial/background-tasks/).
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from fastapi import BackgroundTasks, FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
def write_notification(email: str, message=""):
|
||||
with open("log.txt", mode="w") as email_file:
|
||||
content = f"notification for {email}: {message}"
|
||||
email_file.write(content)
|
||||
|
||||
|
||||
@app.post("/send-notification/{email}")
|
||||
async def send_notification(email: str, background_tasks: BackgroundTasks):
|
||||
background_tasks.add_task(write_notification, email, message="some notification")
|
||||
return {"message": "Notification sent in the background"}
|
||||
```
|
||||
"""
|
||||
|
||||
def add_task(
|
||||
self,
|
||||
func: Annotated[
|
||||
Callable[P, Any],
|
||||
Doc(
|
||||
"""
|
||||
The function to call after the response is sent.
|
||||
|
||||
It can be a regular `def` function or an `async def` function.
|
||||
"""
|
||||
),
|
||||
],
|
||||
*args: P.args,
|
||||
**kwargs: P.kwargs,
|
||||
) -> None:
|
||||
"""
|
||||
Add a function to be called in the background after the response is sent.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for Background Tasks](https://fastapi.tiangolo.com/tutorial/background-tasks/).
|
||||
"""
|
||||
return super().add_task(func, *args, **kwargs)
|
13
venv/lib/python3.11/site-packages/fastapi/cli.py
Normal file
13
venv/lib/python3.11/site-packages/fastapi/cli.py
Normal file
@ -0,0 +1,13 @@
|
||||
try:
|
||||
from fastapi_cli.cli import main as cli_main
|
||||
|
||||
except ImportError: # pragma: no cover
|
||||
cli_main = None # type: ignore
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not cli_main: # type: ignore[truthy-function]
|
||||
message = 'To use the fastapi command, please install "fastapi[standard]":\n\n\tpip install "fastapi[standard]"\n'
|
||||
print(message)
|
||||
raise RuntimeError(message) # noqa: B904
|
||||
cli_main()
|
39
venv/lib/python3.11/site-packages/fastapi/concurrency.py
Normal file
39
venv/lib/python3.11/site-packages/fastapi/concurrency.py
Normal file
@ -0,0 +1,39 @@
|
||||
from contextlib import asynccontextmanager as asynccontextmanager
|
||||
from typing import AsyncGenerator, ContextManager, TypeVar
|
||||
|
||||
import anyio.to_thread
|
||||
from anyio import CapacityLimiter
|
||||
from starlette.concurrency import iterate_in_threadpool as iterate_in_threadpool # noqa
|
||||
from starlette.concurrency import run_in_threadpool as run_in_threadpool # noqa
|
||||
from starlette.concurrency import ( # noqa
|
||||
run_until_first_complete as run_until_first_complete,
|
||||
)
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def contextmanager_in_threadpool(
|
||||
cm: ContextManager[_T],
|
||||
) -> AsyncGenerator[_T, None]:
|
||||
# blocking __exit__ from running waiting on a free thread
|
||||
# can create race conditions/deadlocks if the context manager itself
|
||||
# has its own internal pool (e.g. a database connection pool)
|
||||
# to avoid this we let __exit__ run without a capacity limit
|
||||
# since we're creating a new limiter for each call, any non-zero limit
|
||||
# works (1 is arbitrary)
|
||||
exit_limiter = CapacityLimiter(1)
|
||||
try:
|
||||
yield await run_in_threadpool(cm.__enter__)
|
||||
except Exception as e:
|
||||
ok = bool(
|
||||
await anyio.to_thread.run_sync(
|
||||
cm.__exit__, type(e), e, e.__traceback__, limiter=exit_limiter
|
||||
)
|
||||
)
|
||||
if not ok:
|
||||
raise e
|
||||
else:
|
||||
await anyio.to_thread.run_sync(
|
||||
cm.__exit__, None, None, None, limiter=exit_limiter
|
||||
)
|
204
venv/lib/python3.11/site-packages/fastapi/datastructures.py
Normal file
204
venv/lib/python3.11/site-packages/fastapi/datastructures.py
Normal file
@ -0,0 +1,204 @@
|
||||
from typing import (
|
||||
Any,
|
||||
BinaryIO,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
Optional,
|
||||
Type,
|
||||
TypeVar,
|
||||
cast,
|
||||
)
|
||||
|
||||
from fastapi._compat import (
|
||||
PYDANTIC_V2,
|
||||
CoreSchema,
|
||||
GetJsonSchemaHandler,
|
||||
JsonSchemaValue,
|
||||
with_info_plain_validator_function,
|
||||
)
|
||||
from starlette.datastructures import URL as URL # noqa: F401
|
||||
from starlette.datastructures import Address as Address # noqa: F401
|
||||
from starlette.datastructures import FormData as FormData # noqa: F401
|
||||
from starlette.datastructures import Headers as Headers # noqa: F401
|
||||
from starlette.datastructures import QueryParams as QueryParams # noqa: F401
|
||||
from starlette.datastructures import State as State # noqa: F401
|
||||
from starlette.datastructures import UploadFile as StarletteUploadFile
|
||||
from typing_extensions import Annotated, Doc
|
||||
|
||||
|
||||
class UploadFile(StarletteUploadFile):
|
||||
"""
|
||||
A file uploaded in a request.
|
||||
|
||||
Define it as a *path operation function* (or dependency) parameter.
|
||||
|
||||
If you are using a regular `def` function, you can use the `upload_file.file`
|
||||
attribute to access the raw standard Python file (blocking, not async), useful and
|
||||
needed for non-async code.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for Request Files](https://fastapi.tiangolo.com/tutorial/request-files/).
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import FastAPI, File, UploadFile
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.post("/files/")
|
||||
async def create_file(file: Annotated[bytes, File()]):
|
||||
return {"file_size": len(file)}
|
||||
|
||||
|
||||
@app.post("/uploadfile/")
|
||||
async def create_upload_file(file: UploadFile):
|
||||
return {"filename": file.filename}
|
||||
```
|
||||
"""
|
||||
|
||||
file: Annotated[
|
||||
BinaryIO,
|
||||
Doc("The standard Python file object (non-async)."),
|
||||
]
|
||||
filename: Annotated[Optional[str], Doc("The original file name.")]
|
||||
size: Annotated[Optional[int], Doc("The size of the file in bytes.")]
|
||||
headers: Annotated[Headers, Doc("The headers of the request.")]
|
||||
content_type: Annotated[
|
||||
Optional[str], Doc("The content type of the request, from the headers.")
|
||||
]
|
||||
|
||||
async def write(
|
||||
self,
|
||||
data: Annotated[
|
||||
bytes,
|
||||
Doc(
|
||||
"""
|
||||
The bytes to write to the file.
|
||||
"""
|
||||
),
|
||||
],
|
||||
) -> None:
|
||||
"""
|
||||
Write some bytes to the file.
|
||||
|
||||
You normally wouldn't use this from a file you read in a request.
|
||||
|
||||
To be awaitable, compatible with async, this is run in threadpool.
|
||||
"""
|
||||
return await super().write(data)
|
||||
|
||||
async def read(
|
||||
self,
|
||||
size: Annotated[
|
||||
int,
|
||||
Doc(
|
||||
"""
|
||||
The number of bytes to read from the file.
|
||||
"""
|
||||
),
|
||||
] = -1,
|
||||
) -> bytes:
|
||||
"""
|
||||
Read some bytes from the file.
|
||||
|
||||
To be awaitable, compatible with async, this is run in threadpool.
|
||||
"""
|
||||
return await super().read(size)
|
||||
|
||||
async def seek(
|
||||
self,
|
||||
offset: Annotated[
|
||||
int,
|
||||
Doc(
|
||||
"""
|
||||
The position in bytes to seek to in the file.
|
||||
"""
|
||||
),
|
||||
],
|
||||
) -> None:
|
||||
"""
|
||||
Move to a position in the file.
|
||||
|
||||
Any next read or write will be done from that position.
|
||||
|
||||
To be awaitable, compatible with async, this is run in threadpool.
|
||||
"""
|
||||
return await super().seek(offset)
|
||||
|
||||
async def close(self) -> None:
|
||||
"""
|
||||
Close the file.
|
||||
|
||||
To be awaitable, compatible with async, this is run in threadpool.
|
||||
"""
|
||||
return await super().close()
|
||||
|
||||
@classmethod
|
||||
def __get_validators__(cls: Type["UploadFile"]) -> Iterable[Callable[..., Any]]:
|
||||
yield cls.validate
|
||||
|
||||
@classmethod
|
||||
def validate(cls: Type["UploadFile"], v: Any) -> Any:
|
||||
if not isinstance(v, StarletteUploadFile):
|
||||
raise ValueError(f"Expected UploadFile, received: {type(v)}")
|
||||
return v
|
||||
|
||||
@classmethod
|
||||
def _validate(cls, __input_value: Any, _: Any) -> "UploadFile":
|
||||
if not isinstance(__input_value, StarletteUploadFile):
|
||||
raise ValueError(f"Expected UploadFile, received: {type(__input_value)}")
|
||||
return cast(UploadFile, __input_value)
|
||||
|
||||
if not PYDANTIC_V2:
|
||||
|
||||
@classmethod
|
||||
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
|
||||
field_schema.update({"type": "string", "format": "binary"})
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_json_schema__(
|
||||
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
|
||||
) -> JsonSchemaValue:
|
||||
return {"type": "string", "format": "binary"}
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_core_schema__(
|
||||
cls, source: Type[Any], handler: Callable[[Any], CoreSchema]
|
||||
) -> CoreSchema:
|
||||
return with_info_plain_validator_function(cls._validate)
|
||||
|
||||
|
||||
class DefaultPlaceholder:
|
||||
"""
|
||||
You shouldn't use this class directly.
|
||||
|
||||
It's used internally to recognize when a default value has been overwritten, even
|
||||
if the overridden default value was truthy.
|
||||
"""
|
||||
|
||||
def __init__(self, value: Any):
|
||||
self.value = value
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self.value)
|
||||
|
||||
def __eq__(self, o: object) -> bool:
|
||||
return isinstance(o, DefaultPlaceholder) and o.value == self.value
|
||||
|
||||
|
||||
DefaultType = TypeVar("DefaultType")
|
||||
|
||||
|
||||
def Default(value: DefaultType) -> DefaultType:
|
||||
"""
|
||||
You shouldn't use this function directly.
|
||||
|
||||
It's used internally to recognize when a default value has been overwritten, even
|
||||
if the overridden default value was truthy.
|
||||
"""
|
||||
return DefaultPlaceholder(value) # type: ignore
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,37 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Callable, List, Optional, Sequence, Tuple
|
||||
|
||||
from fastapi._compat import ModelField
|
||||
from fastapi.security.base import SecurityBase
|
||||
|
||||
|
||||
@dataclass
|
||||
class SecurityRequirement:
|
||||
security_scheme: SecurityBase
|
||||
scopes: Optional[Sequence[str]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Dependant:
|
||||
path_params: List[ModelField] = field(default_factory=list)
|
||||
query_params: List[ModelField] = field(default_factory=list)
|
||||
header_params: List[ModelField] = field(default_factory=list)
|
||||
cookie_params: List[ModelField] = field(default_factory=list)
|
||||
body_params: List[ModelField] = field(default_factory=list)
|
||||
dependencies: List["Dependant"] = field(default_factory=list)
|
||||
security_requirements: List[SecurityRequirement] = field(default_factory=list)
|
||||
name: Optional[str] = None
|
||||
call: Optional[Callable[..., Any]] = None
|
||||
request_param_name: Optional[str] = None
|
||||
websocket_param_name: Optional[str] = None
|
||||
http_connection_param_name: Optional[str] = None
|
||||
response_param_name: Optional[str] = None
|
||||
background_tasks_param_name: Optional[str] = None
|
||||
security_scopes_param_name: Optional[str] = None
|
||||
security_scopes: Optional[List[str]] = None
|
||||
use_cache: bool = True
|
||||
path: Optional[str] = None
|
||||
cache_key: Tuple[Optional[Callable[..., Any]], Tuple[str, ...]] = field(init=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.cache_key = (self.call, tuple(sorted(set(self.security_scopes or []))))
|
980
venv/lib/python3.11/site-packages/fastapi/dependencies/utils.py
Normal file
980
venv/lib/python3.11/site-packages/fastapi/dependencies/utils.py
Normal file
@ -0,0 +1,980 @@
|
||||
import inspect
|
||||
from contextlib import AsyncExitStack, contextmanager
|
||||
from copy import copy, deepcopy
|
||||
from dataclasses import dataclass
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Coroutine,
|
||||
Dict,
|
||||
ForwardRef,
|
||||
List,
|
||||
Mapping,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
import anyio
|
||||
from fastapi import params
|
||||
from fastapi._compat import (
|
||||
PYDANTIC_V2,
|
||||
ErrorWrapper,
|
||||
ModelField,
|
||||
RequiredParam,
|
||||
Undefined,
|
||||
_regenerate_error_with_loc,
|
||||
copy_field_info,
|
||||
create_body_model,
|
||||
evaluate_forwardref,
|
||||
field_annotation_is_scalar,
|
||||
get_annotation_from_field_info,
|
||||
get_cached_model_fields,
|
||||
get_missing_field_error,
|
||||
is_bytes_field,
|
||||
is_bytes_sequence_field,
|
||||
is_scalar_field,
|
||||
is_scalar_sequence_field,
|
||||
is_sequence_field,
|
||||
is_uploadfile_or_nonable_uploadfile_annotation,
|
||||
is_uploadfile_sequence_annotation,
|
||||
lenient_issubclass,
|
||||
sequence_types,
|
||||
serialize_sequence_value,
|
||||
value_is_sequence,
|
||||
)
|
||||
from fastapi.background import BackgroundTasks
|
||||
from fastapi.concurrency import (
|
||||
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_model_field, get_path_param_names
|
||||
from pydantic import BaseModel
|
||||
from pydantic.fields import FieldInfo
|
||||
from starlette.background import BackgroundTasks as StarletteBackgroundTasks
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
from starlette.datastructures import (
|
||||
FormData,
|
||||
Headers,
|
||||
ImmutableMultiDict,
|
||||
QueryParams,
|
||||
UploadFile,
|
||||
)
|
||||
from starlette.requests import HTTPConnection, Request
|
||||
from starlette.responses import Response
|
||||
from starlette.websockets import WebSocket
|
||||
from typing_extensions import Annotated, get_args, get_origin
|
||||
|
||||
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 ensure_multipart_is_installed() -> None:
|
||||
try:
|
||||
from python_multipart import __version__
|
||||
|
||||
# Import an attribute that can be mocked/deleted in testing
|
||||
assert __version__ > "0.0.12"
|
||||
except (ImportError, AssertionError):
|
||||
try:
|
||||
# __version__ is available in both multiparts, and can be mocked
|
||||
from multipart import __version__ # type: ignore[no-redef,import-untyped]
|
||||
|
||||
assert __version__
|
||||
try:
|
||||
# parse_options_header is only available in the right multipart
|
||||
from multipart.multipart import ( # type: ignore[import-untyped]
|
||||
parse_options_header,
|
||||
)
|
||||
|
||||
assert parse_options_header
|
||||
except ImportError:
|
||||
logger.error(multipart_incorrect_install_error)
|
||||
raise RuntimeError(multipart_incorrect_install_error) from None
|
||||
except ImportError:
|
||||
logger.error(multipart_not_installed_error)
|
||||
raise RuntimeError(multipart_not_installed_error) from None
|
||||
|
||||
|
||||
def get_param_sub_dependant(
|
||||
*,
|
||||
param_name: str,
|
||||
depends: params.Depends,
|
||||
path: str,
|
||||
security_scopes: Optional[List[str]] = None,
|
||||
) -> Dependant:
|
||||
assert depends.dependency
|
||||
return get_sub_dependant(
|
||||
depends=depends,
|
||||
dependency=depends.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)
|
||||
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_requirements=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_fields_from_params(fields: List[ModelField]) -> List[ModelField]:
|
||||
if not fields:
|
||||
return fields
|
||||
first_field = fields[0]
|
||||
if len(fields) == 1 and lenient_issubclass(first_field.type_, BaseModel):
|
||||
fields_to_extract = get_cached_model_fields(first_field.type_)
|
||||
return fields_to_extract
|
||||
return fields
|
||||
|
||||
|
||||
def get_flat_params(dependant: Dependant) -> List[ModelField]:
|
||||
flat_dependant = get_flat_dependant(dependant, skip_repeats=True)
|
||||
path_params = _get_flat_fields_from_params(flat_dependant.path_params)
|
||||
query_params = _get_flat_fields_from_params(flat_dependant.query_params)
|
||||
header_params = _get_flat_fields_from_params(flat_dependant.header_params)
|
||||
cookie_params = _get_flat_fields_from_params(flat_dependant.cookie_params)
|
||||
return path_params + query_params + header_params + cookie_params
|
||||
|
||||
|
||||
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.annotation, globalns),
|
||||
)
|
||||
for param in signature.parameters.values()
|
||||
]
|
||||
typed_signature = inspect.Signature(typed_params)
|
||||
return typed_signature
|
||||
|
||||
|
||||
def get_typed_annotation(annotation: Any, globalns: Dict[str, Any]) -> Any:
|
||||
if isinstance(annotation, str):
|
||||
annotation = ForwardRef(annotation)
|
||||
annotation = evaluate_forwardref(annotation, globalns, globalns)
|
||||
return annotation
|
||||
|
||||
|
||||
def get_typed_return_annotation(call: Callable[..., Any]) -> Any:
|
||||
signature = inspect.signature(call)
|
||||
annotation = signature.return_annotation
|
||||
|
||||
if annotation is inspect.Signature.empty:
|
||||
return None
|
||||
|
||||
globalns = getattr(call, "__globals__", {})
|
||||
return get_typed_annotation(annotation, globalns)
|
||||
|
||||
|
||||
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
|
||||
dependant = Dependant(
|
||||
call=call,
|
||||
name=name,
|
||||
path=path,
|
||||
security_scopes=security_scopes,
|
||||
use_cache=use_cache,
|
||||
)
|
||||
for param_name, param in signature_params.items():
|
||||
is_path_param = param_name in path_param_names
|
||||
param_details = analyze_param(
|
||||
param_name=param_name,
|
||||
annotation=param.annotation,
|
||||
value=param.default,
|
||||
is_path_param=is_path_param,
|
||||
)
|
||||
if param_details.depends is not None:
|
||||
sub_dependant = get_param_sub_dependant(
|
||||
param_name=param_name,
|
||||
depends=param_details.depends,
|
||||
path=path,
|
||||
security_scopes=security_scopes,
|
||||
)
|
||||
dependant.dependencies.append(sub_dependant)
|
||||
continue
|
||||
if add_non_field_param_to_dependency(
|
||||
param_name=param_name,
|
||||
type_annotation=param_details.type_annotation,
|
||||
dependant=dependant,
|
||||
):
|
||||
assert param_details.field is None, (
|
||||
f"Cannot specify multiple FastAPI annotations for {param_name!r}"
|
||||
)
|
||||
continue
|
||||
assert param_details.field is not None
|
||||
if isinstance(param_details.field.field_info, params.Body):
|
||||
dependant.body_params.append(param_details.field)
|
||||
else:
|
||||
add_param_to_fields(field=param_details.field, dependant=dependant)
|
||||
return dependant
|
||||
|
||||
|
||||
def add_non_field_param_to_dependency(
|
||||
*, param_name: str, type_annotation: Any, dependant: Dependant
|
||||
) -> Optional[bool]:
|
||||
if lenient_issubclass(type_annotation, Request):
|
||||
dependant.request_param_name = param_name
|
||||
return True
|
||||
elif lenient_issubclass(type_annotation, WebSocket):
|
||||
dependant.websocket_param_name = param_name
|
||||
return True
|
||||
elif lenient_issubclass(type_annotation, HTTPConnection):
|
||||
dependant.http_connection_param_name = param_name
|
||||
return True
|
||||
elif lenient_issubclass(type_annotation, Response):
|
||||
dependant.response_param_name = param_name
|
||||
return True
|
||||
elif lenient_issubclass(type_annotation, StarletteBackgroundTasks):
|
||||
dependant.background_tasks_param_name = param_name
|
||||
return True
|
||||
elif lenient_issubclass(type_annotation, SecurityScopes):
|
||||
dependant.security_scopes_param_name = param_name
|
||||
return True
|
||||
return None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ParamDetails:
|
||||
type_annotation: Any
|
||||
depends: Optional[params.Depends]
|
||||
field: Optional[ModelField]
|
||||
|
||||
|
||||
def analyze_param(
|
||||
*,
|
||||
param_name: str,
|
||||
annotation: Any,
|
||||
value: Any,
|
||||
is_path_param: bool,
|
||||
) -> ParamDetails:
|
||||
field_info = None
|
||||
depends = None
|
||||
type_annotation: Any = Any
|
||||
use_annotation: Any = Any
|
||||
if annotation is not inspect.Signature.empty:
|
||||
use_annotation = annotation
|
||||
type_annotation = annotation
|
||||
# Extract Annotated info
|
||||
if get_origin(use_annotation) is Annotated:
|
||||
annotated_args = get_args(annotation)
|
||||
type_annotation = annotated_args[0]
|
||||
fastapi_annotations = [
|
||||
arg
|
||||
for arg in annotated_args[1:]
|
||||
if isinstance(arg, (FieldInfo, params.Depends))
|
||||
]
|
||||
fastapi_specific_annotations = [
|
||||
arg
|
||||
for arg in fastapi_annotations
|
||||
if isinstance(arg, (params.Param, params.Body, params.Depends))
|
||||
]
|
||||
if fastapi_specific_annotations:
|
||||
fastapi_annotation: Union[FieldInfo, params.Depends, None] = (
|
||||
fastapi_specific_annotations[-1]
|
||||
)
|
||||
else:
|
||||
fastapi_annotation = None
|
||||
# Set default for Annotated FieldInfo
|
||||
if isinstance(fastapi_annotation, FieldInfo):
|
||||
# Copy `field_info` because we mutate `field_info.default` below.
|
||||
field_info = copy_field_info(
|
||||
field_info=fastapi_annotation, annotation=use_annotation
|
||||
)
|
||||
assert (
|
||||
field_info.default is Undefined or field_info.default is RequiredParam
|
||||
), (
|
||||
f"`{field_info.__class__.__name__}` default value cannot be set in"
|
||||
f" `Annotated` for {param_name!r}. Set the default value with `=` instead."
|
||||
)
|
||||
if value is not inspect.Signature.empty:
|
||||
assert not is_path_param, "Path parameters cannot have default values"
|
||||
field_info.default = value
|
||||
else:
|
||||
field_info.default = RequiredParam
|
||||
# Get Annotated Depends
|
||||
elif isinstance(fastapi_annotation, params.Depends):
|
||||
depends = fastapi_annotation
|
||||
# Get Depends from default value
|
||||
if isinstance(value, params.Depends):
|
||||
assert depends is None, (
|
||||
"Cannot specify `Depends` in `Annotated` and default value"
|
||||
f" together for {param_name!r}"
|
||||
)
|
||||
assert field_info is None, (
|
||||
"Cannot specify a FastAPI annotation in `Annotated` and `Depends` as a"
|
||||
f" default value together for {param_name!r}"
|
||||
)
|
||||
depends = value
|
||||
# Get FieldInfo from default value
|
||||
elif isinstance(value, FieldInfo):
|
||||
assert field_info is None, (
|
||||
"Cannot specify FastAPI annotations in `Annotated` and default value"
|
||||
f" together for {param_name!r}"
|
||||
)
|
||||
field_info = value
|
||||
if PYDANTIC_V2:
|
||||
field_info.annotation = type_annotation
|
||||
|
||||
# Get Depends from type annotation
|
||||
if depends is not None and depends.dependency is None:
|
||||
# Copy `depends` before mutating it
|
||||
depends = copy(depends)
|
||||
depends.dependency = type_annotation
|
||||
|
||||
# Handle non-param type annotations like Request
|
||||
if lenient_issubclass(
|
||||
type_annotation,
|
||||
(
|
||||
Request,
|
||||
WebSocket,
|
||||
HTTPConnection,
|
||||
Response,
|
||||
StarletteBackgroundTasks,
|
||||
SecurityScopes,
|
||||
),
|
||||
):
|
||||
assert depends is None, f"Cannot specify `Depends` for type {type_annotation!r}"
|
||||
assert field_info is None, (
|
||||
f"Cannot specify FastAPI annotation for type {type_annotation!r}"
|
||||
)
|
||||
# Handle default assignations, neither field_info nor depends was not found in Annotated nor default value
|
||||
elif field_info is None and depends is None:
|
||||
default_value = value if value is not inspect.Signature.empty else RequiredParam
|
||||
if is_path_param:
|
||||
# We might check here that `default_value is RequiredParam`, but the fact is that the same
|
||||
# parameter might sometimes be a path parameter and sometimes not. See
|
||||
# `tests/test_infer_param_optionality.py` for an example.
|
||||
field_info = params.Path(annotation=use_annotation)
|
||||
elif is_uploadfile_or_nonable_uploadfile_annotation(
|
||||
type_annotation
|
||||
) or is_uploadfile_sequence_annotation(type_annotation):
|
||||
field_info = params.File(annotation=use_annotation, default=default_value)
|
||||
elif not field_annotation_is_scalar(annotation=type_annotation):
|
||||
field_info = params.Body(annotation=use_annotation, default=default_value)
|
||||
else:
|
||||
field_info = params.Query(annotation=use_annotation, default=default_value)
|
||||
|
||||
field = None
|
||||
# It's a field_info, not a dependency
|
||||
if field_info is not None:
|
||||
# Handle field_info.in_
|
||||
if is_path_param:
|
||||
assert isinstance(field_info, params.Path), (
|
||||
f"Cannot use `{field_info.__class__.__name__}` for path param"
|
||||
f" {param_name!r}"
|
||||
)
|
||||
elif (
|
||||
isinstance(field_info, params.Param)
|
||||
and getattr(field_info, "in_", None) is None
|
||||
):
|
||||
field_info.in_ = params.ParamTypes.query
|
||||
use_annotation_from_field_info = get_annotation_from_field_info(
|
||||
use_annotation,
|
||||
field_info,
|
||||
param_name,
|
||||
)
|
||||
if isinstance(field_info, params.Form):
|
||||
ensure_multipart_is_installed()
|
||||
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_info.alias = alias
|
||||
field = create_model_field(
|
||||
name=param_name,
|
||||
type_=use_annotation_from_field_info,
|
||||
default=field_info.default,
|
||||
alias=alias,
|
||||
required=field_info.default in (RequiredParam, Undefined),
|
||||
field_info=field_info,
|
||||
)
|
||||
if is_path_param:
|
||||
assert is_scalar_field(field=field), (
|
||||
"Path params must be of one of the supported types"
|
||||
)
|
||||
elif isinstance(field_info, params.Query):
|
||||
assert (
|
||||
is_scalar_field(field)
|
||||
or is_scalar_sequence_field(field)
|
||||
or (
|
||||
lenient_issubclass(field.type_, BaseModel)
|
||||
# For Pydantic v1
|
||||
and getattr(field, "shape", 1) == 1
|
||||
)
|
||||
)
|
||||
|
||||
return ParamDetails(type_annotation=type_annotation, depends=depends, field=field)
|
||||
|
||||
|
||||
def add_param_to_fields(*, field: ModelField, dependant: Dependant) -> None:
|
||||
field_info = field.field_info
|
||||
field_info_in = getattr(field_info, "in_", None)
|
||||
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
|
||||
dunder_call = getattr(call, "__call__", None) # noqa: B004
|
||||
return inspect.iscoroutinefunction(dunder_call)
|
||||
|
||||
|
||||
def is_async_gen_callable(call: Callable[..., Any]) -> bool:
|
||||
if inspect.isasyncgenfunction(call):
|
||||
return True
|
||||
dunder_call = getattr(call, "__call__", None) # noqa: B004
|
||||
return inspect.isasyncgenfunction(dunder_call)
|
||||
|
||||
|
||||
def is_gen_callable(call: Callable[..., Any]) -> bool:
|
||||
if inspect.isgeneratorfunction(call):
|
||||
return True
|
||||
dunder_call = getattr(call, "__call__", None) # noqa: B004
|
||||
return inspect.isgeneratorfunction(dunder_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):
|
||||
cm = asynccontextmanager(call)(**sub_values)
|
||||
return await stack.enter_async_context(cm)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SolvedDependency:
|
||||
values: Dict[str, Any]
|
||||
errors: List[Any]
|
||||
background_tasks: Optional[StarletteBackgroundTasks]
|
||||
response: Response
|
||||
dependency_cache: Dict[Tuple[Callable[..., Any], Tuple[str]], Any]
|
||||
|
||||
|
||||
async def solve_dependencies(
|
||||
*,
|
||||
request: Union[Request, WebSocket],
|
||||
dependant: Dependant,
|
||||
body: Optional[Union[Dict[str, Any], FormData]] = None,
|
||||
background_tasks: Optional[StarletteBackgroundTasks] = None,
|
||||
response: Optional[Response] = None,
|
||||
dependency_overrides_provider: Optional[Any] = None,
|
||||
dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None,
|
||||
async_exit_stack: AsyncExitStack,
|
||||
embed_body_fields: bool,
|
||||
) -> SolvedDependency:
|
||||
values: Dict[str, Any] = {}
|
||||
errors: List[Any] = []
|
||||
if response is None:
|
||||
response = Response()
|
||||
del response.headers["content-length"]
|
||||
response.status_code = None # type: ignore
|
||||
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,
|
||||
)
|
||||
|
||||
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,
|
||||
async_exit_stack=async_exit_stack,
|
||||
embed_body_fields=embed_body_fields,
|
||||
)
|
||||
background_tasks = solved_result.background_tasks
|
||||
dependency_cache.update(solved_result.dependency_cache)
|
||||
if solved_result.errors:
|
||||
errors.extend(solved_result.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):
|
||||
solved = await solve_generator(
|
||||
call=call, stack=async_exit_stack, sub_values=solved_result.values
|
||||
)
|
||||
elif is_coroutine_callable(call):
|
||||
solved = await call(**solved_result.values)
|
||||
else:
|
||||
solved = await run_in_threadpool(call, **solved_result.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
|
||||
body_fields=dependant.body_params,
|
||||
received_body=body,
|
||||
embed_body_fields=embed_body_fields,
|
||||
)
|
||||
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 SolvedDependency(
|
||||
values=values,
|
||||
errors=errors,
|
||||
background_tasks=background_tasks,
|
||||
response=response,
|
||||
dependency_cache=dependency_cache,
|
||||
)
|
||||
|
||||
|
||||
def _validate_value_with_model_field(
|
||||
*, field: ModelField, value: Any, values: Dict[str, Any], loc: Tuple[str, ...]
|
||||
) -> Tuple[Any, List[Any]]:
|
||||
if value is None:
|
||||
if field.required:
|
||||
return None, [get_missing_field_error(loc=loc)]
|
||||
else:
|
||||
return deepcopy(field.default), []
|
||||
v_, errors_ = field.validate(value, values, loc=loc)
|
||||
if isinstance(errors_, ErrorWrapper):
|
||||
return None, [errors_]
|
||||
elif isinstance(errors_, list):
|
||||
new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=())
|
||||
return None, new_errors
|
||||
else:
|
||||
return v_, []
|
||||
|
||||
|
||||
def _get_multidict_value(
|
||||
field: ModelField, values: Mapping[str, Any], alias: Union[str, None] = None
|
||||
) -> Any:
|
||||
alias = alias or field.alias
|
||||
if is_sequence_field(field) and isinstance(values, (ImmutableMultiDict, Headers)):
|
||||
value = values.getlist(alias)
|
||||
else:
|
||||
value = values.get(alias, None)
|
||||
if (
|
||||
value is None
|
||||
or (
|
||||
isinstance(field.field_info, params.Form)
|
||||
and isinstance(value, str) # For type checks
|
||||
and value == ""
|
||||
)
|
||||
or (is_sequence_field(field) and len(value) == 0)
|
||||
):
|
||||
if field.required:
|
||||
return
|
||||
else:
|
||||
return deepcopy(field.default)
|
||||
return value
|
||||
|
||||
|
||||
def request_params_to_args(
|
||||
fields: Sequence[ModelField],
|
||||
received_params: Union[Mapping[str, Any], QueryParams, Headers],
|
||||
) -> Tuple[Dict[str, Any], List[Any]]:
|
||||
values: Dict[str, Any] = {}
|
||||
errors: List[Dict[str, Any]] = []
|
||||
|
||||
if not fields:
|
||||
return values, errors
|
||||
|
||||
first_field = fields[0]
|
||||
fields_to_extract = fields
|
||||
single_not_embedded_field = False
|
||||
default_convert_underscores = True
|
||||
if len(fields) == 1 and lenient_issubclass(first_field.type_, BaseModel):
|
||||
fields_to_extract = get_cached_model_fields(first_field.type_)
|
||||
single_not_embedded_field = True
|
||||
# If headers are in a Pydantic model, the way to disable convert_underscores
|
||||
# would be with Header(convert_underscores=False) at the Pydantic model level
|
||||
default_convert_underscores = getattr(
|
||||
first_field.field_info, "convert_underscores", True
|
||||
)
|
||||
|
||||
params_to_process: Dict[str, Any] = {}
|
||||
|
||||
processed_keys = set()
|
||||
|
||||
for field in fields_to_extract:
|
||||
alias = None
|
||||
if isinstance(received_params, Headers):
|
||||
# Handle fields extracted from a Pydantic Model for a header, each field
|
||||
# doesn't have a FieldInfo of type Header with the default convert_underscores=True
|
||||
convert_underscores = getattr(
|
||||
field.field_info, "convert_underscores", default_convert_underscores
|
||||
)
|
||||
if convert_underscores:
|
||||
alias = (
|
||||
field.alias
|
||||
if field.alias != field.name
|
||||
else field.name.replace("_", "-")
|
||||
)
|
||||
value = _get_multidict_value(field, received_params, alias=alias)
|
||||
if value is not None:
|
||||
params_to_process[field.name] = value
|
||||
processed_keys.add(alias or field.alias)
|
||||
processed_keys.add(field.name)
|
||||
|
||||
for key, value in received_params.items():
|
||||
if key not in processed_keys:
|
||||
params_to_process[key] = value
|
||||
|
||||
if single_not_embedded_field:
|
||||
field_info = first_field.field_info
|
||||
assert isinstance(field_info, params.Param), (
|
||||
"Params must be subclasses of Param"
|
||||
)
|
||||
loc: Tuple[str, ...] = (field_info.in_.value,)
|
||||
v_, errors_ = _validate_value_with_model_field(
|
||||
field=first_field, value=params_to_process, values=values, loc=loc
|
||||
)
|
||||
return {first_field.name: v_}, errors_
|
||||
|
||||
for field in fields:
|
||||
value = _get_multidict_value(field, received_params)
|
||||
field_info = field.field_info
|
||||
assert isinstance(field_info, params.Param), (
|
||||
"Params must be subclasses of Param"
|
||||
)
|
||||
loc = (field_info.in_.value, field.alias)
|
||||
v_, errors_ = _validate_value_with_model_field(
|
||||
field=field, value=value, values=values, loc=loc
|
||||
)
|
||||
if errors_:
|
||||
errors.extend(errors_)
|
||||
else:
|
||||
values[field.name] = v_
|
||||
return values, errors
|
||||
|
||||
|
||||
def _should_embed_body_fields(fields: List[ModelField]) -> bool:
|
||||
if not fields:
|
||||
return False
|
||||
# More than one dependency could have the same field, it would show up as multiple
|
||||
# fields but it's the same one, so count them by name
|
||||
body_param_names_set = {field.name for field in fields}
|
||||
# A top level field has to be a single field, not multiple
|
||||
if len(body_param_names_set) > 1:
|
||||
return True
|
||||
first_field = fields[0]
|
||||
# If it explicitly specifies it is embedded, it has to be embedded
|
||||
if getattr(first_field.field_info, "embed", None):
|
||||
return True
|
||||
# If it's a Form (or File) field, it has to be a BaseModel to be top level
|
||||
# otherwise it has to be embedded, so that the key value pair can be extracted
|
||||
if isinstance(first_field.field_info, params.Form) and not lenient_issubclass(
|
||||
first_field.type_, BaseModel
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
async def _extract_form_body(
|
||||
body_fields: List[ModelField],
|
||||
received_body: FormData,
|
||||
) -> Dict[str, Any]:
|
||||
values = {}
|
||||
first_field = body_fields[0]
|
||||
first_field_info = first_field.field_info
|
||||
|
||||
for field in body_fields:
|
||||
value = _get_multidict_value(field, received_body)
|
||||
if (
|
||||
isinstance(first_field_info, params.File)
|
||||
and is_bytes_field(field)
|
||||
and isinstance(value, UploadFile)
|
||||
):
|
||||
value = await value.read()
|
||||
elif (
|
||||
is_bytes_sequence_field(field)
|
||||
and isinstance(first_field_info, params.File)
|
||||
and value_is_sequence(value)
|
||||
):
|
||||
# For types
|
||||
assert isinstance(value, sequence_types) # type: ignore[arg-type]
|
||||
results: List[Union[bytes, str]] = []
|
||||
|
||||
async def process_fn(
|
||||
fn: Callable[[], Coroutine[Any, Any, Any]],
|
||||
) -> None:
|
||||
result = await fn()
|
||||
results.append(result) # noqa: B023
|
||||
|
||||
async with anyio.create_task_group() as tg:
|
||||
for sub_value in value:
|
||||
tg.start_soon(process_fn, sub_value.read)
|
||||
value = serialize_sequence_value(field=field, value=results)
|
||||
if value is not None:
|
||||
values[field.alias] = value
|
||||
for key, value in received_body.items():
|
||||
if key not in values:
|
||||
values[key] = value
|
||||
return values
|
||||
|
||||
|
||||
async def request_body_to_args(
|
||||
body_fields: List[ModelField],
|
||||
received_body: Optional[Union[Dict[str, Any], FormData]],
|
||||
embed_body_fields: bool,
|
||||
) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
|
||||
values: Dict[str, Any] = {}
|
||||
errors: List[Dict[str, Any]] = []
|
||||
assert body_fields, "request_body_to_args() should be called with fields"
|
||||
single_not_embedded_field = len(body_fields) == 1 and not embed_body_fields
|
||||
first_field = body_fields[0]
|
||||
body_to_process = received_body
|
||||
|
||||
fields_to_extract: List[ModelField] = body_fields
|
||||
|
||||
if single_not_embedded_field and lenient_issubclass(first_field.type_, BaseModel):
|
||||
fields_to_extract = get_cached_model_fields(first_field.type_)
|
||||
|
||||
if isinstance(received_body, FormData):
|
||||
body_to_process = await _extract_form_body(fields_to_extract, received_body)
|
||||
|
||||
if single_not_embedded_field:
|
||||
loc: Tuple[str, ...] = ("body",)
|
||||
v_, errors_ = _validate_value_with_model_field(
|
||||
field=first_field, value=body_to_process, values=values, loc=loc
|
||||
)
|
||||
return {first_field.name: v_}, errors_
|
||||
for field in body_fields:
|
||||
loc = ("body", field.alias)
|
||||
value: Optional[Any] = None
|
||||
if body_to_process is not None:
|
||||
try:
|
||||
value = body_to_process.get(field.alias)
|
||||
# If the received body is a list, not a dict
|
||||
except AttributeError:
|
||||
errors.append(get_missing_field_error(loc))
|
||||
continue
|
||||
v_, errors_ = _validate_value_with_model_field(
|
||||
field=field, value=value, values=values, loc=loc
|
||||
)
|
||||
if errors_:
|
||||
errors.extend(errors_)
|
||||
else:
|
||||
values[field.name] = v_
|
||||
return values, errors
|
||||
|
||||
|
||||
def get_body_field(
|
||||
*, flat_dependant: Dependant, name: str, embed_body_fields: bool
|
||||
) -> Optional[ModelField]:
|
||||
"""
|
||||
Get a ModelField representing the request body for a path operation, combining
|
||||
all body parameters into a single field if necessary.
|
||||
|
||||
Used to check if it's form data (with `isinstance(body_field, params.Form)`)
|
||||
or JSON and to generate the JSON Schema for a request body.
|
||||
|
||||
This is **not** used to validate/parse the request body, that's done with each
|
||||
individual body parameter.
|
||||
"""
|
||||
if not flat_dependant.body_params:
|
||||
return None
|
||||
first_param = flat_dependant.body_params[0]
|
||||
if not embed_body_fields:
|
||||
return first_param
|
||||
model_name = "Body_" + name
|
||||
BodyModel = create_body_model(
|
||||
fields=flat_dependant.body_params, model_name=model_name
|
||||
)
|
||||
required = any(True for f in flat_dependant.body_params if f.required)
|
||||
BodyFieldInfo_kwargs: Dict[str, Any] = {
|
||||
"annotation": BodyModel,
|
||||
"alias": "body",
|
||||
}
|
||||
if not required:
|
||||
BodyFieldInfo_kwargs["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 = [
|
||||
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_model_field(
|
||||
name="body",
|
||||
type_=BodyModel,
|
||||
required=required,
|
||||
alias="body",
|
||||
field_info=BodyFieldInfo(**BodyFieldInfo_kwargs),
|
||||
)
|
||||
return final_field
|
343
venv/lib/python3.11/site-packages/fastapi/encoders.py
Normal file
343
venv/lib/python3.11/site-packages/fastapi/encoders.py
Normal file
@ -0,0 +1,343 @@
|
||||
import dataclasses
|
||||
import datetime
|
||||
from collections import defaultdict, deque
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from ipaddress import (
|
||||
IPv4Address,
|
||||
IPv4Interface,
|
||||
IPv4Network,
|
||||
IPv6Address,
|
||||
IPv6Interface,
|
||||
IPv6Network,
|
||||
)
|
||||
from pathlib import Path, PurePath
|
||||
from re import Pattern
|
||||
from types import GeneratorType
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi.types import IncEx
|
||||
from pydantic import BaseModel
|
||||
from pydantic.color import Color
|
||||
from pydantic.networks import AnyUrl, NameEmail
|
||||
from pydantic.types import SecretBytes, SecretStr
|
||||
from typing_extensions import Annotated, Doc
|
||||
|
||||
from ._compat import PYDANTIC_V2, UndefinedType, Url, _model_dump
|
||||
|
||||
|
||||
# Taken from Pydantic v1 as is
|
||||
def isoformat(o: Union[datetime.date, datetime.time]) -> str:
|
||||
return o.isoformat()
|
||||
|
||||
|
||||
# Taken from Pydantic v1 as is
|
||||
# TODO: pv2 should this return strings instead?
|
||||
def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
|
||||
"""
|
||||
Encodes a Decimal as int of there's no exponent, otherwise float
|
||||
|
||||
This is useful when we use ConstrainedDecimal to represent Numeric(x,0)
|
||||
where a integer (but not int typed) is used. Encoding this as a float
|
||||
results in failed round-tripping between encode and parse.
|
||||
Our Id type is a prime example of this.
|
||||
|
||||
>>> decimal_encoder(Decimal("1.0"))
|
||||
1.0
|
||||
|
||||
>>> decimal_encoder(Decimal("1"))
|
||||
1
|
||||
"""
|
||||
if dec_value.as_tuple().exponent >= 0: # type: ignore[operator]
|
||||
return int(dec_value)
|
||||
else:
|
||||
return float(dec_value)
|
||||
|
||||
|
||||
ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = {
|
||||
bytes: lambda o: o.decode(),
|
||||
Color: str,
|
||||
datetime.date: isoformat,
|
||||
datetime.datetime: isoformat,
|
||||
datetime.time: isoformat,
|
||||
datetime.timedelta: lambda td: td.total_seconds(),
|
||||
Decimal: decimal_encoder,
|
||||
Enum: lambda o: o.value,
|
||||
frozenset: list,
|
||||
deque: list,
|
||||
GeneratorType: list,
|
||||
IPv4Address: str,
|
||||
IPv4Interface: str,
|
||||
IPv4Network: str,
|
||||
IPv6Address: str,
|
||||
IPv6Interface: str,
|
||||
IPv6Network: str,
|
||||
NameEmail: str,
|
||||
Path: str,
|
||||
Pattern: lambda o: o.pattern,
|
||||
SecretBytes: str,
|
||||
SecretStr: str,
|
||||
set: list,
|
||||
UUID: str,
|
||||
Url: str,
|
||||
AnyUrl: str,
|
||||
}
|
||||
|
||||
|
||||
def generate_encoders_by_class_tuples(
|
||||
type_encoder_map: Dict[Any, Callable[[Any], Any]],
|
||||
) -> Dict[Callable[[Any], Any], Tuple[Any, ...]]:
|
||||
encoders_by_class_tuples: Dict[Callable[[Any], Any], Tuple[Any, ...]] = defaultdict(
|
||||
tuple
|
||||
)
|
||||
for type_, encoder in type_encoder_map.items():
|
||||
encoders_by_class_tuples[encoder] += (type_,)
|
||||
return encoders_by_class_tuples
|
||||
|
||||
|
||||
encoders_by_class_tuples = generate_encoders_by_class_tuples(ENCODERS_BY_TYPE)
|
||||
|
||||
|
||||
def jsonable_encoder(
|
||||
obj: Annotated[
|
||||
Any,
|
||||
Doc(
|
||||
"""
|
||||
The input object to convert to JSON.
|
||||
"""
|
||||
),
|
||||
],
|
||||
include: Annotated[
|
||||
Optional[IncEx],
|
||||
Doc(
|
||||
"""
|
||||
Pydantic's `include` parameter, passed to Pydantic models to set the
|
||||
fields to include.
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
exclude: Annotated[
|
||||
Optional[IncEx],
|
||||
Doc(
|
||||
"""
|
||||
Pydantic's `exclude` parameter, passed to Pydantic models to set the
|
||||
fields to exclude.
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
by_alias: Annotated[
|
||||
bool,
|
||||
Doc(
|
||||
"""
|
||||
Pydantic's `by_alias` parameter, passed to Pydantic models to define if
|
||||
the output should use the alias names (when provided) or the Python
|
||||
attribute names. In an API, if you set an alias, it's probably because you
|
||||
want to use it in the result, so you probably want to leave this set to
|
||||
`True`.
|
||||
"""
|
||||
),
|
||||
] = True,
|
||||
exclude_unset: Annotated[
|
||||
bool,
|
||||
Doc(
|
||||
"""
|
||||
Pydantic's `exclude_unset` parameter, passed to Pydantic models to define
|
||||
if it should exclude from the output the fields that were not explicitly
|
||||
set (and that only had their default values).
|
||||
"""
|
||||
),
|
||||
] = False,
|
||||
exclude_defaults: Annotated[
|
||||
bool,
|
||||
Doc(
|
||||
"""
|
||||
Pydantic's `exclude_defaults` parameter, passed to Pydantic models to define
|
||||
if it should exclude from the output the fields that had the same default
|
||||
value, even when they were explicitly set.
|
||||
"""
|
||||
),
|
||||
] = False,
|
||||
exclude_none: Annotated[
|
||||
bool,
|
||||
Doc(
|
||||
"""
|
||||
Pydantic's `exclude_none` parameter, passed to Pydantic models to define
|
||||
if it should exclude from the output any fields that have a `None` value.
|
||||
"""
|
||||
),
|
||||
] = False,
|
||||
custom_encoder: Annotated[
|
||||
Optional[Dict[Any, Callable[[Any], Any]]],
|
||||
Doc(
|
||||
"""
|
||||
Pydantic's `custom_encoder` parameter, passed to Pydantic models to define
|
||||
a custom encoder.
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
sqlalchemy_safe: Annotated[
|
||||
bool,
|
||||
Doc(
|
||||
"""
|
||||
Exclude from the output any fields that start with the name `_sa`.
|
||||
|
||||
This is mainly a hack for compatibility with SQLAlchemy objects, they
|
||||
store internal SQLAlchemy-specific state in attributes named with `_sa`,
|
||||
and those objects can't (and shouldn't be) serialized to JSON.
|
||||
"""
|
||||
),
|
||||
] = True,
|
||||
) -> Any:
|
||||
"""
|
||||
Convert any object to something that can be encoded in JSON.
|
||||
|
||||
This is used internally by FastAPI to make sure anything you return can be
|
||||
encoded as JSON before it is sent to the client.
|
||||
|
||||
You can also use it yourself, for example to convert objects before saving them
|
||||
in a database that supports only JSON.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for JSON Compatible Encoder](https://fastapi.tiangolo.com/tutorial/encoder/).
|
||||
"""
|
||||
custom_encoder = custom_encoder or {}
|
||||
if custom_encoder:
|
||||
if type(obj) in custom_encoder:
|
||||
return custom_encoder[type(obj)](obj)
|
||||
else:
|
||||
for encoder_type, encoder_instance in custom_encoder.items():
|
||||
if isinstance(obj, encoder_type):
|
||||
return encoder_instance(obj)
|
||||
if include is not None and not isinstance(include, (set, dict)):
|
||||
include = set(include)
|
||||
if exclude is not None and not isinstance(exclude, (set, dict)):
|
||||
exclude = set(exclude)
|
||||
if isinstance(obj, BaseModel):
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
encoders: Dict[Any, Any] = {}
|
||||
if not PYDANTIC_V2:
|
||||
encoders = getattr(obj.__config__, "json_encoders", {}) # type: ignore[attr-defined]
|
||||
if custom_encoder:
|
||||
encoders.update(custom_encoder)
|
||||
obj_dict = _model_dump(
|
||||
obj,
|
||||
mode="json",
|
||||
include=include,
|
||||
exclude=exclude,
|
||||
by_alias=by_alias,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_none=exclude_none,
|
||||
exclude_defaults=exclude_defaults,
|
||||
)
|
||||
if "__root__" in obj_dict:
|
||||
obj_dict = obj_dict["__root__"]
|
||||
return jsonable_encoder(
|
||||
obj_dict,
|
||||
exclude_none=exclude_none,
|
||||
exclude_defaults=exclude_defaults,
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
custom_encoder=encoders,
|
||||
sqlalchemy_safe=sqlalchemy_safe,
|
||||
)
|
||||
if dataclasses.is_dataclass(obj):
|
||||
obj_dict = dataclasses.asdict(obj)
|
||||
return jsonable_encoder(
|
||||
obj_dict,
|
||||
include=include,
|
||||
exclude=exclude,
|
||||
by_alias=by_alias,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_defaults=exclude_defaults,
|
||||
exclude_none=exclude_none,
|
||||
custom_encoder=custom_encoder,
|
||||
sqlalchemy_safe=sqlalchemy_safe,
|
||||
)
|
||||
if isinstance(obj, Enum):
|
||||
return obj.value
|
||||
if isinstance(obj, PurePath):
|
||||
return str(obj)
|
||||
if isinstance(obj, (str, int, float, type(None))):
|
||||
return obj
|
||||
if isinstance(obj, UndefinedType):
|
||||
return None
|
||||
if isinstance(obj, dict):
|
||||
encoded_dict = {}
|
||||
allowed_keys = set(obj.keys())
|
||||
if include is not None:
|
||||
allowed_keys &= set(include)
|
||||
if exclude is not None:
|
||||
allowed_keys -= set(exclude)
|
||||
for key, value in obj.items():
|
||||
if (
|
||||
(
|
||||
not sqlalchemy_safe
|
||||
or (not isinstance(key, str))
|
||||
or (not key.startswith("_sa"))
|
||||
)
|
||||
and (value is not None or not exclude_none)
|
||||
and key in allowed_keys
|
||||
):
|
||||
encoded_key = jsonable_encoder(
|
||||
key,
|
||||
by_alias=by_alias,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_none=exclude_none,
|
||||
custom_encoder=custom_encoder,
|
||||
sqlalchemy_safe=sqlalchemy_safe,
|
||||
)
|
||||
encoded_value = jsonable_encoder(
|
||||
value,
|
||||
by_alias=by_alias,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_none=exclude_none,
|
||||
custom_encoder=custom_encoder,
|
||||
sqlalchemy_safe=sqlalchemy_safe,
|
||||
)
|
||||
encoded_dict[encoded_key] = encoded_value
|
||||
return encoded_dict
|
||||
if isinstance(obj, (list, set, frozenset, GeneratorType, tuple, deque)):
|
||||
encoded_list = []
|
||||
for item in obj:
|
||||
encoded_list.append(
|
||||
jsonable_encoder(
|
||||
item,
|
||||
include=include,
|
||||
exclude=exclude,
|
||||
by_alias=by_alias,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_defaults=exclude_defaults,
|
||||
exclude_none=exclude_none,
|
||||
custom_encoder=custom_encoder,
|
||||
sqlalchemy_safe=sqlalchemy_safe,
|
||||
)
|
||||
)
|
||||
return encoded_list
|
||||
|
||||
if type(obj) in ENCODERS_BY_TYPE:
|
||||
return ENCODERS_BY_TYPE[type(obj)](obj)
|
||||
for encoder, classes_tuple in encoders_by_class_tuples.items():
|
||||
if isinstance(obj, classes_tuple):
|
||||
return encoder(obj)
|
||||
|
||||
try:
|
||||
data = dict(obj)
|
||||
except Exception as e:
|
||||
errors: List[Exception] = []
|
||||
errors.append(e)
|
||||
try:
|
||||
data = vars(obj)
|
||||
except Exception as e:
|
||||
errors.append(e)
|
||||
raise ValueError(errors) from e
|
||||
return jsonable_encoder(
|
||||
data,
|
||||
include=include,
|
||||
exclude=exclude,
|
||||
by_alias=by_alias,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_defaults=exclude_defaults,
|
||||
exclude_none=exclude_none,
|
||||
custom_encoder=custom_encoder,
|
||||
sqlalchemy_safe=sqlalchemy_safe,
|
||||
)
|
@ -0,0 +1,34 @@
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
|
||||
from fastapi.utils import is_body_allowed_for_status_code
|
||||
from fastapi.websockets import WebSocket
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse, Response
|
||||
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY, WS_1008_POLICY_VIOLATION
|
||||
|
||||
|
||||
async def http_exception_handler(request: Request, exc: HTTPException) -> Response:
|
||||
headers = getattr(exc, "headers", None)
|
||||
if not is_body_allowed_for_status_code(exc.status_code):
|
||||
return Response(status_code=exc.status_code, headers=headers)
|
||||
return JSONResponse(
|
||||
{"detail": exc.detail}, status_code=exc.status_code, headers=headers
|
||||
)
|
||||
|
||||
|
||||
async def request_validation_exception_handler(
|
||||
request: Request, exc: RequestValidationError
|
||||
) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
status_code=HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
content={"detail": jsonable_encoder(exc.errors())},
|
||||
)
|
||||
|
||||
|
||||
async def websocket_request_validation_exception_handler(
|
||||
websocket: WebSocket, exc: WebSocketRequestValidationError
|
||||
) -> None:
|
||||
await websocket.close(
|
||||
code=WS_1008_POLICY_VIOLATION, reason=jsonable_encoder(exc.errors())
|
||||
)
|
176
venv/lib/python3.11/site-packages/fastapi/exceptions.py
Normal file
176
venv/lib/python3.11/site-packages/fastapi/exceptions.py
Normal file
@ -0,0 +1,176 @@
|
||||
from typing import Any, Dict, Optional, Sequence, Type, Union
|
||||
|
||||
from pydantic import BaseModel, create_model
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
from starlette.exceptions import WebSocketException as StarletteWebSocketException
|
||||
from typing_extensions import Annotated, Doc
|
||||
|
||||
|
||||
class HTTPException(StarletteHTTPException):
|
||||
"""
|
||||
An HTTP exception you can raise in your own code to show errors to the client.
|
||||
|
||||
This is for client errors, invalid authentication, invalid data, etc. Not for server
|
||||
errors in your code.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for Handling Errors](https://fastapi.tiangolo.com/tutorial/handling-errors/).
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI, HTTPException
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
items = {"foo": "The Foo Wrestlers"}
|
||||
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
async def read_item(item_id: str):
|
||||
if item_id not in items:
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
return {"item": items[item_id]}
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
status_code: Annotated[
|
||||
int,
|
||||
Doc(
|
||||
"""
|
||||
HTTP status code to send to the client.
|
||||
"""
|
||||
),
|
||||
],
|
||||
detail: Annotated[
|
||||
Any,
|
||||
Doc(
|
||||
"""
|
||||
Any data to be sent to the client in the `detail` key of the JSON
|
||||
response.
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
headers: Annotated[
|
||||
Optional[Dict[str, str]],
|
||||
Doc(
|
||||
"""
|
||||
Any headers to send to the client in the response.
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
) -> None:
|
||||
super().__init__(status_code=status_code, detail=detail, headers=headers)
|
||||
|
||||
|
||||
class WebSocketException(StarletteWebSocketException):
|
||||
"""
|
||||
A WebSocket exception you can raise in your own code to show errors to the client.
|
||||
|
||||
This is for client errors, invalid authentication, invalid data, etc. Not for server
|
||||
errors in your code.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for WebSockets](https://fastapi.tiangolo.com/advanced/websockets/).
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import (
|
||||
Cookie,
|
||||
FastAPI,
|
||||
WebSocket,
|
||||
WebSocketException,
|
||||
status,
|
||||
)
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.websocket("/items/{item_id}/ws")
|
||||
async def websocket_endpoint(
|
||||
*,
|
||||
websocket: WebSocket,
|
||||
session: Annotated[str | None, Cookie()] = None,
|
||||
item_id: str,
|
||||
):
|
||||
if session is None:
|
||||
raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION)
|
||||
await websocket.accept()
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
await websocket.send_text(f"Session cookie is: {session}")
|
||||
await websocket.send_text(f"Message text was: {data}, for item ID: {item_id}")
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
code: Annotated[
|
||||
int,
|
||||
Doc(
|
||||
"""
|
||||
A closing code from the
|
||||
[valid codes defined in the specification](https://datatracker.ietf.org/doc/html/rfc6455#section-7.4.1).
|
||||
"""
|
||||
),
|
||||
],
|
||||
reason: Annotated[
|
||||
Union[str, None],
|
||||
Doc(
|
||||
"""
|
||||
The reason to close the WebSocket connection.
|
||||
|
||||
It is UTF-8-encoded data. The interpretation of the reason is up to the
|
||||
application, it is not specified by the WebSocket specification.
|
||||
|
||||
It could contain text that could be human-readable or interpretable
|
||||
by the client code, etc.
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
) -> None:
|
||||
super().__init__(code=code, reason=reason)
|
||||
|
||||
|
||||
RequestErrorModel: Type[BaseModel] = create_model("Request")
|
||||
WebSocketErrorModel: Type[BaseModel] = create_model("WebSocket")
|
||||
|
||||
|
||||
class FastAPIError(RuntimeError):
|
||||
"""
|
||||
A generic, FastAPI-specific error.
|
||||
"""
|
||||
|
||||
|
||||
class ValidationException(Exception):
|
||||
def __init__(self, errors: Sequence[Any]) -> None:
|
||||
self._errors = errors
|
||||
|
||||
def errors(self) -> Sequence[Any]:
|
||||
return self._errors
|
||||
|
||||
|
||||
class RequestValidationError(ValidationException):
|
||||
def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None:
|
||||
super().__init__(errors)
|
||||
self.body = body
|
||||
|
||||
|
||||
class WebSocketRequestValidationError(ValidationException):
|
||||
pass
|
||||
|
||||
|
||||
class ResponseValidationError(ValidationException):
|
||||
def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None:
|
||||
super().__init__(errors)
|
||||
self.body = body
|
||||
|
||||
def __str__(self) -> str:
|
||||
message = f"{len(self._errors)} validation errors:\n"
|
||||
for err in self._errors:
|
||||
message += f" {err}\n"
|
||||
return message
|
3
venv/lib/python3.11/site-packages/fastapi/logger.py
Normal file
3
venv/lib/python3.11/site-packages/fastapi/logger.py
Normal file
@ -0,0 +1,3 @@
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("fastapi")
|
@ -0,0 +1 @@
|
||||
from starlette.middleware import Middleware as Middleware
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1 @@
|
||||
from starlette.middleware.cors import CORSMiddleware as CORSMiddleware # noqa
|
@ -0,0 +1 @@
|
||||
from starlette.middleware.gzip import GZipMiddleware as GZipMiddleware # noqa
|
@ -0,0 +1,3 @@
|
||||
from starlette.middleware.httpsredirect import ( # noqa
|
||||
HTTPSRedirectMiddleware as HTTPSRedirectMiddleware,
|
||||
)
|
@ -0,0 +1,3 @@
|
||||
from starlette.middleware.trustedhost import ( # noqa
|
||||
TrustedHostMiddleware as TrustedHostMiddleware,
|
||||
)
|
@ -0,0 +1 @@
|
||||
from starlette.middleware.wsgi import WSGIMiddleware as WSGIMiddleware # noqa
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,3 @@
|
||||
METHODS_WITH_BODY = {"GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"}
|
||||
REF_PREFIX = "#/components/schemas/"
|
||||
REF_TEMPLATE = "#/components/schemas/{model}"
|
344
venv/lib/python3.11/site-packages/fastapi/openapi/docs.py
Normal file
344
venv/lib/python3.11/site-packages/fastapi/openapi/docs.py
Normal file
@ -0,0 +1,344 @@
|
||||
import json
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from starlette.responses import HTMLResponse
|
||||
from typing_extensions import Annotated, Doc
|
||||
|
||||
swagger_ui_default_parameters: Annotated[
|
||||
Dict[str, Any],
|
||||
Doc(
|
||||
"""
|
||||
Default configurations for Swagger UI.
|
||||
|
||||
You can use it as a template to add any other configurations needed.
|
||||
"""
|
||||
),
|
||||
] = {
|
||||
"dom_id": "#swagger-ui",
|
||||
"layout": "BaseLayout",
|
||||
"deepLinking": True,
|
||||
"showExtensions": True,
|
||||
"showCommonExtensions": True,
|
||||
}
|
||||
|
||||
|
||||
def get_swagger_ui_html(
|
||||
*,
|
||||
openapi_url: Annotated[
|
||||
str,
|
||||
Doc(
|
||||
"""
|
||||
The OpenAPI URL that Swagger UI should load and use.
|
||||
|
||||
This is normally done automatically by FastAPI using the default URL
|
||||
`/openapi.json`.
|
||||
"""
|
||||
),
|
||||
],
|
||||
title: Annotated[
|
||||
str,
|
||||
Doc(
|
||||
"""
|
||||
The HTML `<title>` content, normally shown in the browser tab.
|
||||
"""
|
||||
),
|
||||
],
|
||||
swagger_js_url: Annotated[
|
||||
str,
|
||||
Doc(
|
||||
"""
|
||||
The URL to use to load the Swagger UI JavaScript.
|
||||
|
||||
It is normally set to a CDN URL.
|
||||
"""
|
||||
),
|
||||
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js",
|
||||
swagger_css_url: Annotated[
|
||||
str,
|
||||
Doc(
|
||||
"""
|
||||
The URL to use to load the Swagger UI CSS.
|
||||
|
||||
It is normally set to a CDN URL.
|
||||
"""
|
||||
),
|
||||
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css",
|
||||
swagger_favicon_url: Annotated[
|
||||
str,
|
||||
Doc(
|
||||
"""
|
||||
The URL of the favicon to use. It is normally shown in the browser tab.
|
||||
"""
|
||||
),
|
||||
] = "https://fastapi.tiangolo.com/img/favicon.png",
|
||||
oauth2_redirect_url: Annotated[
|
||||
Optional[str],
|
||||
Doc(
|
||||
"""
|
||||
The OAuth2 redirect URL, it is normally automatically handled by FastAPI.
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
init_oauth: Annotated[
|
||||
Optional[Dict[str, Any]],
|
||||
Doc(
|
||||
"""
|
||||
A dictionary with Swagger UI OAuth2 initialization configurations.
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
swagger_ui_parameters: Annotated[
|
||||
Optional[Dict[str, Any]],
|
||||
Doc(
|
||||
"""
|
||||
Configuration parameters for Swagger UI.
|
||||
|
||||
It defaults to [swagger_ui_default_parameters][fastapi.openapi.docs.swagger_ui_default_parameters].
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
) -> HTMLResponse:
|
||||
"""
|
||||
Generate and return the HTML that loads Swagger UI for the interactive
|
||||
API docs (normally served at `/docs`).
|
||||
|
||||
You would only call this function yourself if you needed to override some parts,
|
||||
for example the URLs to use to load Swagger UI's JavaScript and CSS.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for Configure Swagger UI](https://fastapi.tiangolo.com/how-to/configure-swagger-ui/)
|
||||
and the [FastAPI docs for Custom Docs UI Static Assets (Self-Hosting)](https://fastapi.tiangolo.com/how-to/custom-docs-ui-assets/).
|
||||
"""
|
||||
current_swagger_ui_parameters = swagger_ui_default_parameters.copy()
|
||||
if swagger_ui_parameters:
|
||||
current_swagger_ui_parameters.update(swagger_ui_parameters)
|
||||
|
||||
html = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link type="text/css" rel="stylesheet" href="{swagger_css_url}">
|
||||
<link rel="shortcut icon" href="{swagger_favicon_url}">
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="swagger-ui">
|
||||
</div>
|
||||
<script src="{swagger_js_url}"></script>
|
||||
<!-- `SwaggerUIBundle` is now available on the page -->
|
||||
<script>
|
||||
const ui = SwaggerUIBundle({{
|
||||
url: '{openapi_url}',
|
||||
"""
|
||||
|
||||
for key, value in current_swagger_ui_parameters.items():
|
||||
html += f"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\n"
|
||||
|
||||
if oauth2_redirect_url:
|
||||
html += f"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',"
|
||||
|
||||
html += """
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIBundle.SwaggerUIStandalonePreset
|
||||
],
|
||||
})"""
|
||||
|
||||
if init_oauth:
|
||||
html += f"""
|
||||
ui.initOAuth({json.dumps(jsonable_encoder(init_oauth))})
|
||||
"""
|
||||
|
||||
html += """
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return HTMLResponse(html)
|
||||
|
||||
|
||||
def get_redoc_html(
|
||||
*,
|
||||
openapi_url: Annotated[
|
||||
str,
|
||||
Doc(
|
||||
"""
|
||||
The OpenAPI URL that ReDoc should load and use.
|
||||
|
||||
This is normally done automatically by FastAPI using the default URL
|
||||
`/openapi.json`.
|
||||
"""
|
||||
),
|
||||
],
|
||||
title: Annotated[
|
||||
str,
|
||||
Doc(
|
||||
"""
|
||||
The HTML `<title>` content, normally shown in the browser tab.
|
||||
"""
|
||||
),
|
||||
],
|
||||
redoc_js_url: Annotated[
|
||||
str,
|
||||
Doc(
|
||||
"""
|
||||
The URL to use to load the ReDoc JavaScript.
|
||||
|
||||
It is normally set to a CDN URL.
|
||||
"""
|
||||
),
|
||||
] = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js",
|
||||
redoc_favicon_url: Annotated[
|
||||
str,
|
||||
Doc(
|
||||
"""
|
||||
The URL of the favicon to use. It is normally shown in the browser tab.
|
||||
"""
|
||||
),
|
||||
] = "https://fastapi.tiangolo.com/img/favicon.png",
|
||||
with_google_fonts: Annotated[
|
||||
bool,
|
||||
Doc(
|
||||
"""
|
||||
Load and use Google Fonts.
|
||||
"""
|
||||
),
|
||||
] = True,
|
||||
) -> HTMLResponse:
|
||||
"""
|
||||
Generate and return the HTML response that loads ReDoc for the alternative
|
||||
API docs (normally served at `/redoc`).
|
||||
|
||||
You would only call this function yourself if you needed to override some parts,
|
||||
for example the URLs to use to load ReDoc's JavaScript and CSS.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for Custom Docs UI Static Assets (Self-Hosting)](https://fastapi.tiangolo.com/how-to/custom-docs-ui-assets/).
|
||||
"""
|
||||
html = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{title}</title>
|
||||
<!-- needed for adaptive design -->
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
"""
|
||||
if with_google_fonts:
|
||||
html += """
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
||||
"""
|
||||
html += f"""
|
||||
<link rel="shortcut icon" href="{redoc_favicon_url}">
|
||||
<!--
|
||||
ReDoc doesn't change outer page styles
|
||||
-->
|
||||
<style>
|
||||
body {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
ReDoc requires Javascript to function. Please enable it to browse the documentation.
|
||||
</noscript>
|
||||
<redoc spec-url="{openapi_url}"></redoc>
|
||||
<script src="{redoc_js_url}"> </script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return HTMLResponse(html)
|
||||
|
||||
|
||||
def get_swagger_ui_oauth2_redirect_html() -> HTMLResponse:
|
||||
"""
|
||||
Generate the HTML response with the OAuth2 redirection for Swagger UI.
|
||||
|
||||
You normally don't need to use or change this.
|
||||
"""
|
||||
# copied from https://github.com/swagger-api/swagger-ui/blob/v4.14.0/dist/oauth2-redirect.html
|
||||
html = """
|
||||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<title>Swagger UI: OAuth2 Redirect</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
'use strict';
|
||||
function run () {
|
||||
var oauth2 = window.opener.swaggerUIRedirectOauth2;
|
||||
var sentState = oauth2.state;
|
||||
var redirectUrl = oauth2.redirectUrl;
|
||||
var isValid, qp, arr;
|
||||
|
||||
if (/code|token|error/.test(window.location.hash)) {
|
||||
qp = window.location.hash.substring(1).replace('?', '&');
|
||||
} else {
|
||||
qp = location.search.substring(1);
|
||||
}
|
||||
|
||||
arr = qp.split("&");
|
||||
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
|
||||
qp = qp ? JSON.parse('{' + arr.join() + '}',
|
||||
function (key, value) {
|
||||
return key === "" ? value : decodeURIComponent(value);
|
||||
}
|
||||
) : {};
|
||||
|
||||
isValid = qp.state === sentState;
|
||||
|
||||
if ((
|
||||
oauth2.auth.schema.get("flow") === "accessCode" ||
|
||||
oauth2.auth.schema.get("flow") === "authorizationCode" ||
|
||||
oauth2.auth.schema.get("flow") === "authorization_code"
|
||||
) && !oauth2.auth.code) {
|
||||
if (!isValid) {
|
||||
oauth2.errCb({
|
||||
authId: oauth2.auth.name,
|
||||
source: "auth",
|
||||
level: "warning",
|
||||
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
|
||||
});
|
||||
}
|
||||
|
||||
if (qp.code) {
|
||||
delete oauth2.state;
|
||||
oauth2.auth.code = qp.code;
|
||||
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
|
||||
} else {
|
||||
let oauthErrorMsg;
|
||||
if (qp.error) {
|
||||
oauthErrorMsg = "["+qp.error+"]: " +
|
||||
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
|
||||
(qp.error_uri ? "More info: "+qp.error_uri : "");
|
||||
}
|
||||
|
||||
oauth2.errCb({
|
||||
authId: oauth2.auth.name,
|
||||
source: "auth",
|
||||
level: "error",
|
||||
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
|
||||
});
|
||||
}
|
||||
} else {
|
||||
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
|
||||
}
|
||||
window.close();
|
||||
}
|
||||
|
||||
if (document.readyState !== 'loading') {
|
||||
run();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
run();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return HTMLResponse(content=html)
|
445
venv/lib/python3.11/site-packages/fastapi/openapi/models.py
Normal file
445
venv/lib/python3.11/site-packages/fastapi/openapi/models.py
Normal file
@ -0,0 +1,445 @@
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Type, Union
|
||||
|
||||
from fastapi._compat import (
|
||||
PYDANTIC_V2,
|
||||
CoreSchema,
|
||||
GetJsonSchemaHandler,
|
||||
JsonSchemaValue,
|
||||
_model_rebuild,
|
||||
with_info_plain_validator_function,
|
||||
)
|
||||
from fastapi.logger import logger
|
||||
from pydantic import AnyUrl, BaseModel, Field
|
||||
from typing_extensions import Annotated, Literal, TypedDict
|
||||
from typing_extensions import deprecated as typing_deprecated
|
||||
|
||||
try:
|
||||
import email_validator
|
||||
|
||||
assert email_validator # make autoflake ignore the unused import
|
||||
from pydantic import EmailStr
|
||||
except ImportError: # pragma: no cover
|
||||
|
||||
class EmailStr(str): # type: ignore
|
||||
@classmethod
|
||||
def __get_validators__(cls) -> Iterable[Callable[..., Any]]:
|
||||
yield cls.validate
|
||||
|
||||
@classmethod
|
||||
def validate(cls, v: Any) -> str:
|
||||
logger.warning(
|
||||
"email-validator not installed, email fields will be treated as str.\n"
|
||||
"To install, run: pip install email-validator"
|
||||
)
|
||||
return str(v)
|
||||
|
||||
@classmethod
|
||||
def _validate(cls, __input_value: Any, _: Any) -> str:
|
||||
logger.warning(
|
||||
"email-validator not installed, email fields will be treated as str.\n"
|
||||
"To install, run: pip install email-validator"
|
||||
)
|
||||
return str(__input_value)
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_json_schema__(
|
||||
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
|
||||
) -> JsonSchemaValue:
|
||||
return {"type": "string", "format": "email"}
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_core_schema__(
|
||||
cls, source: Type[Any], handler: Callable[[Any], CoreSchema]
|
||||
) -> CoreSchema:
|
||||
return with_info_plain_validator_function(cls._validate)
|
||||
|
||||
|
||||
class BaseModelWithConfig(BaseModel):
|
||||
if PYDANTIC_V2:
|
||||
model_config = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class Contact(BaseModelWithConfig):
|
||||
name: Optional[str] = None
|
||||
url: Optional[AnyUrl] = None
|
||||
email: Optional[EmailStr] = None
|
||||
|
||||
|
||||
class License(BaseModelWithConfig):
|
||||
name: str
|
||||
identifier: Optional[str] = None
|
||||
url: Optional[AnyUrl] = None
|
||||
|
||||
|
||||
class Info(BaseModelWithConfig):
|
||||
title: str
|
||||
summary: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
termsOfService: Optional[str] = None
|
||||
contact: Optional[Contact] = None
|
||||
license: Optional[License] = None
|
||||
version: str
|
||||
|
||||
|
||||
class ServerVariable(BaseModelWithConfig):
|
||||
enum: Annotated[Optional[List[str]], Field(min_length=1)] = None
|
||||
default: str
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class Server(BaseModelWithConfig):
|
||||
url: Union[AnyUrl, str]
|
||||
description: Optional[str] = None
|
||||
variables: Optional[Dict[str, ServerVariable]] = None
|
||||
|
||||
|
||||
class Reference(BaseModel):
|
||||
ref: str = Field(alias="$ref")
|
||||
|
||||
|
||||
class Discriminator(BaseModel):
|
||||
propertyName: str
|
||||
mapping: Optional[Dict[str, str]] = None
|
||||
|
||||
|
||||
class XML(BaseModelWithConfig):
|
||||
name: Optional[str] = None
|
||||
namespace: Optional[str] = None
|
||||
prefix: Optional[str] = None
|
||||
attribute: Optional[bool] = None
|
||||
wrapped: Optional[bool] = None
|
||||
|
||||
|
||||
class ExternalDocumentation(BaseModelWithConfig):
|
||||
description: Optional[str] = None
|
||||
url: AnyUrl
|
||||
|
||||
|
||||
class Schema(BaseModelWithConfig):
|
||||
# Ref: JSON Schema 2020-12: https://json-schema.org/draft/2020-12/json-schema-core.html#name-the-json-schema-core-vocabu
|
||||
# Core Vocabulary
|
||||
schema_: Optional[str] = Field(default=None, alias="$schema")
|
||||
vocabulary: Optional[str] = Field(default=None, alias="$vocabulary")
|
||||
id: Optional[str] = Field(default=None, alias="$id")
|
||||
anchor: Optional[str] = Field(default=None, alias="$anchor")
|
||||
dynamicAnchor: Optional[str] = Field(default=None, alias="$dynamicAnchor")
|
||||
ref: Optional[str] = Field(default=None, alias="$ref")
|
||||
dynamicRef: Optional[str] = Field(default=None, alias="$dynamicRef")
|
||||
defs: Optional[Dict[str, "SchemaOrBool"]] = Field(default=None, alias="$defs")
|
||||
comment: Optional[str] = Field(default=None, alias="$comment")
|
||||
# Ref: JSON Schema 2020-12: https://json-schema.org/draft/2020-12/json-schema-core.html#name-a-vocabulary-for-applying-s
|
||||
# A Vocabulary for Applying Subschemas
|
||||
allOf: Optional[List["SchemaOrBool"]] = None
|
||||
anyOf: Optional[List["SchemaOrBool"]] = None
|
||||
oneOf: Optional[List["SchemaOrBool"]] = None
|
||||
not_: Optional["SchemaOrBool"] = Field(default=None, alias="not")
|
||||
if_: Optional["SchemaOrBool"] = Field(default=None, alias="if")
|
||||
then: Optional["SchemaOrBool"] = None
|
||||
else_: Optional["SchemaOrBool"] = Field(default=None, alias="else")
|
||||
dependentSchemas: Optional[Dict[str, "SchemaOrBool"]] = None
|
||||
prefixItems: Optional[List["SchemaOrBool"]] = None
|
||||
# TODO: uncomment and remove below when deprecating Pydantic v1
|
||||
# It generales a list of schemas for tuples, before prefixItems was available
|
||||
# items: Optional["SchemaOrBool"] = None
|
||||
items: Optional[Union["SchemaOrBool", List["SchemaOrBool"]]] = None
|
||||
contains: Optional["SchemaOrBool"] = None
|
||||
properties: Optional[Dict[str, "SchemaOrBool"]] = None
|
||||
patternProperties: Optional[Dict[str, "SchemaOrBool"]] = None
|
||||
additionalProperties: Optional["SchemaOrBool"] = None
|
||||
propertyNames: Optional["SchemaOrBool"] = None
|
||||
unevaluatedItems: Optional["SchemaOrBool"] = None
|
||||
unevaluatedProperties: Optional["SchemaOrBool"] = None
|
||||
# Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-structural
|
||||
# A Vocabulary for Structural Validation
|
||||
type: Optional[str] = None
|
||||
enum: Optional[List[Any]] = None
|
||||
const: Optional[Any] = None
|
||||
multipleOf: Optional[float] = Field(default=None, gt=0)
|
||||
maximum: Optional[float] = None
|
||||
exclusiveMaximum: Optional[float] = None
|
||||
minimum: Optional[float] = None
|
||||
exclusiveMinimum: Optional[float] = None
|
||||
maxLength: Optional[int] = Field(default=None, ge=0)
|
||||
minLength: Optional[int] = Field(default=None, ge=0)
|
||||
pattern: Optional[str] = None
|
||||
maxItems: Optional[int] = Field(default=None, ge=0)
|
||||
minItems: Optional[int] = Field(default=None, ge=0)
|
||||
uniqueItems: Optional[bool] = None
|
||||
maxContains: Optional[int] = Field(default=None, ge=0)
|
||||
minContains: Optional[int] = Field(default=None, ge=0)
|
||||
maxProperties: Optional[int] = Field(default=None, ge=0)
|
||||
minProperties: Optional[int] = Field(default=None, ge=0)
|
||||
required: Optional[List[str]] = None
|
||||
dependentRequired: Optional[Dict[str, Set[str]]] = None
|
||||
# Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-vocabularies-for-semantic-c
|
||||
# Vocabularies for Semantic Content With "format"
|
||||
format: Optional[str] = None
|
||||
# Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-the-conten
|
||||
# A Vocabulary for the Contents of String-Encoded Data
|
||||
contentEncoding: Optional[str] = None
|
||||
contentMediaType: Optional[str] = None
|
||||
contentSchema: Optional["SchemaOrBool"] = None
|
||||
# Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-basic-meta
|
||||
# A Vocabulary for Basic Meta-Data Annotations
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
default: Optional[Any] = None
|
||||
deprecated: Optional[bool] = None
|
||||
readOnly: Optional[bool] = None
|
||||
writeOnly: Optional[bool] = None
|
||||
examples: Optional[List[Any]] = None
|
||||
# Ref: OpenAPI 3.1.0: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#schema-object
|
||||
# Schema Object
|
||||
discriminator: Optional[Discriminator] = None
|
||||
xml: Optional[XML] = None
|
||||
externalDocs: Optional[ExternalDocumentation] = None
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
typing_deprecated(
|
||||
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
|
||||
"although still supported. Use examples instead."
|
||||
),
|
||||
] = None
|
||||
|
||||
|
||||
# Ref: https://json-schema.org/draft/2020-12/json-schema-core.html#name-json-schema-documents
|
||||
# A JSON Schema MUST be an object or a boolean.
|
||||
SchemaOrBool = Union[Schema, bool]
|
||||
|
||||
|
||||
class Example(TypedDict, total=False):
|
||||
summary: Optional[str]
|
||||
description: Optional[str]
|
||||
value: Optional[Any]
|
||||
externalValue: Optional[AnyUrl]
|
||||
|
||||
if PYDANTIC_V2: # type: ignore [misc]
|
||||
__pydantic_config__ = {"extra": "allow"}
|
||||
|
||||
else:
|
||||
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class ParameterInType(Enum):
|
||||
query = "query"
|
||||
header = "header"
|
||||
path = "path"
|
||||
cookie = "cookie"
|
||||
|
||||
|
||||
class Encoding(BaseModelWithConfig):
|
||||
contentType: Optional[str] = None
|
||||
headers: Optional[Dict[str, Union["Header", Reference]]] = None
|
||||
style: Optional[str] = None
|
||||
explode: Optional[bool] = None
|
||||
allowReserved: Optional[bool] = None
|
||||
|
||||
|
||||
class MediaType(BaseModelWithConfig):
|
||||
schema_: Optional[Union[Schema, Reference]] = Field(default=None, alias="schema")
|
||||
example: Optional[Any] = None
|
||||
examples: Optional[Dict[str, Union[Example, Reference]]] = None
|
||||
encoding: Optional[Dict[str, Encoding]] = None
|
||||
|
||||
|
||||
class ParameterBase(BaseModelWithConfig):
|
||||
description: Optional[str] = None
|
||||
required: Optional[bool] = None
|
||||
deprecated: Optional[bool] = None
|
||||
# Serialization rules for simple scenarios
|
||||
style: Optional[str] = None
|
||||
explode: Optional[bool] = None
|
||||
allowReserved: Optional[bool] = None
|
||||
schema_: Optional[Union[Schema, Reference]] = Field(default=None, alias="schema")
|
||||
example: Optional[Any] = None
|
||||
examples: Optional[Dict[str, Union[Example, Reference]]] = None
|
||||
# Serialization rules for more complex scenarios
|
||||
content: Optional[Dict[str, MediaType]] = None
|
||||
|
||||
|
||||
class Parameter(ParameterBase):
|
||||
name: str
|
||||
in_: ParameterInType = Field(alias="in")
|
||||
|
||||
|
||||
class Header(ParameterBase):
|
||||
pass
|
||||
|
||||
|
||||
class RequestBody(BaseModelWithConfig):
|
||||
description: Optional[str] = None
|
||||
content: Dict[str, MediaType]
|
||||
required: Optional[bool] = None
|
||||
|
||||
|
||||
class Link(BaseModelWithConfig):
|
||||
operationRef: Optional[str] = None
|
||||
operationId: Optional[str] = None
|
||||
parameters: Optional[Dict[str, Union[Any, str]]] = None
|
||||
requestBody: Optional[Union[Any, str]] = None
|
||||
description: Optional[str] = None
|
||||
server: Optional[Server] = None
|
||||
|
||||
|
||||
class Response(BaseModelWithConfig):
|
||||
description: str
|
||||
headers: Optional[Dict[str, Union[Header, Reference]]] = None
|
||||
content: Optional[Dict[str, MediaType]] = None
|
||||
links: Optional[Dict[str, Union[Link, Reference]]] = None
|
||||
|
||||
|
||||
class Operation(BaseModelWithConfig):
|
||||
tags: Optional[List[str]] = None
|
||||
summary: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
externalDocs: Optional[ExternalDocumentation] = None
|
||||
operationId: Optional[str] = None
|
||||
parameters: Optional[List[Union[Parameter, Reference]]] = None
|
||||
requestBody: Optional[Union[RequestBody, Reference]] = None
|
||||
# Using Any for Specification Extensions
|
||||
responses: Optional[Dict[str, Union[Response, Any]]] = None
|
||||
callbacks: Optional[Dict[str, Union[Dict[str, "PathItem"], Reference]]] = None
|
||||
deprecated: Optional[bool] = None
|
||||
security: Optional[List[Dict[str, List[str]]]] = None
|
||||
servers: Optional[List[Server]] = None
|
||||
|
||||
|
||||
class PathItem(BaseModelWithConfig):
|
||||
ref: Optional[str] = Field(default=None, alias="$ref")
|
||||
summary: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
get: Optional[Operation] = None
|
||||
put: Optional[Operation] = None
|
||||
post: Optional[Operation] = None
|
||||
delete: Optional[Operation] = None
|
||||
options: Optional[Operation] = None
|
||||
head: Optional[Operation] = None
|
||||
patch: Optional[Operation] = None
|
||||
trace: Optional[Operation] = None
|
||||
servers: Optional[List[Server]] = None
|
||||
parameters: Optional[List[Union[Parameter, Reference]]] = None
|
||||
|
||||
|
||||
class SecuritySchemeType(Enum):
|
||||
apiKey = "apiKey"
|
||||
http = "http"
|
||||
oauth2 = "oauth2"
|
||||
openIdConnect = "openIdConnect"
|
||||
|
||||
|
||||
class SecurityBase(BaseModelWithConfig):
|
||||
type_: SecuritySchemeType = Field(alias="type")
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class APIKeyIn(Enum):
|
||||
query = "query"
|
||||
header = "header"
|
||||
cookie = "cookie"
|
||||
|
||||
|
||||
class APIKey(SecurityBase):
|
||||
type_: SecuritySchemeType = Field(default=SecuritySchemeType.apiKey, alias="type")
|
||||
in_: APIKeyIn = Field(alias="in")
|
||||
name: str
|
||||
|
||||
|
||||
class HTTPBase(SecurityBase):
|
||||
type_: SecuritySchemeType = Field(default=SecuritySchemeType.http, alias="type")
|
||||
scheme: str
|
||||
|
||||
|
||||
class HTTPBearer(HTTPBase):
|
||||
scheme: Literal["bearer"] = "bearer"
|
||||
bearerFormat: Optional[str] = None
|
||||
|
||||
|
||||
class OAuthFlow(BaseModelWithConfig):
|
||||
refreshUrl: Optional[str] = None
|
||||
scopes: Dict[str, str] = {}
|
||||
|
||||
|
||||
class OAuthFlowImplicit(OAuthFlow):
|
||||
authorizationUrl: str
|
||||
|
||||
|
||||
class OAuthFlowPassword(OAuthFlow):
|
||||
tokenUrl: str
|
||||
|
||||
|
||||
class OAuthFlowClientCredentials(OAuthFlow):
|
||||
tokenUrl: str
|
||||
|
||||
|
||||
class OAuthFlowAuthorizationCode(OAuthFlow):
|
||||
authorizationUrl: str
|
||||
tokenUrl: str
|
||||
|
||||
|
||||
class OAuthFlows(BaseModelWithConfig):
|
||||
implicit: Optional[OAuthFlowImplicit] = None
|
||||
password: Optional[OAuthFlowPassword] = None
|
||||
clientCredentials: Optional[OAuthFlowClientCredentials] = None
|
||||
authorizationCode: Optional[OAuthFlowAuthorizationCode] = None
|
||||
|
||||
|
||||
class OAuth2(SecurityBase):
|
||||
type_: SecuritySchemeType = Field(default=SecuritySchemeType.oauth2, alias="type")
|
||||
flows: OAuthFlows
|
||||
|
||||
|
||||
class OpenIdConnect(SecurityBase):
|
||||
type_: SecuritySchemeType = Field(
|
||||
default=SecuritySchemeType.openIdConnect, alias="type"
|
||||
)
|
||||
openIdConnectUrl: str
|
||||
|
||||
|
||||
SecurityScheme = Union[APIKey, HTTPBase, OAuth2, OpenIdConnect, HTTPBearer]
|
||||
|
||||
|
||||
class Components(BaseModelWithConfig):
|
||||
schemas: Optional[Dict[str, Union[Schema, Reference]]] = None
|
||||
responses: Optional[Dict[str, Union[Response, Reference]]] = None
|
||||
parameters: Optional[Dict[str, Union[Parameter, Reference]]] = None
|
||||
examples: Optional[Dict[str, Union[Example, Reference]]] = None
|
||||
requestBodies: Optional[Dict[str, Union[RequestBody, Reference]]] = None
|
||||
headers: Optional[Dict[str, Union[Header, Reference]]] = None
|
||||
securitySchemes: Optional[Dict[str, Union[SecurityScheme, Reference]]] = None
|
||||
links: Optional[Dict[str, Union[Link, Reference]]] = None
|
||||
# Using Any for Specification Extensions
|
||||
callbacks: Optional[Dict[str, Union[Dict[str, PathItem], Reference, Any]]] = None
|
||||
pathItems: Optional[Dict[str, Union[PathItem, Reference]]] = None
|
||||
|
||||
|
||||
class Tag(BaseModelWithConfig):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
externalDocs: Optional[ExternalDocumentation] = None
|
||||
|
||||
|
||||
class OpenAPI(BaseModelWithConfig):
|
||||
openapi: str
|
||||
info: Info
|
||||
jsonSchemaDialect: Optional[str] = None
|
||||
servers: Optional[List[Server]] = None
|
||||
# Using Any for Specification Extensions
|
||||
paths: Optional[Dict[str, Union[PathItem, Any]]] = None
|
||||
webhooks: Optional[Dict[str, Union[PathItem, Reference]]] = None
|
||||
components: Optional[Components] = None
|
||||
security: Optional[List[Dict[str, List[str]]]] = None
|
||||
tags: Optional[List[Tag]] = None
|
||||
externalDocs: Optional[ExternalDocumentation] = None
|
||||
|
||||
|
||||
_model_rebuild(Schema)
|
||||
_model_rebuild(Operation)
|
||||
_model_rebuild(Encoding)
|
569
venv/lib/python3.11/site-packages/fastapi/openapi/utils.py
Normal file
569
venv/lib/python3.11/site-packages/fastapi/openapi/utils.py
Normal file
@ -0,0 +1,569 @@
|
||||
import http.client
|
||||
import inspect
|
||||
import warnings
|
||||
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union, cast
|
||||
|
||||
from fastapi import routing
|
||||
from fastapi._compat import (
|
||||
GenerateJsonSchema,
|
||||
JsonSchemaValue,
|
||||
ModelField,
|
||||
Undefined,
|
||||
get_compat_model_name_map,
|
||||
get_definitions,
|
||||
get_schema_from_model_field,
|
||||
lenient_issubclass,
|
||||
)
|
||||
from fastapi.datastructures import DefaultPlaceholder
|
||||
from fastapi.dependencies.models import Dependant
|
||||
from fastapi.dependencies.utils import (
|
||||
_get_flat_fields_from_params,
|
||||
get_flat_dependant,
|
||||
get_flat_params,
|
||||
)
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX, REF_TEMPLATE
|
||||
from fastapi.openapi.models import OpenAPI
|
||||
from fastapi.params import Body, ParamTypes
|
||||
from fastapi.responses import Response
|
||||
from fastapi.types import ModelNameMap
|
||||
from fastapi.utils import (
|
||||
deep_dict_update,
|
||||
generate_operation_id_for_path,
|
||||
is_body_allowed_for_status_code,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.routing import BaseRoute
|
||||
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
|
||||
from typing_extensions import Literal
|
||||
|
||||
validation_error_definition = {
|
||||
"title": "ValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loc": {
|
||||
"title": "Location",
|
||||
"type": "array",
|
||||
"items": {"anyOf": [{"type": "string"}, {"type": "integer"}]},
|
||||
},
|
||||
"msg": {"title": "Message", "type": "string"},
|
||||
"type": {"title": "Error Type", "type": "string"},
|
||||
},
|
||||
"required": ["loc", "msg", "type"],
|
||||
}
|
||||
|
||||
validation_error_response_definition = {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": REF_PREFIX + "ValidationError"},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
status_code_ranges: Dict[str, str] = {
|
||||
"1XX": "Information",
|
||||
"2XX": "Success",
|
||||
"3XX": "Redirection",
|
||||
"4XX": "Client Error",
|
||||
"5XX": "Server Error",
|
||||
"DEFAULT": "Default Response",
|
||||
}
|
||||
|
||||
|
||||
def get_openapi_security_definitions(
|
||||
flat_dependant: Dependant,
|
||||
) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
|
||||
security_definitions = {}
|
||||
operation_security = []
|
||||
for security_requirement in flat_dependant.security_requirements:
|
||||
security_definition = jsonable_encoder(
|
||||
security_requirement.security_scheme.model,
|
||||
by_alias=True,
|
||||
exclude_none=True,
|
||||
)
|
||||
security_name = security_requirement.security_scheme.scheme_name
|
||||
security_definitions[security_name] = security_definition
|
||||
operation_security.append({security_name: security_requirement.scopes})
|
||||
return security_definitions, operation_security
|
||||
|
||||
|
||||
def _get_openapi_operation_parameters(
|
||||
*,
|
||||
dependant: Dependant,
|
||||
schema_generator: GenerateJsonSchema,
|
||||
model_name_map: ModelNameMap,
|
||||
field_mapping: Dict[
|
||||
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
|
||||
],
|
||||
separate_input_output_schemas: bool = True,
|
||||
) -> List[Dict[str, Any]]:
|
||||
parameters = []
|
||||
flat_dependant = get_flat_dependant(dependant, skip_repeats=True)
|
||||
path_params = _get_flat_fields_from_params(flat_dependant.path_params)
|
||||
query_params = _get_flat_fields_from_params(flat_dependant.query_params)
|
||||
header_params = _get_flat_fields_from_params(flat_dependant.header_params)
|
||||
cookie_params = _get_flat_fields_from_params(flat_dependant.cookie_params)
|
||||
parameter_groups = [
|
||||
(ParamTypes.path, path_params),
|
||||
(ParamTypes.query, query_params),
|
||||
(ParamTypes.header, header_params),
|
||||
(ParamTypes.cookie, cookie_params),
|
||||
]
|
||||
default_convert_underscores = True
|
||||
if len(flat_dependant.header_params) == 1:
|
||||
first_field = flat_dependant.header_params[0]
|
||||
if lenient_issubclass(first_field.type_, BaseModel):
|
||||
default_convert_underscores = getattr(
|
||||
first_field.field_info, "convert_underscores", True
|
||||
)
|
||||
for param_type, param_group in parameter_groups:
|
||||
for param in param_group:
|
||||
field_info = param.field_info
|
||||
# field_info = cast(Param, field_info)
|
||||
if not getattr(field_info, "include_in_schema", True):
|
||||
continue
|
||||
param_schema = get_schema_from_model_field(
|
||||
field=param,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
field_mapping=field_mapping,
|
||||
separate_input_output_schemas=separate_input_output_schemas,
|
||||
)
|
||||
name = param.alias
|
||||
convert_underscores = getattr(
|
||||
param.field_info,
|
||||
"convert_underscores",
|
||||
default_convert_underscores,
|
||||
)
|
||||
if (
|
||||
param_type == ParamTypes.header
|
||||
and param.alias == param.name
|
||||
and convert_underscores
|
||||
):
|
||||
name = param.name.replace("_", "-")
|
||||
|
||||
parameter = {
|
||||
"name": name,
|
||||
"in": param_type.value,
|
||||
"required": param.required,
|
||||
"schema": param_schema,
|
||||
}
|
||||
if field_info.description:
|
||||
parameter["description"] = field_info.description
|
||||
openapi_examples = getattr(field_info, "openapi_examples", None)
|
||||
example = getattr(field_info, "example", None)
|
||||
if openapi_examples:
|
||||
parameter["examples"] = jsonable_encoder(openapi_examples)
|
||||
elif example != Undefined:
|
||||
parameter["example"] = jsonable_encoder(example)
|
||||
if getattr(field_info, "deprecated", None):
|
||||
parameter["deprecated"] = True
|
||||
parameters.append(parameter)
|
||||
return parameters
|
||||
|
||||
|
||||
def get_openapi_operation_request_body(
|
||||
*,
|
||||
body_field: Optional[ModelField],
|
||||
schema_generator: GenerateJsonSchema,
|
||||
model_name_map: ModelNameMap,
|
||||
field_mapping: Dict[
|
||||
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
|
||||
],
|
||||
separate_input_output_schemas: bool = True,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
if not body_field:
|
||||
return None
|
||||
assert isinstance(body_field, ModelField)
|
||||
body_schema = get_schema_from_model_field(
|
||||
field=body_field,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
field_mapping=field_mapping,
|
||||
separate_input_output_schemas=separate_input_output_schemas,
|
||||
)
|
||||
field_info = cast(Body, body_field.field_info)
|
||||
request_media_type = field_info.media_type
|
||||
required = body_field.required
|
||||
request_body_oai: Dict[str, Any] = {}
|
||||
if required:
|
||||
request_body_oai["required"] = required
|
||||
request_media_content: Dict[str, Any] = {"schema": body_schema}
|
||||
if field_info.openapi_examples:
|
||||
request_media_content["examples"] = jsonable_encoder(
|
||||
field_info.openapi_examples
|
||||
)
|
||||
elif field_info.example != Undefined:
|
||||
request_media_content["example"] = jsonable_encoder(field_info.example)
|
||||
request_body_oai["content"] = {request_media_type: request_media_content}
|
||||
return request_body_oai
|
||||
|
||||
|
||||
def generate_operation_id(
|
||||
*, route: routing.APIRoute, method: str
|
||||
) -> str: # pragma: nocover
|
||||
warnings.warn(
|
||||
"fastapi.openapi.utils.generate_operation_id() was deprecated, "
|
||||
"it is not used internally, and will be removed soon",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
if route.operation_id:
|
||||
return route.operation_id
|
||||
path: str = route.path_format
|
||||
return generate_operation_id_for_path(name=route.name, path=path, method=method)
|
||||
|
||||
|
||||
def generate_operation_summary(*, route: routing.APIRoute, method: str) -> str:
|
||||
if route.summary:
|
||||
return route.summary
|
||||
return route.name.replace("_", " ").title()
|
||||
|
||||
|
||||
def get_openapi_operation_metadata(
|
||||
*, route: routing.APIRoute, method: str, operation_ids: Set[str]
|
||||
) -> Dict[str, Any]:
|
||||
operation: Dict[str, Any] = {}
|
||||
if route.tags:
|
||||
operation["tags"] = route.tags
|
||||
operation["summary"] = generate_operation_summary(route=route, method=method)
|
||||
if route.description:
|
||||
operation["description"] = route.description
|
||||
operation_id = route.operation_id or route.unique_id
|
||||
if operation_id in operation_ids:
|
||||
message = (
|
||||
f"Duplicate Operation ID {operation_id} for function "
|
||||
+ f"{route.endpoint.__name__}"
|
||||
)
|
||||
file_name = getattr(route.endpoint, "__globals__", {}).get("__file__")
|
||||
if file_name:
|
||||
message += f" at {file_name}"
|
||||
warnings.warn(message, stacklevel=1)
|
||||
operation_ids.add(operation_id)
|
||||
operation["operationId"] = operation_id
|
||||
if route.deprecated:
|
||||
operation["deprecated"] = route.deprecated
|
||||
return operation
|
||||
|
||||
|
||||
def get_openapi_path(
|
||||
*,
|
||||
route: routing.APIRoute,
|
||||
operation_ids: Set[str],
|
||||
schema_generator: GenerateJsonSchema,
|
||||
model_name_map: ModelNameMap,
|
||||
field_mapping: Dict[
|
||||
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
|
||||
],
|
||||
separate_input_output_schemas: bool = True,
|
||||
) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]:
|
||||
path = {}
|
||||
security_schemes: Dict[str, Any] = {}
|
||||
definitions: Dict[str, Any] = {}
|
||||
assert route.methods is not None, "Methods must be a list"
|
||||
if isinstance(route.response_class, DefaultPlaceholder):
|
||||
current_response_class: Type[Response] = route.response_class.value
|
||||
else:
|
||||
current_response_class = route.response_class
|
||||
assert current_response_class, "A response class is needed to generate OpenAPI"
|
||||
route_response_media_type: Optional[str] = current_response_class.media_type
|
||||
if route.include_in_schema:
|
||||
for method in route.methods:
|
||||
operation = get_openapi_operation_metadata(
|
||||
route=route, method=method, operation_ids=operation_ids
|
||||
)
|
||||
parameters: List[Dict[str, Any]] = []
|
||||
flat_dependant = get_flat_dependant(route.dependant, skip_repeats=True)
|
||||
security_definitions, operation_security = get_openapi_security_definitions(
|
||||
flat_dependant=flat_dependant
|
||||
)
|
||||
if operation_security:
|
||||
operation.setdefault("security", []).extend(operation_security)
|
||||
if security_definitions:
|
||||
security_schemes.update(security_definitions)
|
||||
operation_parameters = _get_openapi_operation_parameters(
|
||||
dependant=route.dependant,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
field_mapping=field_mapping,
|
||||
separate_input_output_schemas=separate_input_output_schemas,
|
||||
)
|
||||
parameters.extend(operation_parameters)
|
||||
if parameters:
|
||||
all_parameters = {
|
||||
(param["in"], param["name"]): param for param in parameters
|
||||
}
|
||||
required_parameters = {
|
||||
(param["in"], param["name"]): param
|
||||
for param in parameters
|
||||
if param.get("required")
|
||||
}
|
||||
# Make sure required definitions of the same parameter take precedence
|
||||
# over non-required definitions
|
||||
all_parameters.update(required_parameters)
|
||||
operation["parameters"] = list(all_parameters.values())
|
||||
if method in METHODS_WITH_BODY:
|
||||
request_body_oai = get_openapi_operation_request_body(
|
||||
body_field=route.body_field,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
field_mapping=field_mapping,
|
||||
separate_input_output_schemas=separate_input_output_schemas,
|
||||
)
|
||||
if request_body_oai:
|
||||
operation["requestBody"] = request_body_oai
|
||||
if route.callbacks:
|
||||
callbacks = {}
|
||||
for callback in route.callbacks:
|
||||
if isinstance(callback, routing.APIRoute):
|
||||
(
|
||||
cb_path,
|
||||
cb_security_schemes,
|
||||
cb_definitions,
|
||||
) = get_openapi_path(
|
||||
route=callback,
|
||||
operation_ids=operation_ids,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
field_mapping=field_mapping,
|
||||
separate_input_output_schemas=separate_input_output_schemas,
|
||||
)
|
||||
callbacks[callback.name] = {callback.path: cb_path}
|
||||
operation["callbacks"] = callbacks
|
||||
if route.status_code is not None:
|
||||
status_code = str(route.status_code)
|
||||
else:
|
||||
# It would probably make more sense for all response classes to have an
|
||||
# explicit default status_code, and to extract it from them, instead of
|
||||
# doing this inspection tricks, that would probably be in the future
|
||||
# TODO: probably make status_code a default class attribute for all
|
||||
# responses in Starlette
|
||||
response_signature = inspect.signature(current_response_class.__init__)
|
||||
status_code_param = response_signature.parameters.get("status_code")
|
||||
if status_code_param is not None:
|
||||
if isinstance(status_code_param.default, int):
|
||||
status_code = str(status_code_param.default)
|
||||
operation.setdefault("responses", {}).setdefault(status_code, {})[
|
||||
"description"
|
||||
] = route.response_description
|
||||
if route_response_media_type and is_body_allowed_for_status_code(
|
||||
route.status_code
|
||||
):
|
||||
response_schema = {"type": "string"}
|
||||
if lenient_issubclass(current_response_class, JSONResponse):
|
||||
if route.response_field:
|
||||
response_schema = get_schema_from_model_field(
|
||||
field=route.response_field,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
field_mapping=field_mapping,
|
||||
separate_input_output_schemas=separate_input_output_schemas,
|
||||
)
|
||||
else:
|
||||
response_schema = {}
|
||||
operation.setdefault("responses", {}).setdefault(
|
||||
status_code, {}
|
||||
).setdefault("content", {}).setdefault(route_response_media_type, {})[
|
||||
"schema"
|
||||
] = response_schema
|
||||
if route.responses:
|
||||
operation_responses = operation.setdefault("responses", {})
|
||||
for (
|
||||
additional_status_code,
|
||||
additional_response,
|
||||
) in route.responses.items():
|
||||
process_response = additional_response.copy()
|
||||
process_response.pop("model", None)
|
||||
status_code_key = str(additional_status_code).upper()
|
||||
if status_code_key == "DEFAULT":
|
||||
status_code_key = "default"
|
||||
openapi_response = operation_responses.setdefault(
|
||||
status_code_key, {}
|
||||
)
|
||||
assert isinstance(process_response, dict), (
|
||||
"An additional response must be a dict"
|
||||
)
|
||||
field = route.response_fields.get(additional_status_code)
|
||||
additional_field_schema: Optional[Dict[str, Any]] = None
|
||||
if field:
|
||||
additional_field_schema = get_schema_from_model_field(
|
||||
field=field,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
field_mapping=field_mapping,
|
||||
separate_input_output_schemas=separate_input_output_schemas,
|
||||
)
|
||||
media_type = route_response_media_type or "application/json"
|
||||
additional_schema = (
|
||||
process_response.setdefault("content", {})
|
||||
.setdefault(media_type, {})
|
||||
.setdefault("schema", {})
|
||||
)
|
||||
deep_dict_update(additional_schema, additional_field_schema)
|
||||
status_text: Optional[str] = status_code_ranges.get(
|
||||
str(additional_status_code).upper()
|
||||
) or http.client.responses.get(int(additional_status_code))
|
||||
description = (
|
||||
process_response.get("description")
|
||||
or openapi_response.get("description")
|
||||
or status_text
|
||||
or "Additional Response"
|
||||
)
|
||||
deep_dict_update(openapi_response, process_response)
|
||||
openapi_response["description"] = description
|
||||
http422 = str(HTTP_422_UNPROCESSABLE_ENTITY)
|
||||
all_route_params = get_flat_params(route.dependant)
|
||||
if (all_route_params or route.body_field) and not any(
|
||||
status in operation["responses"]
|
||||
for status in [http422, "4XX", "default"]
|
||||
):
|
||||
operation["responses"][http422] = {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": REF_PREFIX + "HTTPValidationError"}
|
||||
}
|
||||
},
|
||||
}
|
||||
if "ValidationError" not in definitions:
|
||||
definitions.update(
|
||||
{
|
||||
"ValidationError": validation_error_definition,
|
||||
"HTTPValidationError": validation_error_response_definition,
|
||||
}
|
||||
)
|
||||
if route.openapi_extra:
|
||||
deep_dict_update(operation, route.openapi_extra)
|
||||
path[method.lower()] = operation
|
||||
return path, security_schemes, definitions
|
||||
|
||||
|
||||
def get_fields_from_routes(
|
||||
routes: Sequence[BaseRoute],
|
||||
) -> List[ModelField]:
|
||||
body_fields_from_routes: List[ModelField] = []
|
||||
responses_from_routes: List[ModelField] = []
|
||||
request_fields_from_routes: List[ModelField] = []
|
||||
callback_flat_models: List[ModelField] = []
|
||||
for route in routes:
|
||||
if getattr(route, "include_in_schema", None) and isinstance(
|
||||
route, routing.APIRoute
|
||||
):
|
||||
if route.body_field:
|
||||
assert isinstance(route.body_field, ModelField), (
|
||||
"A request body must be a Pydantic Field"
|
||||
)
|
||||
body_fields_from_routes.append(route.body_field)
|
||||
if route.response_field:
|
||||
responses_from_routes.append(route.response_field)
|
||||
if route.response_fields:
|
||||
responses_from_routes.extend(route.response_fields.values())
|
||||
if route.callbacks:
|
||||
callback_flat_models.extend(get_fields_from_routes(route.callbacks))
|
||||
params = get_flat_params(route.dependant)
|
||||
request_fields_from_routes.extend(params)
|
||||
|
||||
flat_models = callback_flat_models + list(
|
||||
body_fields_from_routes + responses_from_routes + request_fields_from_routes
|
||||
)
|
||||
return flat_models
|
||||
|
||||
|
||||
def get_openapi(
|
||||
*,
|
||||
title: str,
|
||||
version: str,
|
||||
openapi_version: str = "3.1.0",
|
||||
summary: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
routes: Sequence[BaseRoute],
|
||||
webhooks: Optional[Sequence[BaseRoute]] = None,
|
||||
tags: Optional[List[Dict[str, Any]]] = None,
|
||||
servers: Optional[List[Dict[str, Union[str, Any]]]] = None,
|
||||
terms_of_service: Optional[str] = None,
|
||||
contact: Optional[Dict[str, Union[str, Any]]] = None,
|
||||
license_info: Optional[Dict[str, Union[str, Any]]] = None,
|
||||
separate_input_output_schemas: bool = True,
|
||||
) -> Dict[str, Any]:
|
||||
info: Dict[str, Any] = {"title": title, "version": version}
|
||||
if summary:
|
||||
info["summary"] = summary
|
||||
if description:
|
||||
info["description"] = description
|
||||
if terms_of_service:
|
||||
info["termsOfService"] = terms_of_service
|
||||
if contact:
|
||||
info["contact"] = contact
|
||||
if license_info:
|
||||
info["license"] = license_info
|
||||
output: Dict[str, Any] = {"openapi": openapi_version, "info": info}
|
||||
if servers:
|
||||
output["servers"] = servers
|
||||
components: Dict[str, Dict[str, Any]] = {}
|
||||
paths: Dict[str, Dict[str, Any]] = {}
|
||||
webhook_paths: Dict[str, Dict[str, Any]] = {}
|
||||
operation_ids: Set[str] = set()
|
||||
all_fields = get_fields_from_routes(list(routes or []) + list(webhooks or []))
|
||||
model_name_map = get_compat_model_name_map(all_fields)
|
||||
schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE)
|
||||
field_mapping, definitions = get_definitions(
|
||||
fields=all_fields,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
separate_input_output_schemas=separate_input_output_schemas,
|
||||
)
|
||||
for route in routes or []:
|
||||
if isinstance(route, routing.APIRoute):
|
||||
result = get_openapi_path(
|
||||
route=route,
|
||||
operation_ids=operation_ids,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
field_mapping=field_mapping,
|
||||
separate_input_output_schemas=separate_input_output_schemas,
|
||||
)
|
||||
if result:
|
||||
path, security_schemes, path_definitions = result
|
||||
if path:
|
||||
paths.setdefault(route.path_format, {}).update(path)
|
||||
if security_schemes:
|
||||
components.setdefault("securitySchemes", {}).update(
|
||||
security_schemes
|
||||
)
|
||||
if path_definitions:
|
||||
definitions.update(path_definitions)
|
||||
for webhook in webhooks or []:
|
||||
if isinstance(webhook, routing.APIRoute):
|
||||
result = get_openapi_path(
|
||||
route=webhook,
|
||||
operation_ids=operation_ids,
|
||||
schema_generator=schema_generator,
|
||||
model_name_map=model_name_map,
|
||||
field_mapping=field_mapping,
|
||||
separate_input_output_schemas=separate_input_output_schemas,
|
||||
)
|
||||
if result:
|
||||
path, security_schemes, path_definitions = result
|
||||
if path:
|
||||
webhook_paths.setdefault(webhook.path_format, {}).update(path)
|
||||
if security_schemes:
|
||||
components.setdefault("securitySchemes", {}).update(
|
||||
security_schemes
|
||||
)
|
||||
if path_definitions:
|
||||
definitions.update(path_definitions)
|
||||
if definitions:
|
||||
components["schemas"] = {k: definitions[k] for k in sorted(definitions)}
|
||||
if components:
|
||||
output["components"] = components
|
||||
output["paths"] = paths
|
||||
if webhook_paths:
|
||||
output["webhooks"] = webhook_paths
|
||||
if tags:
|
||||
output["tags"] = tags
|
||||
return jsonable_encoder(OpenAPI(**output), by_alias=True, exclude_none=True) # type: ignore
|
2360
venv/lib/python3.11/site-packages/fastapi/param_functions.py
Normal file
2360
venv/lib/python3.11/site-packages/fastapi/param_functions.py
Normal file
File diff suppressed because it is too large
Load Diff
786
venv/lib/python3.11/site-packages/fastapi/params.py
Normal file
786
venv/lib/python3.11/site-packages/fastapi/params.py
Normal file
@ -0,0 +1,786 @@
|
||||
import warnings
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, Dict, List, Optional, Sequence, Union
|
||||
|
||||
from fastapi.openapi.models import Example
|
||||
from pydantic.fields import FieldInfo
|
||||
from typing_extensions import Annotated, deprecated
|
||||
|
||||
from ._compat import (
|
||||
PYDANTIC_V2,
|
||||
PYDANTIC_VERSION_MINOR_TUPLE,
|
||||
Undefined,
|
||||
)
|
||||
|
||||
_Unset: Any = Undefined
|
||||
|
||||
|
||||
class ParamTypes(Enum):
|
||||
query = "query"
|
||||
header = "header"
|
||||
path = "path"
|
||||
cookie = "cookie"
|
||||
|
||||
|
||||
class Param(FieldInfo):
|
||||
in_: ParamTypes
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
default_factory: Union[Callable[[], Any], None] = _Unset,
|
||||
annotation: Optional[Any] = None,
|
||||
alias: Optional[str] = None,
|
||||
alias_priority: Union[int, None] = _Unset,
|
||||
# TODO: update when deprecating Pydantic v1, import these types
|
||||
# validation_alias: str | AliasPath | AliasChoices | None
|
||||
validation_alias: Union[str, None] = None,
|
||||
serialization_alias: Union[str, None] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
gt: Optional[float] = None,
|
||||
ge: Optional[float] = None,
|
||||
lt: Optional[float] = None,
|
||||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
discriminator: Union[str, None] = None,
|
||||
strict: Union[bool, None] = _Unset,
|
||||
multiple_of: Union[float, None] = _Unset,
|
||||
allow_inf_nan: Union[bool, None] = _Unset,
|
||||
max_digits: Union[int, None] = _Unset,
|
||||
decimal_places: Union[int, None] = _Unset,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
deprecated(
|
||||
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
|
||||
"although still supported. Use examples instead."
|
||||
),
|
||||
] = _Unset,
|
||||
openapi_examples: Optional[Dict[str, Example]] = None,
|
||||
deprecated: Union[deprecated, str, bool, None] = None,
|
||||
include_in_schema: bool = True,
|
||||
json_schema_extra: Union[Dict[str, Any], None] = None,
|
||||
**extra: Any,
|
||||
):
|
||||
if example is not _Unset:
|
||||
warnings.warn(
|
||||
"`example` has been deprecated, please use `examples` instead",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=4,
|
||||
)
|
||||
self.example = example
|
||||
self.include_in_schema = include_in_schema
|
||||
self.openapi_examples = openapi_examples
|
||||
kwargs = dict(
|
||||
default=default,
|
||||
default_factory=default_factory,
|
||||
alias=alias,
|
||||
title=title,
|
||||
description=description,
|
||||
gt=gt,
|
||||
ge=ge,
|
||||
lt=lt,
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
discriminator=discriminator,
|
||||
multiple_of=multiple_of,
|
||||
allow_inf_nan=allow_inf_nan,
|
||||
max_digits=max_digits,
|
||||
decimal_places=decimal_places,
|
||||
**extra,
|
||||
)
|
||||
if examples is not None:
|
||||
kwargs["examples"] = examples
|
||||
if regex is not None:
|
||||
warnings.warn(
|
||||
"`regex` has been deprecated, please use `pattern` instead",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=4,
|
||||
)
|
||||
current_json_schema_extra = json_schema_extra or extra
|
||||
if PYDANTIC_VERSION_MINOR_TUPLE < (2, 7):
|
||||
self.deprecated = deprecated
|
||||
else:
|
||||
kwargs["deprecated"] = deprecated
|
||||
if PYDANTIC_V2:
|
||||
kwargs.update(
|
||||
{
|
||||
"annotation": annotation,
|
||||
"alias_priority": alias_priority,
|
||||
"validation_alias": validation_alias,
|
||||
"serialization_alias": serialization_alias,
|
||||
"strict": strict,
|
||||
"json_schema_extra": current_json_schema_extra,
|
||||
}
|
||||
)
|
||||
kwargs["pattern"] = pattern or regex
|
||||
else:
|
||||
kwargs["regex"] = pattern or regex
|
||||
kwargs.update(**current_json_schema_extra)
|
||||
use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset}
|
||||
|
||||
super().__init__(**use_kwargs)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.default})"
|
||||
|
||||
|
||||
class Path(Param):
|
||||
in_ = ParamTypes.path
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
default: Any = ...,
|
||||
*,
|
||||
default_factory: Union[Callable[[], Any], None] = _Unset,
|
||||
annotation: Optional[Any] = None,
|
||||
alias: Optional[str] = None,
|
||||
alias_priority: Union[int, None] = _Unset,
|
||||
# TODO: update when deprecating Pydantic v1, import these types
|
||||
# validation_alias: str | AliasPath | AliasChoices | None
|
||||
validation_alias: Union[str, None] = None,
|
||||
serialization_alias: Union[str, None] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
gt: Optional[float] = None,
|
||||
ge: Optional[float] = None,
|
||||
lt: Optional[float] = None,
|
||||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
discriminator: Union[str, None] = None,
|
||||
strict: Union[bool, None] = _Unset,
|
||||
multiple_of: Union[float, None] = _Unset,
|
||||
allow_inf_nan: Union[bool, None] = _Unset,
|
||||
max_digits: Union[int, None] = _Unset,
|
||||
decimal_places: Union[int, None] = _Unset,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
deprecated(
|
||||
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
|
||||
"although still supported. Use examples instead."
|
||||
),
|
||||
] = _Unset,
|
||||
openapi_examples: Optional[Dict[str, Example]] = None,
|
||||
deprecated: Union[deprecated, str, bool, None] = None,
|
||||
include_in_schema: bool = True,
|
||||
json_schema_extra: Union[Dict[str, Any], None] = None,
|
||||
**extra: Any,
|
||||
):
|
||||
assert default is ..., "Path parameters cannot have a default value"
|
||||
self.in_ = self.in_
|
||||
super().__init__(
|
||||
default=default,
|
||||
default_factory=default_factory,
|
||||
annotation=annotation,
|
||||
alias=alias,
|
||||
alias_priority=alias_priority,
|
||||
validation_alias=validation_alias,
|
||||
serialization_alias=serialization_alias,
|
||||
title=title,
|
||||
description=description,
|
||||
gt=gt,
|
||||
ge=ge,
|
||||
lt=lt,
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
discriminator=discriminator,
|
||||
strict=strict,
|
||||
multiple_of=multiple_of,
|
||||
allow_inf_nan=allow_inf_nan,
|
||||
max_digits=max_digits,
|
||||
decimal_places=decimal_places,
|
||||
deprecated=deprecated,
|
||||
example=example,
|
||||
examples=examples,
|
||||
openapi_examples=openapi_examples,
|
||||
include_in_schema=include_in_schema,
|
||||
json_schema_extra=json_schema_extra,
|
||||
**extra,
|
||||
)
|
||||
|
||||
|
||||
class Query(Param):
|
||||
in_ = ParamTypes.query
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
default_factory: Union[Callable[[], Any], None] = _Unset,
|
||||
annotation: Optional[Any] = None,
|
||||
alias: Optional[str] = None,
|
||||
alias_priority: Union[int, None] = _Unset,
|
||||
# TODO: update when deprecating Pydantic v1, import these types
|
||||
# validation_alias: str | AliasPath | AliasChoices | None
|
||||
validation_alias: Union[str, None] = None,
|
||||
serialization_alias: Union[str, None] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
gt: Optional[float] = None,
|
||||
ge: Optional[float] = None,
|
||||
lt: Optional[float] = None,
|
||||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
discriminator: Union[str, None] = None,
|
||||
strict: Union[bool, None] = _Unset,
|
||||
multiple_of: Union[float, None] = _Unset,
|
||||
allow_inf_nan: Union[bool, None] = _Unset,
|
||||
max_digits: Union[int, None] = _Unset,
|
||||
decimal_places: Union[int, None] = _Unset,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
deprecated(
|
||||
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
|
||||
"although still supported. Use examples instead."
|
||||
),
|
||||
] = _Unset,
|
||||
openapi_examples: Optional[Dict[str, Example]] = None,
|
||||
deprecated: Union[deprecated, str, bool, None] = None,
|
||||
include_in_schema: bool = True,
|
||||
json_schema_extra: Union[Dict[str, Any], None] = None,
|
||||
**extra: Any,
|
||||
):
|
||||
super().__init__(
|
||||
default=default,
|
||||
default_factory=default_factory,
|
||||
annotation=annotation,
|
||||
alias=alias,
|
||||
alias_priority=alias_priority,
|
||||
validation_alias=validation_alias,
|
||||
serialization_alias=serialization_alias,
|
||||
title=title,
|
||||
description=description,
|
||||
gt=gt,
|
||||
ge=ge,
|
||||
lt=lt,
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
discriminator=discriminator,
|
||||
strict=strict,
|
||||
multiple_of=multiple_of,
|
||||
allow_inf_nan=allow_inf_nan,
|
||||
max_digits=max_digits,
|
||||
decimal_places=decimal_places,
|
||||
deprecated=deprecated,
|
||||
example=example,
|
||||
examples=examples,
|
||||
openapi_examples=openapi_examples,
|
||||
include_in_schema=include_in_schema,
|
||||
json_schema_extra=json_schema_extra,
|
||||
**extra,
|
||||
)
|
||||
|
||||
|
||||
class Header(Param):
|
||||
in_ = ParamTypes.header
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
default_factory: Union[Callable[[], Any], None] = _Unset,
|
||||
annotation: Optional[Any] = None,
|
||||
alias: Optional[str] = None,
|
||||
alias_priority: Union[int, None] = _Unset,
|
||||
# TODO: update when deprecating Pydantic v1, import these types
|
||||
# validation_alias: str | AliasPath | AliasChoices | None
|
||||
validation_alias: Union[str, None] = None,
|
||||
serialization_alias: Union[str, None] = None,
|
||||
convert_underscores: bool = True,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
gt: Optional[float] = None,
|
||||
ge: Optional[float] = None,
|
||||
lt: Optional[float] = None,
|
||||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
discriminator: Union[str, None] = None,
|
||||
strict: Union[bool, None] = _Unset,
|
||||
multiple_of: Union[float, None] = _Unset,
|
||||
allow_inf_nan: Union[bool, None] = _Unset,
|
||||
max_digits: Union[int, None] = _Unset,
|
||||
decimal_places: Union[int, None] = _Unset,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
deprecated(
|
||||
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
|
||||
"although still supported. Use examples instead."
|
||||
),
|
||||
] = _Unset,
|
||||
openapi_examples: Optional[Dict[str, Example]] = None,
|
||||
deprecated: Union[deprecated, str, bool, None] = None,
|
||||
include_in_schema: bool = True,
|
||||
json_schema_extra: Union[Dict[str, Any], None] = None,
|
||||
**extra: Any,
|
||||
):
|
||||
self.convert_underscores = convert_underscores
|
||||
super().__init__(
|
||||
default=default,
|
||||
default_factory=default_factory,
|
||||
annotation=annotation,
|
||||
alias=alias,
|
||||
alias_priority=alias_priority,
|
||||
validation_alias=validation_alias,
|
||||
serialization_alias=serialization_alias,
|
||||
title=title,
|
||||
description=description,
|
||||
gt=gt,
|
||||
ge=ge,
|
||||
lt=lt,
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
discriminator=discriminator,
|
||||
strict=strict,
|
||||
multiple_of=multiple_of,
|
||||
allow_inf_nan=allow_inf_nan,
|
||||
max_digits=max_digits,
|
||||
decimal_places=decimal_places,
|
||||
deprecated=deprecated,
|
||||
example=example,
|
||||
examples=examples,
|
||||
openapi_examples=openapi_examples,
|
||||
include_in_schema=include_in_schema,
|
||||
json_schema_extra=json_schema_extra,
|
||||
**extra,
|
||||
)
|
||||
|
||||
|
||||
class Cookie(Param):
|
||||
in_ = ParamTypes.cookie
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
default_factory: Union[Callable[[], Any], None] = _Unset,
|
||||
annotation: Optional[Any] = None,
|
||||
alias: Optional[str] = None,
|
||||
alias_priority: Union[int, None] = _Unset,
|
||||
# TODO: update when deprecating Pydantic v1, import these types
|
||||
# validation_alias: str | AliasPath | AliasChoices | None
|
||||
validation_alias: Union[str, None] = None,
|
||||
serialization_alias: Union[str, None] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
gt: Optional[float] = None,
|
||||
ge: Optional[float] = None,
|
||||
lt: Optional[float] = None,
|
||||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
discriminator: Union[str, None] = None,
|
||||
strict: Union[bool, None] = _Unset,
|
||||
multiple_of: Union[float, None] = _Unset,
|
||||
allow_inf_nan: Union[bool, None] = _Unset,
|
||||
max_digits: Union[int, None] = _Unset,
|
||||
decimal_places: Union[int, None] = _Unset,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
deprecated(
|
||||
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
|
||||
"although still supported. Use examples instead."
|
||||
),
|
||||
] = _Unset,
|
||||
openapi_examples: Optional[Dict[str, Example]] = None,
|
||||
deprecated: Union[deprecated, str, bool, None] = None,
|
||||
include_in_schema: bool = True,
|
||||
json_schema_extra: Union[Dict[str, Any], None] = None,
|
||||
**extra: Any,
|
||||
):
|
||||
super().__init__(
|
||||
default=default,
|
||||
default_factory=default_factory,
|
||||
annotation=annotation,
|
||||
alias=alias,
|
||||
alias_priority=alias_priority,
|
||||
validation_alias=validation_alias,
|
||||
serialization_alias=serialization_alias,
|
||||
title=title,
|
||||
description=description,
|
||||
gt=gt,
|
||||
ge=ge,
|
||||
lt=lt,
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
discriminator=discriminator,
|
||||
strict=strict,
|
||||
multiple_of=multiple_of,
|
||||
allow_inf_nan=allow_inf_nan,
|
||||
max_digits=max_digits,
|
||||
decimal_places=decimal_places,
|
||||
deprecated=deprecated,
|
||||
example=example,
|
||||
examples=examples,
|
||||
openapi_examples=openapi_examples,
|
||||
include_in_schema=include_in_schema,
|
||||
json_schema_extra=json_schema_extra,
|
||||
**extra,
|
||||
)
|
||||
|
||||
|
||||
class Body(FieldInfo):
|
||||
def __init__(
|
||||
self,
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
default_factory: Union[Callable[[], Any], None] = _Unset,
|
||||
annotation: Optional[Any] = None,
|
||||
embed: Union[bool, None] = None,
|
||||
media_type: str = "application/json",
|
||||
alias: Optional[str] = None,
|
||||
alias_priority: Union[int, None] = _Unset,
|
||||
# TODO: update when deprecating Pydantic v1, import these types
|
||||
# validation_alias: str | AliasPath | AliasChoices | None
|
||||
validation_alias: Union[str, None] = None,
|
||||
serialization_alias: Union[str, None] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
gt: Optional[float] = None,
|
||||
ge: Optional[float] = None,
|
||||
lt: Optional[float] = None,
|
||||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
discriminator: Union[str, None] = None,
|
||||
strict: Union[bool, None] = _Unset,
|
||||
multiple_of: Union[float, None] = _Unset,
|
||||
allow_inf_nan: Union[bool, None] = _Unset,
|
||||
max_digits: Union[int, None] = _Unset,
|
||||
decimal_places: Union[int, None] = _Unset,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
deprecated(
|
||||
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
|
||||
"although still supported. Use examples instead."
|
||||
),
|
||||
] = _Unset,
|
||||
openapi_examples: Optional[Dict[str, Example]] = None,
|
||||
deprecated: Union[deprecated, str, bool, None] = None,
|
||||
include_in_schema: bool = True,
|
||||
json_schema_extra: Union[Dict[str, Any], None] = None,
|
||||
**extra: Any,
|
||||
):
|
||||
self.embed = embed
|
||||
self.media_type = media_type
|
||||
if example is not _Unset:
|
||||
warnings.warn(
|
||||
"`example` has been deprecated, please use `examples` instead",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=4,
|
||||
)
|
||||
self.example = example
|
||||
self.include_in_schema = include_in_schema
|
||||
self.openapi_examples = openapi_examples
|
||||
kwargs = dict(
|
||||
default=default,
|
||||
default_factory=default_factory,
|
||||
alias=alias,
|
||||
title=title,
|
||||
description=description,
|
||||
gt=gt,
|
||||
ge=ge,
|
||||
lt=lt,
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
discriminator=discriminator,
|
||||
multiple_of=multiple_of,
|
||||
allow_inf_nan=allow_inf_nan,
|
||||
max_digits=max_digits,
|
||||
decimal_places=decimal_places,
|
||||
**extra,
|
||||
)
|
||||
if examples is not None:
|
||||
kwargs["examples"] = examples
|
||||
if regex is not None:
|
||||
warnings.warn(
|
||||
"`regex` has been deprecated, please use `pattern` instead",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=4,
|
||||
)
|
||||
current_json_schema_extra = json_schema_extra or extra
|
||||
if PYDANTIC_VERSION_MINOR_TUPLE < (2, 7):
|
||||
self.deprecated = deprecated
|
||||
else:
|
||||
kwargs["deprecated"] = deprecated
|
||||
if PYDANTIC_V2:
|
||||
kwargs.update(
|
||||
{
|
||||
"annotation": annotation,
|
||||
"alias_priority": alias_priority,
|
||||
"validation_alias": validation_alias,
|
||||
"serialization_alias": serialization_alias,
|
||||
"strict": strict,
|
||||
"json_schema_extra": current_json_schema_extra,
|
||||
}
|
||||
)
|
||||
kwargs["pattern"] = pattern or regex
|
||||
else:
|
||||
kwargs["regex"] = pattern or regex
|
||||
kwargs.update(**current_json_schema_extra)
|
||||
|
||||
use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset}
|
||||
|
||||
super().__init__(**use_kwargs)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.default})"
|
||||
|
||||
|
||||
class Form(Body):
|
||||
def __init__(
|
||||
self,
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
default_factory: Union[Callable[[], Any], None] = _Unset,
|
||||
annotation: Optional[Any] = None,
|
||||
media_type: str = "application/x-www-form-urlencoded",
|
||||
alias: Optional[str] = None,
|
||||
alias_priority: Union[int, None] = _Unset,
|
||||
# TODO: update when deprecating Pydantic v1, import these types
|
||||
# validation_alias: str | AliasPath | AliasChoices | None
|
||||
validation_alias: Union[str, None] = None,
|
||||
serialization_alias: Union[str, None] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
gt: Optional[float] = None,
|
||||
ge: Optional[float] = None,
|
||||
lt: Optional[float] = None,
|
||||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
discriminator: Union[str, None] = None,
|
||||
strict: Union[bool, None] = _Unset,
|
||||
multiple_of: Union[float, None] = _Unset,
|
||||
allow_inf_nan: Union[bool, None] = _Unset,
|
||||
max_digits: Union[int, None] = _Unset,
|
||||
decimal_places: Union[int, None] = _Unset,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
deprecated(
|
||||
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
|
||||
"although still supported. Use examples instead."
|
||||
),
|
||||
] = _Unset,
|
||||
openapi_examples: Optional[Dict[str, Example]] = None,
|
||||
deprecated: Union[deprecated, str, bool, None] = None,
|
||||
include_in_schema: bool = True,
|
||||
json_schema_extra: Union[Dict[str, Any], None] = None,
|
||||
**extra: Any,
|
||||
):
|
||||
super().__init__(
|
||||
default=default,
|
||||
default_factory=default_factory,
|
||||
annotation=annotation,
|
||||
media_type=media_type,
|
||||
alias=alias,
|
||||
alias_priority=alias_priority,
|
||||
validation_alias=validation_alias,
|
||||
serialization_alias=serialization_alias,
|
||||
title=title,
|
||||
description=description,
|
||||
gt=gt,
|
||||
ge=ge,
|
||||
lt=lt,
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
discriminator=discriminator,
|
||||
strict=strict,
|
||||
multiple_of=multiple_of,
|
||||
allow_inf_nan=allow_inf_nan,
|
||||
max_digits=max_digits,
|
||||
decimal_places=decimal_places,
|
||||
deprecated=deprecated,
|
||||
example=example,
|
||||
examples=examples,
|
||||
openapi_examples=openapi_examples,
|
||||
include_in_schema=include_in_schema,
|
||||
json_schema_extra=json_schema_extra,
|
||||
**extra,
|
||||
)
|
||||
|
||||
|
||||
class File(Form):
|
||||
def __init__(
|
||||
self,
|
||||
default: Any = Undefined,
|
||||
*,
|
||||
default_factory: Union[Callable[[], Any], None] = _Unset,
|
||||
annotation: Optional[Any] = None,
|
||||
media_type: str = "multipart/form-data",
|
||||
alias: Optional[str] = None,
|
||||
alias_priority: Union[int, None] = _Unset,
|
||||
# TODO: update when deprecating Pydantic v1, import these types
|
||||
# validation_alias: str | AliasPath | AliasChoices | None
|
||||
validation_alias: Union[str, None] = None,
|
||||
serialization_alias: Union[str, None] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
gt: Optional[float] = None,
|
||||
ge: Optional[float] = None,
|
||||
lt: Optional[float] = None,
|
||||
le: Optional[float] = None,
|
||||
min_length: Optional[int] = None,
|
||||
max_length: Optional[int] = None,
|
||||
pattern: Optional[str] = None,
|
||||
regex: Annotated[
|
||||
Optional[str],
|
||||
deprecated(
|
||||
"Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
|
||||
),
|
||||
] = None,
|
||||
discriminator: Union[str, None] = None,
|
||||
strict: Union[bool, None] = _Unset,
|
||||
multiple_of: Union[float, None] = _Unset,
|
||||
allow_inf_nan: Union[bool, None] = _Unset,
|
||||
max_digits: Union[int, None] = _Unset,
|
||||
decimal_places: Union[int, None] = _Unset,
|
||||
examples: Optional[List[Any]] = None,
|
||||
example: Annotated[
|
||||
Optional[Any],
|
||||
deprecated(
|
||||
"Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
|
||||
"although still supported. Use examples instead."
|
||||
),
|
||||
] = _Unset,
|
||||
openapi_examples: Optional[Dict[str, Example]] = None,
|
||||
deprecated: Union[deprecated, str, bool, None] = None,
|
||||
include_in_schema: bool = True,
|
||||
json_schema_extra: Union[Dict[str, Any], None] = None,
|
||||
**extra: Any,
|
||||
):
|
||||
super().__init__(
|
||||
default=default,
|
||||
default_factory=default_factory,
|
||||
annotation=annotation,
|
||||
media_type=media_type,
|
||||
alias=alias,
|
||||
alias_priority=alias_priority,
|
||||
validation_alias=validation_alias,
|
||||
serialization_alias=serialization_alias,
|
||||
title=title,
|
||||
description=description,
|
||||
gt=gt,
|
||||
ge=ge,
|
||||
lt=lt,
|
||||
le=le,
|
||||
min_length=min_length,
|
||||
max_length=max_length,
|
||||
pattern=pattern,
|
||||
regex=regex,
|
||||
discriminator=discriminator,
|
||||
strict=strict,
|
||||
multiple_of=multiple_of,
|
||||
allow_inf_nan=allow_inf_nan,
|
||||
max_digits=max_digits,
|
||||
decimal_places=decimal_places,
|
||||
deprecated=deprecated,
|
||||
example=example,
|
||||
examples=examples,
|
||||
openapi_examples=openapi_examples,
|
||||
include_in_schema=include_in_schema,
|
||||
json_schema_extra=json_schema_extra,
|
||||
**extra,
|
||||
)
|
||||
|
||||
|
||||
class Depends:
|
||||
def __init__(
|
||||
self, dependency: Optional[Callable[..., Any]] = None, *, use_cache: bool = True
|
||||
):
|
||||
self.dependency = dependency
|
||||
self.use_cache = use_cache
|
||||
|
||||
def __repr__(self) -> str:
|
||||
attr = getattr(self.dependency, "__name__", type(self.dependency).__name__)
|
||||
cache = "" if self.use_cache else ", use_cache=False"
|
||||
return f"{self.__class__.__name__}({attr}{cache})"
|
||||
|
||||
|
||||
class Security(Depends):
|
||||
def __init__(
|
||||
self,
|
||||
dependency: Optional[Callable[..., Any]] = None,
|
||||
*,
|
||||
scopes: Optional[Sequence[str]] = None,
|
||||
use_cache: bool = True,
|
||||
):
|
||||
super().__init__(dependency=dependency, use_cache=use_cache)
|
||||
self.scopes = scopes or []
|
0
venv/lib/python3.11/site-packages/fastapi/py.typed
Normal file
0
venv/lib/python3.11/site-packages/fastapi/py.typed
Normal file
2
venv/lib/python3.11/site-packages/fastapi/requests.py
Normal file
2
venv/lib/python3.11/site-packages/fastapi/requests.py
Normal file
@ -0,0 +1,2 @@
|
||||
from starlette.requests import HTTPConnection as HTTPConnection # noqa: F401
|
||||
from starlette.requests import Request as Request # noqa: F401
|
48
venv/lib/python3.11/site-packages/fastapi/responses.py
Normal file
48
venv/lib/python3.11/site-packages/fastapi/responses.py
Normal file
@ -0,0 +1,48 @@
|
||||
from typing import Any
|
||||
|
||||
from starlette.responses import FileResponse as FileResponse # noqa
|
||||
from starlette.responses import HTMLResponse as HTMLResponse # noqa
|
||||
from starlette.responses import JSONResponse as JSONResponse # noqa
|
||||
from starlette.responses import PlainTextResponse as PlainTextResponse # noqa
|
||||
from starlette.responses import RedirectResponse as RedirectResponse # noqa
|
||||
from starlette.responses import Response as Response # noqa
|
||||
from starlette.responses import StreamingResponse as StreamingResponse # noqa
|
||||
|
||||
try:
|
||||
import ujson
|
||||
except ImportError: # pragma: nocover
|
||||
ujson = None # type: ignore
|
||||
|
||||
|
||||
try:
|
||||
import orjson
|
||||
except ImportError: # pragma: nocover
|
||||
orjson = None # type: ignore
|
||||
|
||||
|
||||
class UJSONResponse(JSONResponse):
|
||||
"""
|
||||
JSON response using the high-performance ujson library to serialize data to JSON.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/).
|
||||
"""
|
||||
|
||||
def render(self, content: Any) -> bytes:
|
||||
assert ujson is not None, "ujson must be installed to use UJSONResponse"
|
||||
return ujson.dumps(content, ensure_ascii=False).encode("utf-8")
|
||||
|
||||
|
||||
class ORJSONResponse(JSONResponse):
|
||||
"""
|
||||
JSON response using the high-performance orjson library to serialize data to JSON.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/).
|
||||
"""
|
||||
|
||||
def render(self, content: Any) -> bytes:
|
||||
assert orjson is not None, "orjson must be installed to use ORJSONResponse"
|
||||
return orjson.dumps(
|
||||
content, option=orjson.OPT_NON_STR_KEYS | orjson.OPT_SERIALIZE_NUMPY
|
||||
)
|
4439
venv/lib/python3.11/site-packages/fastapi/routing.py
Normal file
4439
venv/lib/python3.11/site-packages/fastapi/routing.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,15 @@
|
||||
from .api_key import APIKeyCookie as APIKeyCookie
|
||||
from .api_key import APIKeyHeader as APIKeyHeader
|
||||
from .api_key import APIKeyQuery as APIKeyQuery
|
||||
from .http import HTTPAuthorizationCredentials as HTTPAuthorizationCredentials
|
||||
from .http import HTTPBasic as HTTPBasic
|
||||
from .http import HTTPBasicCredentials as HTTPBasicCredentials
|
||||
from .http import HTTPBearer as HTTPBearer
|
||||
from .http import HTTPDigest as HTTPDigest
|
||||
from .oauth2 import OAuth2 as OAuth2
|
||||
from .oauth2 import OAuth2AuthorizationCodeBearer as OAuth2AuthorizationCodeBearer
|
||||
from .oauth2 import OAuth2PasswordBearer as OAuth2PasswordBearer
|
||||
from .oauth2 import OAuth2PasswordRequestForm as OAuth2PasswordRequestForm
|
||||
from .oauth2 import OAuth2PasswordRequestFormStrict as OAuth2PasswordRequestFormStrict
|
||||
from .oauth2 import SecurityScopes as SecurityScopes
|
||||
from .open_id_connect_url import OpenIdConnect as OpenIdConnect
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
288
venv/lib/python3.11/site-packages/fastapi/security/api_key.py
Normal file
288
venv/lib/python3.11/site-packages/fastapi/security/api_key.py
Normal file
@ -0,0 +1,288 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi.openapi.models import APIKey, APIKeyIn
|
||||
from fastapi.security.base import SecurityBase
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.status import HTTP_403_FORBIDDEN
|
||||
from typing_extensions import Annotated, Doc
|
||||
|
||||
|
||||
class APIKeyBase(SecurityBase):
|
||||
@staticmethod
|
||||
def check_api_key(api_key: Optional[str], auto_error: bool) -> Optional[str]:
|
||||
if not api_key:
|
||||
if auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
return None
|
||||
return api_key
|
||||
|
||||
|
||||
class APIKeyQuery(APIKeyBase):
|
||||
"""
|
||||
API key authentication using a query parameter.
|
||||
|
||||
This defines the name of the query parameter that should be provided in the request
|
||||
with the API key and integrates that into the OpenAPI documentation. It extracts
|
||||
the key value sent in the query parameter automatically and provides it as the
|
||||
dependency result. But it doesn't define how to send that API key to the client.
|
||||
|
||||
## Usage
|
||||
|
||||
Create an instance object and use that object as the dependency in `Depends()`.
|
||||
|
||||
The dependency result will be a string containing the key value.
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.security import APIKeyQuery
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
query_scheme = APIKeyQuery(name="api_key")
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_items(api_key: str = Depends(query_scheme)):
|
||||
return {"api_key": api_key}
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name: Annotated[
|
||||
str,
|
||||
Doc("Query parameter name."),
|
||||
],
|
||||
scheme_name: Annotated[
|
||||
Optional[str],
|
||||
Doc(
|
||||
"""
|
||||
Security scheme name.
|
||||
|
||||
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
description: Annotated[
|
||||
Optional[str],
|
||||
Doc(
|
||||
"""
|
||||
Security scheme description.
|
||||
|
||||
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
auto_error: Annotated[
|
||||
bool,
|
||||
Doc(
|
||||
"""
|
||||
By default, if the query parameter is not provided, `APIKeyQuery` will
|
||||
automatically cancel the request and send the client an error.
|
||||
|
||||
If `auto_error` is set to `False`, when the query parameter is not
|
||||
available, instead of erroring out, the dependency result will be
|
||||
`None`.
|
||||
|
||||
This is useful when you want to have optional authentication.
|
||||
|
||||
It is also useful when you want to have authentication that can be
|
||||
provided in one of multiple optional ways (for example, in a query
|
||||
parameter or in an HTTP Bearer token).
|
||||
"""
|
||||
),
|
||||
] = True,
|
||||
):
|
||||
self.model: APIKey = APIKey(
|
||||
**{"in": APIKeyIn.query}, # type: ignore[arg-type]
|
||||
name=name,
|
||||
description=description,
|
||||
)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.auto_error = auto_error
|
||||
|
||||
async def __call__(self, request: Request) -> Optional[str]:
|
||||
api_key = request.query_params.get(self.model.name)
|
||||
return self.check_api_key(api_key, self.auto_error)
|
||||
|
||||
|
||||
class APIKeyHeader(APIKeyBase):
|
||||
"""
|
||||
API key authentication using a header.
|
||||
|
||||
This defines the name of the header that should be provided in the request with
|
||||
the API key and integrates that into the OpenAPI documentation. It extracts
|
||||
the key value sent in the header automatically and provides it as the dependency
|
||||
result. But it doesn't define how to send that key to the client.
|
||||
|
||||
## Usage
|
||||
|
||||
Create an instance object and use that object as the dependency in `Depends()`.
|
||||
|
||||
The dependency result will be a string containing the key value.
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.security import APIKeyHeader
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
header_scheme = APIKeyHeader(name="x-key")
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_items(key: str = Depends(header_scheme)):
|
||||
return {"key": key}
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name: Annotated[str, Doc("Header name.")],
|
||||
scheme_name: Annotated[
|
||||
Optional[str],
|
||||
Doc(
|
||||
"""
|
||||
Security scheme name.
|
||||
|
||||
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
description: Annotated[
|
||||
Optional[str],
|
||||
Doc(
|
||||
"""
|
||||
Security scheme description.
|
||||
|
||||
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
auto_error: Annotated[
|
||||
bool,
|
||||
Doc(
|
||||
"""
|
||||
By default, if the header is not provided, `APIKeyHeader` will
|
||||
automatically cancel the request and send the client an error.
|
||||
|
||||
If `auto_error` is set to `False`, when the header is not available,
|
||||
instead of erroring out, the dependency result will be `None`.
|
||||
|
||||
This is useful when you want to have optional authentication.
|
||||
|
||||
It is also useful when you want to have authentication that can be
|
||||
provided in one of multiple optional ways (for example, in a header or
|
||||
in an HTTP Bearer token).
|
||||
"""
|
||||
),
|
||||
] = True,
|
||||
):
|
||||
self.model: APIKey = APIKey(
|
||||
**{"in": APIKeyIn.header}, # type: ignore[arg-type]
|
||||
name=name,
|
||||
description=description,
|
||||
)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.auto_error = auto_error
|
||||
|
||||
async def __call__(self, request: Request) -> Optional[str]:
|
||||
api_key = request.headers.get(self.model.name)
|
||||
return self.check_api_key(api_key, self.auto_error)
|
||||
|
||||
|
||||
class APIKeyCookie(APIKeyBase):
|
||||
"""
|
||||
API key authentication using a cookie.
|
||||
|
||||
This defines the name of the cookie that should be provided in the request with
|
||||
the API key and integrates that into the OpenAPI documentation. It extracts
|
||||
the key value sent in the cookie automatically and provides it as the dependency
|
||||
result. But it doesn't define how to set that cookie.
|
||||
|
||||
## Usage
|
||||
|
||||
Create an instance object and use that object as the dependency in `Depends()`.
|
||||
|
||||
The dependency result will be a string containing the key value.
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.security import APIKeyCookie
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
cookie_scheme = APIKeyCookie(name="session")
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_items(session: str = Depends(cookie_scheme)):
|
||||
return {"session": session}
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name: Annotated[str, Doc("Cookie name.")],
|
||||
scheme_name: Annotated[
|
||||
Optional[str],
|
||||
Doc(
|
||||
"""
|
||||
Security scheme name.
|
||||
|
||||
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
description: Annotated[
|
||||
Optional[str],
|
||||
Doc(
|
||||
"""
|
||||
Security scheme description.
|
||||
|
||||
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
auto_error: Annotated[
|
||||
bool,
|
||||
Doc(
|
||||
"""
|
||||
By default, if the cookie is not provided, `APIKeyCookie` will
|
||||
automatically cancel the request and send the client an error.
|
||||
|
||||
If `auto_error` is set to `False`, when the cookie is not available,
|
||||
instead of erroring out, the dependency result will be `None`.
|
||||
|
||||
This is useful when you want to have optional authentication.
|
||||
|
||||
It is also useful when you want to have authentication that can be
|
||||
provided in one of multiple optional ways (for example, in a cookie or
|
||||
in an HTTP Bearer token).
|
||||
"""
|
||||
),
|
||||
] = True,
|
||||
):
|
||||
self.model: APIKey = APIKey(
|
||||
**{"in": APIKeyIn.cookie}, # type: ignore[arg-type]
|
||||
name=name,
|
||||
description=description,
|
||||
)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.auto_error = auto_error
|
||||
|
||||
async def __call__(self, request: Request) -> Optional[str]:
|
||||
api_key = request.cookies.get(self.model.name)
|
||||
return self.check_api_key(api_key, self.auto_error)
|
@ -0,0 +1,6 @@
|
||||
from fastapi.openapi.models import SecurityBase as SecurityBaseModel
|
||||
|
||||
|
||||
class SecurityBase:
|
||||
model: SecurityBaseModel
|
||||
scheme_name: str
|
423
venv/lib/python3.11/site-packages/fastapi/security/http.py
Normal file
423
venv/lib/python3.11/site-packages/fastapi/security/http.py
Normal file
@ -0,0 +1,423 @@
|
||||
import binascii
|
||||
from base64 import b64decode
|
||||
from typing import Optional
|
||||
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.openapi.models import HTTPBase as HTTPBaseModel
|
||||
from fastapi.openapi.models import HTTPBearer as HTTPBearerModel
|
||||
from fastapi.security.base import SecurityBase
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
from pydantic import BaseModel
|
||||
from starlette.requests import Request
|
||||
from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
|
||||
from typing_extensions import Annotated, Doc
|
||||
|
||||
|
||||
class HTTPBasicCredentials(BaseModel):
|
||||
"""
|
||||
The HTTP Basic credentials given as the result of using `HTTPBasic` in a
|
||||
dependency.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for HTTP Basic Auth](https://fastapi.tiangolo.com/advanced/security/http-basic-auth/).
|
||||
"""
|
||||
|
||||
username: Annotated[str, Doc("The HTTP Basic username.")]
|
||||
password: Annotated[str, Doc("The HTTP Basic password.")]
|
||||
|
||||
|
||||
class HTTPAuthorizationCredentials(BaseModel):
|
||||
"""
|
||||
The HTTP authorization credentials in the result of using `HTTPBearer` or
|
||||
`HTTPDigest` in a dependency.
|
||||
|
||||
The HTTP authorization header value is split by the first space.
|
||||
|
||||
The first part is the `scheme`, the second part is the `credentials`.
|
||||
|
||||
For example, in an HTTP Bearer token scheme, the client will send a header
|
||||
like:
|
||||
|
||||
```
|
||||
Authorization: Bearer deadbeef12346
|
||||
```
|
||||
|
||||
In this case:
|
||||
|
||||
* `scheme` will have the value `"Bearer"`
|
||||
* `credentials` will have the value `"deadbeef12346"`
|
||||
"""
|
||||
|
||||
scheme: Annotated[
|
||||
str,
|
||||
Doc(
|
||||
"""
|
||||
The HTTP authorization scheme extracted from the header value.
|
||||
"""
|
||||
),
|
||||
]
|
||||
credentials: Annotated[
|
||||
str,
|
||||
Doc(
|
||||
"""
|
||||
The HTTP authorization credentials extracted from the header value.
|
||||
"""
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class HTTPBase(SecurityBase):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
scheme: str,
|
||||
scheme_name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
auto_error: bool = True,
|
||||
):
|
||||
self.model = HTTPBaseModel(scheme=scheme, description=description)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.auto_error = auto_error
|
||||
|
||||
async def __call__(
|
||||
self, request: Request
|
||||
) -> Optional[HTTPAuthorizationCredentials]:
|
||||
authorization = request.headers.get("Authorization")
|
||||
scheme, credentials = get_authorization_scheme_param(authorization)
|
||||
if not (authorization and scheme and credentials):
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
else:
|
||||
return None
|
||||
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
||||
|
||||
|
||||
class HTTPBasic(HTTPBase):
|
||||
"""
|
||||
HTTP Basic authentication.
|
||||
|
||||
## Usage
|
||||
|
||||
Create an instance object and use that object as the dependency in `Depends()`.
|
||||
|
||||
The dependency result will be an `HTTPBasicCredentials` object containing the
|
||||
`username` and the `password`.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for HTTP Basic Auth](https://fastapi.tiangolo.com/advanced/security/http-basic-auth/).
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
security = HTTPBasic()
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(credentials: Annotated[HTTPBasicCredentials, Depends(security)]):
|
||||
return {"username": credentials.username, "password": credentials.password}
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
scheme_name: Annotated[
|
||||
Optional[str],
|
||||
Doc(
|
||||
"""
|
||||
Security scheme name.
|
||||
|
||||
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
realm: Annotated[
|
||||
Optional[str],
|
||||
Doc(
|
||||
"""
|
||||
HTTP Basic authentication realm.
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
description: Annotated[
|
||||
Optional[str],
|
||||
Doc(
|
||||
"""
|
||||
Security scheme description.
|
||||
|
||||
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
auto_error: Annotated[
|
||||
bool,
|
||||
Doc(
|
||||
"""
|
||||
By default, if the HTTP Basic authentication is not provided (a
|
||||
header), `HTTPBasic` will automatically cancel the request and send the
|
||||
client an error.
|
||||
|
||||
If `auto_error` is set to `False`, when the HTTP Basic authentication
|
||||
is not available, instead of erroring out, the dependency result will
|
||||
be `None`.
|
||||
|
||||
This is useful when you want to have optional authentication.
|
||||
|
||||
It is also useful when you want to have authentication that can be
|
||||
provided in one of multiple optional ways (for example, in HTTP Basic
|
||||
authentication or in an HTTP Bearer token).
|
||||
"""
|
||||
),
|
||||
] = True,
|
||||
):
|
||||
self.model = HTTPBaseModel(scheme="basic", description=description)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.realm = realm
|
||||
self.auto_error = auto_error
|
||||
|
||||
async def __call__( # type: ignore
|
||||
self, request: Request
|
||||
) -> Optional[HTTPBasicCredentials]:
|
||||
authorization = request.headers.get("Authorization")
|
||||
scheme, param = get_authorization_scheme_param(authorization)
|
||||
if self.realm:
|
||||
unauthorized_headers = {"WWW-Authenticate": f'Basic realm="{self.realm}"'}
|
||||
else:
|
||||
unauthorized_headers = {"WWW-Authenticate": "Basic"}
|
||||
if not authorization or scheme.lower() != "basic":
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_401_UNAUTHORIZED,
|
||||
detail="Not authenticated",
|
||||
headers=unauthorized_headers,
|
||||
)
|
||||
else:
|
||||
return None
|
||||
invalid_user_credentials_exc = HTTPException(
|
||||
status_code=HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication credentials",
|
||||
headers=unauthorized_headers,
|
||||
)
|
||||
try:
|
||||
data = b64decode(param).decode("ascii")
|
||||
except (ValueError, UnicodeDecodeError, binascii.Error):
|
||||
raise invalid_user_credentials_exc # noqa: B904
|
||||
username, separator, password = data.partition(":")
|
||||
if not separator:
|
||||
raise invalid_user_credentials_exc
|
||||
return HTTPBasicCredentials(username=username, password=password)
|
||||
|
||||
|
||||
class HTTPBearer(HTTPBase):
|
||||
"""
|
||||
HTTP Bearer token authentication.
|
||||
|
||||
## Usage
|
||||
|
||||
Create an instance object and use that object as the dependency in `Depends()`.
|
||||
|
||||
The dependency result will be an `HTTPAuthorizationCredentials` object containing
|
||||
the `scheme` and the `credentials`.
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(
|
||||
credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)]
|
||||
):
|
||||
return {"scheme": credentials.scheme, "credentials": credentials.credentials}
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
bearerFormat: Annotated[Optional[str], Doc("Bearer token format.")] = None,
|
||||
scheme_name: Annotated[
|
||||
Optional[str],
|
||||
Doc(
|
||||
"""
|
||||
Security scheme name.
|
||||
|
||||
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
description: Annotated[
|
||||
Optional[str],
|
||||
Doc(
|
||||
"""
|
||||
Security scheme description.
|
||||
|
||||
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
auto_error: Annotated[
|
||||
bool,
|
||||
Doc(
|
||||
"""
|
||||
By default, if the HTTP Bearer token is not provided (in an
|
||||
`Authorization` header), `HTTPBearer` will automatically cancel the
|
||||
request and send the client an error.
|
||||
|
||||
If `auto_error` is set to `False`, when the HTTP Bearer token
|
||||
is not available, instead of erroring out, the dependency result will
|
||||
be `None`.
|
||||
|
||||
This is useful when you want to have optional authentication.
|
||||
|
||||
It is also useful when you want to have authentication that can be
|
||||
provided in one of multiple optional ways (for example, in an HTTP
|
||||
Bearer token or in a cookie).
|
||||
"""
|
||||
),
|
||||
] = True,
|
||||
):
|
||||
self.model = HTTPBearerModel(bearerFormat=bearerFormat, description=description)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.auto_error = auto_error
|
||||
|
||||
async def __call__(
|
||||
self, request: Request
|
||||
) -> Optional[HTTPAuthorizationCredentials]:
|
||||
authorization = request.headers.get("Authorization")
|
||||
scheme, credentials = get_authorization_scheme_param(authorization)
|
||||
if not (authorization and scheme and credentials):
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
else:
|
||||
return None
|
||||
if scheme.lower() != "bearer":
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN,
|
||||
detail="Invalid authentication credentials",
|
||||
)
|
||||
else:
|
||||
return None
|
||||
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
||||
|
||||
|
||||
class HTTPDigest(HTTPBase):
|
||||
"""
|
||||
HTTP Digest authentication.
|
||||
|
||||
## Usage
|
||||
|
||||
Create an instance object and use that object as the dependency in `Depends()`.
|
||||
|
||||
The dependency result will be an `HTTPAuthorizationCredentials` object containing
|
||||
the `scheme` and the `credentials`.
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPDigest
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
security = HTTPDigest()
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(
|
||||
credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)]
|
||||
):
|
||||
return {"scheme": credentials.scheme, "credentials": credentials.credentials}
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
scheme_name: Annotated[
|
||||
Optional[str],
|
||||
Doc(
|
||||
"""
|
||||
Security scheme name.
|
||||
|
||||
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
description: Annotated[
|
||||
Optional[str],
|
||||
Doc(
|
||||
"""
|
||||
Security scheme description.
|
||||
|
||||
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
auto_error: Annotated[
|
||||
bool,
|
||||
Doc(
|
||||
"""
|
||||
By default, if the HTTP Digest is not provided, `HTTPDigest` will
|
||||
automatically cancel the request and send the client an error.
|
||||
|
||||
If `auto_error` is set to `False`, when the HTTP Digest is not
|
||||
available, instead of erroring out, the dependency result will
|
||||
be `None`.
|
||||
|
||||
This is useful when you want to have optional authentication.
|
||||
|
||||
It is also useful when you want to have authentication that can be
|
||||
provided in one of multiple optional ways (for example, in HTTP
|
||||
Digest or in a cookie).
|
||||
"""
|
||||
),
|
||||
] = True,
|
||||
):
|
||||
self.model = HTTPBaseModel(scheme="digest", description=description)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.auto_error = auto_error
|
||||
|
||||
async def __call__(
|
||||
self, request: Request
|
||||
) -> Optional[HTTPAuthorizationCredentials]:
|
||||
authorization = request.headers.get("Authorization")
|
||||
scheme, credentials = get_authorization_scheme_param(authorization)
|
||||
if not (authorization and scheme and credentials):
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
else:
|
||||
return None
|
||||
if scheme.lower() != "digest":
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN,
|
||||
detail="Invalid authentication credentials",
|
||||
)
|
||||
else:
|
||||
return None
|
||||
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
638
venv/lib/python3.11/site-packages/fastapi/security/oauth2.py
Normal file
638
venv/lib/python3.11/site-packages/fastapi/security/oauth2.py
Normal file
@ -0,0 +1,638 @@
|
||||
from typing import Any, Dict, List, Optional, Union, cast
|
||||
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.openapi.models import OAuth2 as OAuth2Model
|
||||
from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel
|
||||
from fastapi.param_functions import Form
|
||||
from fastapi.security.base import SecurityBase
|
||||
from fastapi.security.utils import get_authorization_scheme_param
|
||||
from starlette.requests import Request
|
||||
from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
|
||||
|
||||
# TODO: import from typing when deprecating Python 3.9
|
||||
from typing_extensions import Annotated, Doc
|
||||
|
||||
|
||||
class OAuth2PasswordRequestForm:
|
||||
"""
|
||||
This is a dependency class to collect the `username` and `password` as form data
|
||||
for an OAuth2 password flow.
|
||||
|
||||
The OAuth2 specification dictates that for a password flow the data should be
|
||||
collected using form data (instead of JSON) and that it should have the specific
|
||||
fields `username` and `password`.
|
||||
|
||||
All the initialization parameters are extracted from the request.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/).
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.post("/login")
|
||||
def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
|
||||
data = {}
|
||||
data["scopes"] = []
|
||||
for scope in form_data.scopes:
|
||||
data["scopes"].append(scope)
|
||||
if form_data.client_id:
|
||||
data["client_id"] = form_data.client_id
|
||||
if form_data.client_secret:
|
||||
data["client_secret"] = form_data.client_secret
|
||||
return data
|
||||
```
|
||||
|
||||
Note that for OAuth2 the scope `items:read` is a single scope in an opaque string.
|
||||
You could have custom internal logic to separate it by colon characters (`:`) or
|
||||
similar, and get the two parts `items` and `read`. Many applications do that to
|
||||
group and organize permissions, you could do it as well in your application, just
|
||||
know that that it is application specific, it's not part of the specification.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
grant_type: Annotated[
|
||||
Union[str, None],
|
||||
Form(pattern="^password$"),
|
||||
Doc(
|
||||
"""
|
||||
The OAuth2 spec says it is required and MUST be the fixed string
|
||||
"password". Nevertheless, this dependency class is permissive and
|
||||
allows not passing it. If you want to enforce it, use instead the
|
||||
`OAuth2PasswordRequestFormStrict` dependency.
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
username: Annotated[
|
||||
str,
|
||||
Form(),
|
||||
Doc(
|
||||
"""
|
||||
`username` string. The OAuth2 spec requires the exact field name
|
||||
`username`.
|
||||
"""
|
||||
),
|
||||
],
|
||||
password: Annotated[
|
||||
str,
|
||||
Form(),
|
||||
Doc(
|
||||
"""
|
||||
`password` string. The OAuth2 spec requires the exact field name
|
||||
`password".
|
||||
"""
|
||||
),
|
||||
],
|
||||
scope: Annotated[
|
||||
str,
|
||||
Form(),
|
||||
Doc(
|
||||
"""
|
||||
A single string with actually several scopes separated by spaces. Each
|
||||
scope is also a string.
|
||||
|
||||
For example, a single string with:
|
||||
|
||||
```python
|
||||
"items:read items:write users:read profile openid"
|
||||
````
|
||||
|
||||
would represent the scopes:
|
||||
|
||||
* `items:read`
|
||||
* `items:write`
|
||||
* `users:read`
|
||||
* `profile`
|
||||
* `openid`
|
||||
"""
|
||||
),
|
||||
] = "",
|
||||
client_id: Annotated[
|
||||
Union[str, None],
|
||||
Form(),
|
||||
Doc(
|
||||
"""
|
||||
If there's a `client_id`, it can be sent as part of the form fields.
|
||||
But the OAuth2 specification recommends sending the `client_id` and
|
||||
`client_secret` (if any) using HTTP Basic auth.
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
client_secret: Annotated[
|
||||
Union[str, None],
|
||||
Form(),
|
||||
Doc(
|
||||
"""
|
||||
If there's a `client_password` (and a `client_id`), they can be sent
|
||||
as part of the form fields. But the OAuth2 specification recommends
|
||||
sending the `client_id` and `client_secret` (if any) using HTTP Basic
|
||||
auth.
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
):
|
||||
self.grant_type = grant_type
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.scopes = scope.split()
|
||||
self.client_id = client_id
|
||||
self.client_secret = client_secret
|
||||
|
||||
|
||||
class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm):
|
||||
"""
|
||||
This is a dependency class to collect the `username` and `password` as form data
|
||||
for an OAuth2 password flow.
|
||||
|
||||
The OAuth2 specification dictates that for a password flow the data should be
|
||||
collected using form data (instead of JSON) and that it should have the specific
|
||||
fields `username` and `password`.
|
||||
|
||||
All the initialization parameters are extracted from the request.
|
||||
|
||||
The only difference between `OAuth2PasswordRequestFormStrict` and
|
||||
`OAuth2PasswordRequestForm` is that `OAuth2PasswordRequestFormStrict` requires the
|
||||
client to send the form field `grant_type` with the value `"password"`, which
|
||||
is required in the OAuth2 specification (it seems that for no particular reason),
|
||||
while for `OAuth2PasswordRequestForm` `grant_type` is optional.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/).
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.post("/login")
|
||||
def login(form_data: Annotated[OAuth2PasswordRequestFormStrict, Depends()]):
|
||||
data = {}
|
||||
data["scopes"] = []
|
||||
for scope in form_data.scopes:
|
||||
data["scopes"].append(scope)
|
||||
if form_data.client_id:
|
||||
data["client_id"] = form_data.client_id
|
||||
if form_data.client_secret:
|
||||
data["client_secret"] = form_data.client_secret
|
||||
return data
|
||||
```
|
||||
|
||||
Note that for OAuth2 the scope `items:read` is a single scope in an opaque string.
|
||||
You could have custom internal logic to separate it by colon characters (`:`) or
|
||||
similar, and get the two parts `items` and `read`. Many applications do that to
|
||||
group and organize permissions, you could do it as well in your application, just
|
||||
know that that it is application specific, it's not part of the specification.
|
||||
|
||||
|
||||
grant_type: the OAuth2 spec says it is required and MUST be the fixed string "password".
|
||||
This dependency is strict about it. If you want to be permissive, use instead the
|
||||
OAuth2PasswordRequestForm dependency class.
|
||||
username: username string. The OAuth2 spec requires the exact field name "username".
|
||||
password: password string. The OAuth2 spec requires the exact field name "password".
|
||||
scope: Optional string. Several scopes (each one a string) separated by spaces. E.g.
|
||||
"items:read items:write users:read profile openid"
|
||||
client_id: optional string. OAuth2 recommends sending the client_id and client_secret (if any)
|
||||
using HTTP Basic auth, as: client_id:client_secret
|
||||
client_secret: optional string. OAuth2 recommends sending the client_id and client_secret (if any)
|
||||
using HTTP Basic auth, as: client_id:client_secret
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
grant_type: Annotated[
|
||||
str,
|
||||
Form(pattern="^password$"),
|
||||
Doc(
|
||||
"""
|
||||
The OAuth2 spec says it is required and MUST be the fixed string
|
||||
"password". This dependency is strict about it. If you want to be
|
||||
permissive, use instead the `OAuth2PasswordRequestForm` dependency
|
||||
class.
|
||||
"""
|
||||
),
|
||||
],
|
||||
username: Annotated[
|
||||
str,
|
||||
Form(),
|
||||
Doc(
|
||||
"""
|
||||
`username` string. The OAuth2 spec requires the exact field name
|
||||
`username`.
|
||||
"""
|
||||
),
|
||||
],
|
||||
password: Annotated[
|
||||
str,
|
||||
Form(),
|
||||
Doc(
|
||||
"""
|
||||
`password` string. The OAuth2 spec requires the exact field name
|
||||
`password".
|
||||
"""
|
||||
),
|
||||
],
|
||||
scope: Annotated[
|
||||
str,
|
||||
Form(),
|
||||
Doc(
|
||||
"""
|
||||
A single string with actually several scopes separated by spaces. Each
|
||||
scope is also a string.
|
||||
|
||||
For example, a single string with:
|
||||
|
||||
```python
|
||||
"items:read items:write users:read profile openid"
|
||||
````
|
||||
|
||||
would represent the scopes:
|
||||
|
||||
* `items:read`
|
||||
* `items:write`
|
||||
* `users:read`
|
||||
* `profile`
|
||||
* `openid`
|
||||
"""
|
||||
),
|
||||
] = "",
|
||||
client_id: Annotated[
|
||||
Union[str, None],
|
||||
Form(),
|
||||
Doc(
|
||||
"""
|
||||
If there's a `client_id`, it can be sent as part of the form fields.
|
||||
But the OAuth2 specification recommends sending the `client_id` and
|
||||
`client_secret` (if any) using HTTP Basic auth.
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
client_secret: Annotated[
|
||||
Union[str, None],
|
||||
Form(),
|
||||
Doc(
|
||||
"""
|
||||
If there's a `client_password` (and a `client_id`), they can be sent
|
||||
as part of the form fields. But the OAuth2 specification recommends
|
||||
sending the `client_id` and `client_secret` (if any) using HTTP Basic
|
||||
auth.
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
):
|
||||
super().__init__(
|
||||
grant_type=grant_type,
|
||||
username=username,
|
||||
password=password,
|
||||
scope=scope,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
)
|
||||
|
||||
|
||||
class OAuth2(SecurityBase):
|
||||
"""
|
||||
This is the base class for OAuth2 authentication, an instance of it would be used
|
||||
as a dependency. All other OAuth2 classes inherit from it and customize it for
|
||||
each OAuth2 flow.
|
||||
|
||||
You normally would not create a new class inheriting from it but use one of the
|
||||
existing subclasses, and maybe compose them if you want to support multiple flows.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for Security](https://fastapi.tiangolo.com/tutorial/security/).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
flows: Annotated[
|
||||
Union[OAuthFlowsModel, Dict[str, Dict[str, Any]]],
|
||||
Doc(
|
||||
"""
|
||||
The dictionary of OAuth2 flows.
|
||||
"""
|
||||
),
|
||||
] = OAuthFlowsModel(),
|
||||
scheme_name: Annotated[
|
||||
Optional[str],
|
||||
Doc(
|
||||
"""
|
||||
Security scheme name.
|
||||
|
||||
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
description: Annotated[
|
||||
Optional[str],
|
||||
Doc(
|
||||
"""
|
||||
Security scheme description.
|
||||
|
||||
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
auto_error: Annotated[
|
||||
bool,
|
||||
Doc(
|
||||
"""
|
||||
By default, if no HTTP Authorization header is provided, required for
|
||||
OAuth2 authentication, it will automatically cancel the request and
|
||||
send the client an error.
|
||||
|
||||
If `auto_error` is set to `False`, when the HTTP Authorization header
|
||||
is not available, instead of erroring out, the dependency result will
|
||||
be `None`.
|
||||
|
||||
This is useful when you want to have optional authentication.
|
||||
|
||||
It is also useful when you want to have authentication that can be
|
||||
provided in one of multiple optional ways (for example, with OAuth2
|
||||
or in a cookie).
|
||||
"""
|
||||
),
|
||||
] = True,
|
||||
):
|
||||
self.model = OAuth2Model(
|
||||
flows=cast(OAuthFlowsModel, flows), description=description
|
||||
)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.auto_error = auto_error
|
||||
|
||||
async def __call__(self, request: Request) -> Optional[str]:
|
||||
authorization = request.headers.get("Authorization")
|
||||
if not authorization:
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
else:
|
||||
return None
|
||||
return authorization
|
||||
|
||||
|
||||
class OAuth2PasswordBearer(OAuth2):
|
||||
"""
|
||||
OAuth2 flow for authentication using a bearer token obtained with a password.
|
||||
An instance of it would be used as a dependency.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tokenUrl: Annotated[
|
||||
str,
|
||||
Doc(
|
||||
"""
|
||||
The URL to obtain the OAuth2 token. This would be the *path operation*
|
||||
that has `OAuth2PasswordRequestForm` as a dependency.
|
||||
"""
|
||||
),
|
||||
],
|
||||
scheme_name: Annotated[
|
||||
Optional[str],
|
||||
Doc(
|
||||
"""
|
||||
Security scheme name.
|
||||
|
||||
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
scopes: Annotated[
|
||||
Optional[Dict[str, str]],
|
||||
Doc(
|
||||
"""
|
||||
The OAuth2 scopes that would be required by the *path operations* that
|
||||
use this dependency.
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
description: Annotated[
|
||||
Optional[str],
|
||||
Doc(
|
||||
"""
|
||||
Security scheme description.
|
||||
|
||||
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
auto_error: Annotated[
|
||||
bool,
|
||||
Doc(
|
||||
"""
|
||||
By default, if no HTTP Authorization header is provided, required for
|
||||
OAuth2 authentication, it will automatically cancel the request and
|
||||
send the client an error.
|
||||
|
||||
If `auto_error` is set to `False`, when the HTTP Authorization header
|
||||
is not available, instead of erroring out, the dependency result will
|
||||
be `None`.
|
||||
|
||||
This is useful when you want to have optional authentication.
|
||||
|
||||
It is also useful when you want to have authentication that can be
|
||||
provided in one of multiple optional ways (for example, with OAuth2
|
||||
or in a cookie).
|
||||
"""
|
||||
),
|
||||
] = True,
|
||||
):
|
||||
if not scopes:
|
||||
scopes = {}
|
||||
flows = OAuthFlowsModel(
|
||||
password=cast(Any, {"tokenUrl": tokenUrl, "scopes": scopes})
|
||||
)
|
||||
super().__init__(
|
||||
flows=flows,
|
||||
scheme_name=scheme_name,
|
||||
description=description,
|
||||
auto_error=auto_error,
|
||||
)
|
||||
|
||||
async def __call__(self, request: Request) -> Optional[str]:
|
||||
authorization = request.headers.get("Authorization")
|
||||
scheme, param = get_authorization_scheme_param(authorization)
|
||||
if not authorization or scheme.lower() != "bearer":
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_401_UNAUTHORIZED,
|
||||
detail="Not authenticated",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
else:
|
||||
return None
|
||||
return param
|
||||
|
||||
|
||||
class OAuth2AuthorizationCodeBearer(OAuth2):
|
||||
"""
|
||||
OAuth2 flow for authentication using a bearer token obtained with an OAuth2 code
|
||||
flow. An instance of it would be used as a dependency.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
authorizationUrl: str,
|
||||
tokenUrl: Annotated[
|
||||
str,
|
||||
Doc(
|
||||
"""
|
||||
The URL to obtain the OAuth2 token.
|
||||
"""
|
||||
),
|
||||
],
|
||||
refreshUrl: Annotated[
|
||||
Optional[str],
|
||||
Doc(
|
||||
"""
|
||||
The URL to refresh the token and obtain a new one.
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
scheme_name: Annotated[
|
||||
Optional[str],
|
||||
Doc(
|
||||
"""
|
||||
Security scheme name.
|
||||
|
||||
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
scopes: Annotated[
|
||||
Optional[Dict[str, str]],
|
||||
Doc(
|
||||
"""
|
||||
The OAuth2 scopes that would be required by the *path operations* that
|
||||
use this dependency.
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
description: Annotated[
|
||||
Optional[str],
|
||||
Doc(
|
||||
"""
|
||||
Security scheme description.
|
||||
|
||||
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
auto_error: Annotated[
|
||||
bool,
|
||||
Doc(
|
||||
"""
|
||||
By default, if no HTTP Authorization header is provided, required for
|
||||
OAuth2 authentication, it will automatically cancel the request and
|
||||
send the client an error.
|
||||
|
||||
If `auto_error` is set to `False`, when the HTTP Authorization header
|
||||
is not available, instead of erroring out, the dependency result will
|
||||
be `None`.
|
||||
|
||||
This is useful when you want to have optional authentication.
|
||||
|
||||
It is also useful when you want to have authentication that can be
|
||||
provided in one of multiple optional ways (for example, with OAuth2
|
||||
or in a cookie).
|
||||
"""
|
||||
),
|
||||
] = True,
|
||||
):
|
||||
if not scopes:
|
||||
scopes = {}
|
||||
flows = OAuthFlowsModel(
|
||||
authorizationCode=cast(
|
||||
Any,
|
||||
{
|
||||
"authorizationUrl": authorizationUrl,
|
||||
"tokenUrl": tokenUrl,
|
||||
"refreshUrl": refreshUrl,
|
||||
"scopes": scopes,
|
||||
},
|
||||
)
|
||||
)
|
||||
super().__init__(
|
||||
flows=flows,
|
||||
scheme_name=scheme_name,
|
||||
description=description,
|
||||
auto_error=auto_error,
|
||||
)
|
||||
|
||||
async def __call__(self, request: Request) -> Optional[str]:
|
||||
authorization = request.headers.get("Authorization")
|
||||
scheme, param = get_authorization_scheme_param(authorization)
|
||||
if not authorization or scheme.lower() != "bearer":
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_401_UNAUTHORIZED,
|
||||
detail="Not authenticated",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
else:
|
||||
return None # pragma: nocover
|
||||
return param
|
||||
|
||||
|
||||
class SecurityScopes:
|
||||
"""
|
||||
This is a special class that you can define in a parameter in a dependency to
|
||||
obtain the OAuth2 scopes required by all the dependencies in the same chain.
|
||||
|
||||
This way, multiple dependencies can have different scopes, even when used in the
|
||||
same *path operation*. And with this, you can access all the scopes required in
|
||||
all those dependencies in a single place.
|
||||
|
||||
Read more about it in the
|
||||
[FastAPI docs for OAuth2 scopes](https://fastapi.tiangolo.com/advanced/security/oauth2-scopes/).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
scopes: Annotated[
|
||||
Optional[List[str]],
|
||||
Doc(
|
||||
"""
|
||||
This will be filled by FastAPI.
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
):
|
||||
self.scopes: Annotated[
|
||||
List[str],
|
||||
Doc(
|
||||
"""
|
||||
The list of all the scopes required by dependencies.
|
||||
"""
|
||||
),
|
||||
] = scopes or []
|
||||
self.scope_str: Annotated[
|
||||
str,
|
||||
Doc(
|
||||
"""
|
||||
All the scopes required by all the dependencies in a single string
|
||||
separated by spaces, as defined in the OAuth2 specification.
|
||||
"""
|
||||
),
|
||||
] = " ".join(self.scopes)
|
@ -0,0 +1,84 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi.openapi.models import OpenIdConnect as OpenIdConnectModel
|
||||
from fastapi.security.base import SecurityBase
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.status import HTTP_403_FORBIDDEN
|
||||
from typing_extensions import Annotated, Doc
|
||||
|
||||
|
||||
class OpenIdConnect(SecurityBase):
|
||||
"""
|
||||
OpenID Connect authentication class. An instance of it would be used as a
|
||||
dependency.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
openIdConnectUrl: Annotated[
|
||||
str,
|
||||
Doc(
|
||||
"""
|
||||
The OpenID Connect URL.
|
||||
"""
|
||||
),
|
||||
],
|
||||
scheme_name: Annotated[
|
||||
Optional[str],
|
||||
Doc(
|
||||
"""
|
||||
Security scheme name.
|
||||
|
||||
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
description: Annotated[
|
||||
Optional[str],
|
||||
Doc(
|
||||
"""
|
||||
Security scheme description.
|
||||
|
||||
It will be included in the generated OpenAPI (e.g. visible at `/docs`).
|
||||
"""
|
||||
),
|
||||
] = None,
|
||||
auto_error: Annotated[
|
||||
bool,
|
||||
Doc(
|
||||
"""
|
||||
By default, if no HTTP Authorization header is provided, required for
|
||||
OpenID Connect authentication, it will automatically cancel the request
|
||||
and send the client an error.
|
||||
|
||||
If `auto_error` is set to `False`, when the HTTP Authorization header
|
||||
is not available, instead of erroring out, the dependency result will
|
||||
be `None`.
|
||||
|
||||
This is useful when you want to have optional authentication.
|
||||
|
||||
It is also useful when you want to have authentication that can be
|
||||
provided in one of multiple optional ways (for example, with OpenID
|
||||
Connect or in a cookie).
|
||||
"""
|
||||
),
|
||||
] = True,
|
||||
):
|
||||
self.model = OpenIdConnectModel(
|
||||
openIdConnectUrl=openIdConnectUrl, description=description
|
||||
)
|
||||
self.scheme_name = scheme_name or self.__class__.__name__
|
||||
self.auto_error = auto_error
|
||||
|
||||
async def __call__(self, request: Request) -> Optional[str]:
|
||||
authorization = request.headers.get("Authorization")
|
||||
if not authorization:
|
||||
if self.auto_error:
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
else:
|
||||
return None
|
||||
return authorization
|
10
venv/lib/python3.11/site-packages/fastapi/security/utils.py
Normal file
10
venv/lib/python3.11/site-packages/fastapi/security/utils.py
Normal file
@ -0,0 +1,10 @@
|
||||
from typing import Optional, Tuple
|
||||
|
||||
|
||||
def get_authorization_scheme_param(
|
||||
authorization_header_value: Optional[str],
|
||||
) -> Tuple[str, str]:
|
||||
if not authorization_header_value:
|
||||
return "", ""
|
||||
scheme, _, param = authorization_header_value.partition(" ")
|
||||
return scheme, param
|
1
venv/lib/python3.11/site-packages/fastapi/staticfiles.py
Normal file
1
venv/lib/python3.11/site-packages/fastapi/staticfiles.py
Normal file
@ -0,0 +1 @@
|
||||
from starlette.staticfiles import StaticFiles as StaticFiles # noqa
|
1
venv/lib/python3.11/site-packages/fastapi/templating.py
Normal file
1
venv/lib/python3.11/site-packages/fastapi/templating.py
Normal file
@ -0,0 +1 @@
|
||||
from starlette.templating import Jinja2Templates as Jinja2Templates # noqa
|
1
venv/lib/python3.11/site-packages/fastapi/testclient.py
Normal file
1
venv/lib/python3.11/site-packages/fastapi/testclient.py
Normal file
@ -0,0 +1 @@
|
||||
from starlette.testclient import TestClient as TestClient # noqa
|
10
venv/lib/python3.11/site-packages/fastapi/types.py
Normal file
10
venv/lib/python3.11/site-packages/fastapi/types.py
Normal file
@ -0,0 +1,10 @@
|
||||
import types
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, Dict, Set, Type, TypeVar, Union
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
DecoratedCallable = TypeVar("DecoratedCallable", bound=Callable[..., Any])
|
||||
UnionType = getattr(types, "UnionType", Union)
|
||||
ModelNameMap = Dict[Union[Type[BaseModel], Type[Enum]], str]
|
||||
IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any]]
|
220
venv/lib/python3.11/site-packages/fastapi/utils.py
Normal file
220
venv/lib/python3.11/site-packages/fastapi/utils.py
Normal file
@ -0,0 +1,220 @@
|
||||
import re
|
||||
import warnings
|
||||
from dataclasses import is_dataclass
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
MutableMapping,
|
||||
Optional,
|
||||
Set,
|
||||
Type,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
from weakref import WeakKeyDictionary
|
||||
|
||||
import fastapi
|
||||
from fastapi._compat import (
|
||||
PYDANTIC_V2,
|
||||
BaseConfig,
|
||||
ModelField,
|
||||
PydanticSchemaGenerationError,
|
||||
Undefined,
|
||||
UndefinedType,
|
||||
Validator,
|
||||
lenient_issubclass,
|
||||
)
|
||||
from fastapi.datastructures import DefaultPlaceholder, DefaultType
|
||||
from pydantic import BaseModel, create_model
|
||||
from pydantic.fields import FieldInfo
|
||||
from typing_extensions import Literal
|
||||
|
||||
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))
|
||||
|
||||
|
||||
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: Type[BaseConfig] = BaseConfig,
|
||||
field_info: Optional[FieldInfo] = None,
|
||||
alias: Optional[str] = None,
|
||||
mode: Literal["validation", "serialization"] = "validation",
|
||||
) -> ModelField:
|
||||
class_validators = class_validators or {}
|
||||
if PYDANTIC_V2:
|
||||
field_info = field_info or FieldInfo(
|
||||
annotation=type_, default=default, alias=alias
|
||||
)
|
||||
else:
|
||||
field_info = field_info or FieldInfo()
|
||||
kwargs = {"name": name, "field_info": field_info}
|
||||
if PYDANTIC_V2:
|
||||
kwargs.update({"mode": mode})
|
||||
else:
|
||||
kwargs.update(
|
||||
{
|
||||
"type_": type_,
|
||||
"class_validators": class_validators,
|
||||
"default": default,
|
||||
"required": required,
|
||||
"model_config": model_config,
|
||||
"alias": alias,
|
||||
}
|
||||
)
|
||||
try:
|
||||
return ModelField(**kwargs) # type: ignore[arg-type]
|
||||
except (RuntimeError, PydanticSchemaGenerationError):
|
||||
raise fastapi.exceptions.FastAPIError(
|
||||
"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/"
|
||||
) from None
|
||||
|
||||
|
||||
def create_cloned_field(
|
||||
field: ModelField,
|
||||
*,
|
||||
cloned_types: Optional[MutableMapping[Type[BaseModel], Type[BaseModel]]] = None,
|
||||
) -> ModelField:
|
||||
if PYDANTIC_V2:
|
||||
return field
|
||||
# cloned_types caches already cloned types to support recursive models and improve
|
||||
# performance by avoiding unnecessary cloning
|
||||
if cloned_types is None:
|
||||
cloned_types = _CLONED_TYPES_CACHE
|
||||
|
||||
original_type = field.type_
|
||||
if is_dataclass(original_type) and hasattr(original_type, "__pydantic_model__"):
|
||||
original_type = original_type.__pydantic_model__
|
||||
use_type = original_type
|
||||
if lenient_issubclass(original_type, BaseModel):
|
||||
original_type = cast(Type[BaseModel], original_type)
|
||||
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
|
||||
)
|
||||
new_field = create_model_field(name=field.name, type_=use_type)
|
||||
new_field.has_alias = field.has_alias # type: ignore[attr-defined]
|
||||
new_field.alias = field.alias # type: ignore[misc]
|
||||
new_field.class_validators = field.class_validators # type: ignore[attr-defined]
|
||||
new_field.default = field.default # type: ignore[misc]
|
||||
new_field.required = field.required # type: ignore[misc]
|
||||
new_field.model_config = field.model_config # type: ignore[attr-defined]
|
||||
new_field.field_info = field.field_info
|
||||
new_field.allow_none = field.allow_none # type: ignore[attr-defined]
|
||||
new_field.validate_always = field.validate_always # type: ignore[attr-defined]
|
||||
if field.sub_fields: # type: ignore[attr-defined]
|
||||
new_field.sub_fields = [ # type: ignore[attr-defined]
|
||||
create_cloned_field(sub_field, cloned_types=cloned_types)
|
||||
for sub_field in field.sub_fields # type: ignore[attr-defined]
|
||||
]
|
||||
if field.key_field: # type: ignore[attr-defined]
|
||||
new_field.key_field = create_cloned_field( # type: ignore[attr-defined]
|
||||
field.key_field, # type: ignore[attr-defined]
|
||||
cloned_types=cloned_types,
|
||||
)
|
||||
new_field.validators = field.validators # type: ignore[attr-defined]
|
||||
new_field.pre_validators = field.pre_validators # type: ignore[attr-defined]
|
||||
new_field.post_validators = field.post_validators # type: ignore[attr-defined]
|
||||
new_field.parse_json = field.parse_json # type: ignore[attr-defined]
|
||||
new_field.shape = field.shape # type: ignore[attr-defined]
|
||||
new_field.populate_validators() # type: ignore[attr-defined]
|
||||
return new_field
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
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
|
3
venv/lib/python3.11/site-packages/fastapi/websockets.py
Normal file
3
venv/lib/python3.11/site-packages/fastapi/websockets.py
Normal file
@ -0,0 +1,3 @@
|
||||
from starlette.websockets import WebSocket as WebSocket # noqa
|
||||
from starlette.websockets import WebSocketDisconnect as WebSocketDisconnect # noqa
|
||||
from starlette.websockets import WebSocketState as WebSocketState # noqa
|
Reference in New Issue
Block a user