mirror of
https://github.com/fastapi/fastapi.git
synced 2025-12-28 08:40:21 -05:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdb6d43e10 | ||
|
|
a7c718e968 | ||
|
|
f4d753620b | ||
|
|
fadfe4c586 | ||
|
|
5fd83c5fa4 | ||
|
|
14daaf409f | ||
|
|
c7dc26b760 | ||
|
|
f5ccb3c35d | ||
|
|
4cea311e6e | ||
|
|
f8718072a0 | ||
|
|
3dbbecdd16 | ||
|
|
6d5530ec1c | ||
|
|
0761f11d1a | ||
|
|
f2e7ef7056 | ||
|
|
d5d9a20937 | ||
|
|
96f092179f | ||
|
|
8505b716af | ||
|
|
78272ac1f3 | ||
|
|
f1bee9a271 | ||
|
|
b20b2218cd | ||
|
|
b9cf69cd42 | ||
|
|
f803c77515 | ||
|
|
0c67022048 | ||
|
|
d8fe307d61 |
1
Pipfile
1
Pipfile
@@ -29,6 +29,7 @@ starlette = "==0.12.8"
|
||||
pydantic = "==0.32.2"
|
||||
databases = {extras = ["sqlite"],version = "*"}
|
||||
hypercorn = "*"
|
||||
orjson = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.6"
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
## Latest changes
|
||||
|
||||
## 0.40.0
|
||||
|
||||
* Add notes to docs about installing `python-multipart` when using forms. PR [#574](https://github.com/tiangolo/fastapi/pull/574) by [@sliptonic](https://github.com/sliptonic).
|
||||
* Generate OpenAPI schemas in alphabetical order. PR [#554](https://github.com/tiangolo/fastapi/pull/554) by [@dmontagu](https://github.com/dmontagu).
|
||||
* Add support for truncating docstrings from *path operation functions*.
|
||||
* New docs at [Advanced description from docstring](https://fastapi.tiangolo.com/tutorial/path-operation-advanced-configuration/#advanced-description-from-docstring).
|
||||
* PR [#556](https://github.com/tiangolo/fastapi/pull/556) by [@svalouch](https://github.com/svalouch).
|
||||
* Fix `DOCTYPE` in HTML files generated for Swagger UI and ReDoc. PR [#537](https://github.com/tiangolo/fastapi/pull/537) by [@Trim21](https://github.com/Trim21).
|
||||
* Fix handling `4XX` responses overriding default `422` validation error responses. PR [#517](https://github.com/tiangolo/fastapi/pull/517) by [@tsouvarev](https://github.com/tsouvarev).
|
||||
* Fix typo in documentation for [Simple HTTP Basic Auth](https://fastapi.tiangolo.com/tutorial/security/http-basic-auth/#simple-http-basic-auth). PR [#514](https://github.com/tiangolo/fastapi/pull/514) by [@prostomarkeloff](https://github.com/prostomarkeloff).
|
||||
* Fix incorrect documentation example in [first steps](https://fastapi.tiangolo.com/tutorial/first-steps/). PR [#511](https://github.com/tiangolo/fastapi/pull/511) by [@IgnatovFedor](https://github.com/IgnatovFedor).
|
||||
* Add support for Swagger UI [initOauth](https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/oauth2.md) settings with the parameter `swagger_ui_init_oauth`. PR [#499](https://github.com/tiangolo/fastapi/pull/499) by [@zamiramir](https://github.com/zamiramir).
|
||||
|
||||
## 0.39.0
|
||||
|
||||
* Allow path parameters to have default values (e.g. `None`) and discard them instead of raising an error.
|
||||
* This allows declaring a parameter like `user_id: str = None` that can be taken from a query parameter, but the same path operation can be included in a router with a path `/users/{user_id}`, in which case will be taken from the path and will be required.
|
||||
* PR [#464](https://github.com/tiangolo/fastapi/pull/464) by [@jonathanunderwood](https://github.com/jonathanunderwood).
|
||||
* Add support for setting a `default_response_class` in the `FastAPI` instance or in `include_router`. Initial PR [#467](https://github.com/tiangolo/fastapi/pull/467) by [@toppk](https://github.com/toppk).
|
||||
* Add support for type annotations using strings and `from __future__ import annotations`. PR [#451](https://github.com/tiangolo/fastapi/pull/451) by [@dmontagu](https://github.com/dmontagu).
|
||||
|
||||
## 0.38.1
|
||||
|
||||
* Fix incorrect `Request` class import. PR [#493](https://github.com/tiangolo/fastapi/pull/493) by [@kamalgill](https://github.com/kamalgill).
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
from typing import Set
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
description: str = None
|
||||
price: float
|
||||
tax: float = None
|
||||
tags: Set[str] = []
|
||||
|
||||
|
||||
@app.post("/items/", response_model=Item, summary="Create an item")
|
||||
async def create_item(*, item: Item):
|
||||
"""
|
||||
Create an item with all the information:
|
||||
|
||||
- **name**: each item must have a name
|
||||
- **description**: a long description
|
||||
- **price**: required
|
||||
- **tax**: if the item doesn't have tax, you can omit this
|
||||
- **tags**: a set of unique tag strings for this item
|
||||
\f
|
||||
:param item: User input.
|
||||
"""
|
||||
return item
|
||||
@@ -37,7 +37,7 @@ Open your browser at <a href="http://127.0.0.1:8000" target="_blank">http://127.
|
||||
You will see the JSON response as:
|
||||
|
||||
```JSON
|
||||
{"hello": "world"}
|
||||
{"message": "Hello World"}
|
||||
```
|
||||
|
||||
### Interactive API docs
|
||||
|
||||
@@ -18,3 +18,15 @@ To exclude a path operation from the generated OpenAPI schema (and thus, from th
|
||||
```Python hl_lines="6"
|
||||
{!./src/path_operation_advanced_configuration/tutorial002.py!}
|
||||
```
|
||||
|
||||
## Advanced description from docstring
|
||||
|
||||
You can limit the lines used from the docstring of a *path operation function* for OpenAPI.
|
||||
|
||||
Adding an `\f` (an escaped "form feed" character) causes **FastAPI** to truncate the output used for OpenAPI at this point.
|
||||
|
||||
It won't show up in the documentation, but other tools (such as Sphinx) will be able to use the rest.
|
||||
|
||||
```Python hl_lines="19 20 21 22 23 24 25 26 27 28 29"
|
||||
{!./src/path_operation_advanced_configuration/tutorial003.py!}
|
||||
```
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
You can define files to be uploaded by the client using `File`.
|
||||
|
||||
!!! info
|
||||
To receive uploaded files, first install [`python-multipart`](https://andrew-d.github.io/python-multipart/).
|
||||
|
||||
E.g. `pip install python-multipart`.
|
||||
|
||||
This is because uploaded files are sent as "form data".
|
||||
|
||||
## Import `File`
|
||||
|
||||
Import `File` and `UploadFile` from `fastapi`:
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
You can define files and form fields at the same time using `File` and `Form`.
|
||||
|
||||
!!! info
|
||||
To receive uploaded files and/or form data, first install [`python-multipart`](https://andrew-d.github.io/python-multipart/).
|
||||
|
||||
E.g. `pip install python-multipart`.
|
||||
|
||||
## Import `File` and `Form`
|
||||
|
||||
```Python hl_lines="1"
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
When you need to receive form fields instead of JSON, you can use `Form`.
|
||||
|
||||
!!! info
|
||||
To use forms, first install [`python-multipart`](https://andrew-d.github.io/python-multipart/).
|
||||
|
||||
E.g. `pip install python-multipart`.
|
||||
|
||||
## Import `Form`
|
||||
|
||||
Import `Form` from `fastapi`:
|
||||
|
||||
@@ -24,6 +24,13 @@ Copy the example in a file `main.py`:
|
||||
|
||||
## Run it
|
||||
|
||||
!!! info
|
||||
First install [`python-multipart`](https://andrew-d.github.io/python-multipart/).
|
||||
|
||||
E.g. `pip install python-multipart`.
|
||||
|
||||
This is because **OAuth2** uses "form data" for sending the `username` and `password`.
|
||||
|
||||
Run the example with:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -12,8 +12,8 @@ Then, when you type that username and password, the browser sends them in the he
|
||||
|
||||
## Simple HTTP Basic Auth
|
||||
|
||||
* Import `HTTPBAsic` and `HTTPBasicCredentials`.
|
||||
* Create a "`security` scheme" using `HTTPBAsic`.
|
||||
* Import `HTTPBasic` and `HTTPBasicCredentials`.
|
||||
* Create a "`security` scheme" using `HTTPBasic`.
|
||||
* Use that `security` with a dependency in your *path operation*.
|
||||
* It returns an object of type `HTTPBasicCredentials`:
|
||||
* It contains the `username` and `password` sent.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.38.1"
|
||||
__version__ = "0.40.0"
|
||||
|
||||
from starlette.background import BackgroundTasks
|
||||
|
||||
|
||||
@@ -33,11 +33,14 @@ class FastAPI(Starlette):
|
||||
version: str = "0.1.0",
|
||||
openapi_url: Optional[str] = "/openapi.json",
|
||||
openapi_prefix: str = "",
|
||||
default_response_class: Type[Response] = JSONResponse,
|
||||
docs_url: Optional[str] = "/docs",
|
||||
redoc_url: Optional[str] = "/redoc",
|
||||
swagger_ui_oauth2_redirect_url: Optional[str] = "/docs/oauth2-redirect",
|
||||
swagger_ui_init_oauth: Optional[dict] = None,
|
||||
**extra: Dict[str, Any],
|
||||
) -> None:
|
||||
self.default_response_class = default_response_class
|
||||
self._debug = debug
|
||||
self.router: routing.APIRouter = routing.APIRouter(
|
||||
routes, dependency_overrides_provider=self
|
||||
@@ -55,6 +58,7 @@ class FastAPI(Starlette):
|
||||
self.docs_url = docs_url
|
||||
self.redoc_url = redoc_url
|
||||
self.swagger_ui_oauth2_redirect_url = swagger_ui_oauth2_redirect_url
|
||||
self.swagger_ui_init_oauth = swagger_ui_init_oauth
|
||||
self.extra = extra
|
||||
self.dependency_overrides: Dict[Callable, Callable] = {}
|
||||
|
||||
@@ -96,6 +100,7 @@ class FastAPI(Starlette):
|
||||
openapi_url=openapi_url,
|
||||
title=self.title + " - Swagger UI",
|
||||
oauth2_redirect_url=self.swagger_ui_oauth2_redirect_url,
|
||||
init_oauth=self.swagger_ui_init_oauth,
|
||||
)
|
||||
|
||||
self.add_route(self.docs_url, swagger_ui_html, include_in_schema=False)
|
||||
@@ -144,7 +149,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> None:
|
||||
self.router.add_api_route(
|
||||
@@ -166,7 +171,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias=response_model_by_alias,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -190,7 +195,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@@ -213,7 +218,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias=response_model_by_alias,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
return func
|
||||
@@ -240,6 +245,7 @@ class FastAPI(Starlette):
|
||||
tags: List[str] = None,
|
||||
dependencies: Sequence[Depends] = None,
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
default_response_class: Optional[Type[Response]] = None,
|
||||
) -> None:
|
||||
self.router.include_router(
|
||||
router,
|
||||
@@ -247,6 +253,8 @@ class FastAPI(Starlette):
|
||||
tags=tags,
|
||||
dependencies=dependencies,
|
||||
responses=responses or {},
|
||||
default_response_class=default_response_class
|
||||
or self.default_response_class,
|
||||
)
|
||||
|
||||
def get(
|
||||
@@ -268,7 +276,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.get(
|
||||
@@ -288,7 +296,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias=response_model_by_alias,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -311,7 +319,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.put(
|
||||
@@ -331,7 +339,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias=response_model_by_alias,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -354,7 +362,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.post(
|
||||
@@ -374,7 +382,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias=response_model_by_alias,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -397,7 +405,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.delete(
|
||||
@@ -417,7 +425,7 @@ class FastAPI(Starlette):
|
||||
operation_id=operation_id,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -440,7 +448,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.options(
|
||||
@@ -460,7 +468,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias=response_model_by_alias,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -483,7 +491,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.head(
|
||||
@@ -503,7 +511,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias=response_model_by_alias,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -526,7 +534,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.patch(
|
||||
@@ -546,7 +554,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias=response_model_by_alias,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -569,7 +577,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.trace(
|
||||
@@ -589,6 +597,6 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias=response_model_by_alias,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -26,7 +26,7 @@ from pydantic.error_wrappers import ErrorWrapper
|
||||
from pydantic.errors import MissingError
|
||||
from pydantic.fields import Field, Required, Shape
|
||||
from pydantic.schema import get_annotation_from_schema
|
||||
from pydantic.utils import lenient_issubclass
|
||||
from pydantic.utils import ForwardRef, evaluate_forwardref, lenient_issubclass
|
||||
from starlette.background import BackgroundTasks
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
from starlette.datastructures import FormData, Headers, QueryParams, UploadFile
|
||||
@@ -171,6 +171,30 @@ def is_scalar_sequence_field(field: Field) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def get_typed_signature(call: Callable) -> inspect.Signature:
|
||||
signature = inspect.signature(call)
|
||||
globalns = getattr(call, "__globals__", {})
|
||||
typed_params = [
|
||||
inspect.Parameter(
|
||||
name=param.name,
|
||||
kind=param.kind,
|
||||
default=param.default,
|
||||
annotation=get_typed_annotation(param, globalns),
|
||||
)
|
||||
for param in signature.parameters.values()
|
||||
]
|
||||
typed_signature = inspect.Signature(typed_params)
|
||||
return typed_signature
|
||||
|
||||
|
||||
def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) -> Any:
|
||||
annotation = param.annotation
|
||||
if isinstance(annotation, str):
|
||||
annotation = ForwardRef(annotation)
|
||||
annotation = evaluate_forwardref(annotation, globalns, globalns)
|
||||
return annotation
|
||||
|
||||
|
||||
def get_dependant(
|
||||
*,
|
||||
path: str,
|
||||
@@ -180,7 +204,7 @@ def get_dependant(
|
||||
use_cache: bool = True,
|
||||
) -> Dependant:
|
||||
path_param_names = get_path_param_names(path)
|
||||
endpoint_signature = inspect.signature(call)
|
||||
endpoint_signature = get_typed_signature(call)
|
||||
signature_params = endpoint_signature.parameters
|
||||
dependant = Dependant(call=call, name=name, path=path, use_cache=use_cache)
|
||||
for param_name, param in signature_params.items():
|
||||
@@ -196,16 +220,18 @@ def get_dependant(
|
||||
continue
|
||||
param_field = get_param_field(param=param, default_schema=params.Query)
|
||||
if param_name in path_param_names:
|
||||
assert param.default == param.empty or isinstance(
|
||||
param.default, params.Path
|
||||
), "Path params must have no defaults or use Path(...)"
|
||||
assert is_scalar_field(
|
||||
field=param_field
|
||||
), f"Path params must be of one of the supported types"
|
||||
if isinstance(param.default, params.Path):
|
||||
ignore_default = False
|
||||
else:
|
||||
ignore_default = True
|
||||
param_field = get_param_field(
|
||||
param=param,
|
||||
default_schema=params.Path,
|
||||
force_type=params.ParamTypes.path,
|
||||
ignore_default=ignore_default,
|
||||
)
|
||||
add_param_to_fields(field=param_field, dependant=dependant)
|
||||
elif is_scalar_field(field=param_field):
|
||||
@@ -248,10 +274,11 @@ def get_param_field(
|
||||
param: inspect.Parameter,
|
||||
default_schema: Type[params.Param] = params.Param,
|
||||
force_type: params.ParamTypes = None,
|
||||
ignore_default: bool = False,
|
||||
) -> Field:
|
||||
default_value = Required
|
||||
had_schema = False
|
||||
if not param.default == param.empty:
|
||||
if not param.default == param.empty and ignore_default is False:
|
||||
default_value = param.default
|
||||
if isinstance(default_value, Schema):
|
||||
had_schema = True
|
||||
@@ -329,8 +356,12 @@ async def solve_dependencies(
|
||||
]:
|
||||
values: Dict[str, Any] = {}
|
||||
errors: List[ErrorWrapper] = []
|
||||
response = response or Response( # type: ignore
|
||||
content=None, status_code=None, headers=None, media_type=None, background=None
|
||||
response = response or Response(
|
||||
content=None,
|
||||
status_code=None, # type: ignore
|
||||
headers=None,
|
||||
media_type=None,
|
||||
background=None,
|
||||
)
|
||||
dependency_cache = dependency_cache or {}
|
||||
sub_dependant: Dependant
|
||||
@@ -405,7 +436,7 @@ async def solve_dependencies(
|
||||
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( # type: ignore # body_params checked above
|
||||
body_values, body_errors = await request_body_to_args( # body_params checked above
|
||||
required_params=dependant.body_params, received_body=body
|
||||
)
|
||||
values.update(body_values)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
|
||||
@@ -11,10 +13,11 @@ def get_swagger_ui_html(
|
||||
swagger_css_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui.css",
|
||||
swagger_favicon_url: str = "https://fastapi.tiangolo.com/img/favicon.png",
|
||||
oauth2_redirect_url: Optional[str] = None,
|
||||
init_oauth: Optional[dict] = None,
|
||||
) -> HTMLResponse:
|
||||
|
||||
html = f"""
|
||||
<! doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link type="text/css" rel="stylesheet" href="{swagger_css_url}">
|
||||
@@ -42,7 +45,14 @@ def get_swagger_ui_html(
|
||||
],
|
||||
layout: "BaseLayout",
|
||||
deepLinking: true
|
||||
})
|
||||
})"""
|
||||
|
||||
if init_oauth:
|
||||
html += f"""
|
||||
ui.initOAuth({json.dumps(jsonable_encoder(init_oauth))})
|
||||
"""
|
||||
|
||||
html += """
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -94,7 +104,7 @@ def get_redoc_html(
|
||||
|
||||
def get_swagger_ui_oauth2_redirect_html() -> HTMLResponse:
|
||||
html = """
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-US">
|
||||
<body onload="run()">
|
||||
</body>
|
||||
|
||||
@@ -11,7 +11,7 @@ try:
|
||||
import email_validator
|
||||
|
||||
assert email_validator # make autoflake ignore the unused import
|
||||
from pydantic.types import EmailStr # type: ignore
|
||||
from pydantic.types import EmailStr
|
||||
except ImportError: # pragma: no cover
|
||||
logger.warning(
|
||||
"email-validator not installed, email fields will be treated as str.\n"
|
||||
|
||||
@@ -151,6 +151,10 @@ def get_openapi_path(
|
||||
security_schemes: Dict[str, Any] = {}
|
||||
definitions: Dict[str, Any] = {}
|
||||
assert route.methods is not None, "Methods must be a list"
|
||||
assert (
|
||||
route.response_class and route.response_class.media_type
|
||||
), "A response class with media_type is needed to generate OpenAPI"
|
||||
route_response_media_type: str = route.response_class.media_type
|
||||
if route.include_in_schema:
|
||||
for method in route.methods:
|
||||
operation = get_openapi_operation_metadata(route=route, method=method)
|
||||
@@ -185,7 +189,7 @@ def get_openapi_path(
|
||||
field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
|
||||
)
|
||||
response.setdefault("content", {}).setdefault(
|
||||
route.response_class.media_type, {}
|
||||
route_response_media_type, {}
|
||||
)["schema"] = response_schema
|
||||
status_text: Optional[str] = status_code_ranges.get(
|
||||
str(additional_status_code).upper()
|
||||
@@ -213,7 +217,7 @@ def get_openapi_path(
|
||||
] = route.response_description
|
||||
operation.setdefault("responses", {}).setdefault(
|
||||
status_code, {}
|
||||
).setdefault("content", {}).setdefault(route.response_class.media_type, {})[
|
||||
).setdefault("content", {}).setdefault(route_response_media_type, {})[
|
||||
"schema"
|
||||
] = response_schema
|
||||
|
||||
@@ -221,7 +225,7 @@ def get_openapi_path(
|
||||
if (all_route_params or route.body_field) and not any(
|
||||
[
|
||||
status in operation["responses"]
|
||||
for status in [http422, "4xx", "default"]
|
||||
for status in [http422, "4XX", "default"]
|
||||
]
|
||||
):
|
||||
operation["responses"][http422] = {
|
||||
@@ -279,7 +283,7 @@ def get_openapi(
|
||||
if path_definitions:
|
||||
definitions.update(path_definitions)
|
||||
if definitions:
|
||||
components.setdefault("schemas", {}).update(definitions)
|
||||
components["schemas"] = {k: definitions[k] for k in sorted(definitions)}
|
||||
if components:
|
||||
output["components"] = components
|
||||
output["paths"] = paths
|
||||
|
||||
@@ -200,7 +200,7 @@ class APIRoute(routing.Route):
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Optional[Type[Response]] = None,
|
||||
dependency_overrides_provider: Any = None,
|
||||
) -> None:
|
||||
self.path = path
|
||||
@@ -215,9 +215,6 @@ class APIRoute(routing.Route):
|
||||
)
|
||||
self.response_model = response_model
|
||||
if self.response_model:
|
||||
assert lenient_issubclass(
|
||||
response_class, JSONResponse
|
||||
), "To declare a type the response must be a JSON response"
|
||||
response_name = "Response_" + self.unique_id
|
||||
self.response_field: Optional[Field] = Field(
|
||||
name=response_name,
|
||||
@@ -249,6 +246,9 @@ class APIRoute(routing.Route):
|
||||
self.dependencies = []
|
||||
self.summary = summary
|
||||
self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "")
|
||||
# if a "form feed" character (page break) is found in the description text,
|
||||
# truncate description text to the content preceding the first "form feed"
|
||||
self.description = self.description.split("\f")[0]
|
||||
self.response_description = response_description
|
||||
self.responses = responses or {}
|
||||
response_fields = {}
|
||||
@@ -299,7 +299,7 @@ class APIRoute(routing.Route):
|
||||
dependant=self.dependant,
|
||||
body_field=self.body_field,
|
||||
status_code=self.status_code,
|
||||
response_class=self.response_class,
|
||||
response_class=self.response_class or JSONResponse,
|
||||
response_field=self.secure_cloned_response_field,
|
||||
response_model_include=self.response_model_include,
|
||||
response_model_exclude=self.response_model_exclude,
|
||||
@@ -346,7 +346,7 @@ class APIRouter(routing.Router):
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> None:
|
||||
route = self.route_class(
|
||||
@@ -394,7 +394,7 @@ class APIRouter(routing.Router):
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@@ -445,6 +445,7 @@ class APIRouter(routing.Router):
|
||||
tags: List[str] = None,
|
||||
dependencies: Sequence[params.Depends] = None,
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
default_response_class: Optional[Type[Response]] = None,
|
||||
) -> None:
|
||||
if prefix:
|
||||
assert prefix.startswith("/"), "A path prefix must start with '/'"
|
||||
@@ -484,7 +485,7 @@ class APIRouter(routing.Router):
|
||||
response_model_by_alias=route.response_model_by_alias,
|
||||
response_model_skip_defaults=route.response_model_skip_defaults,
|
||||
include_in_schema=route.include_in_schema,
|
||||
response_class=route.response_class,
|
||||
response_class=route.response_class or default_response_class,
|
||||
name=route.name,
|
||||
)
|
||||
elif isinstance(route, routing.Route):
|
||||
@@ -523,10 +524,9 @@ class APIRouter(routing.Router):
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
|
||||
return self.api_route(
|
||||
path=path,
|
||||
response_model=response_model,
|
||||
@@ -568,7 +568,7 @@ class APIRouter(routing.Router):
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
@@ -612,7 +612,7 @@ class APIRouter(routing.Router):
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
@@ -656,7 +656,7 @@ class APIRouter(routing.Router):
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
@@ -700,7 +700,7 @@ class APIRouter(routing.Router):
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
@@ -744,7 +744,7 @@ class APIRouter(routing.Router):
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
@@ -788,7 +788,7 @@ class APIRouter(routing.Router):
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
@@ -832,7 +832,7 @@ class APIRouter(routing.Router):
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
|
||||
@@ -58,10 +58,10 @@ def create_cloned_field(field: Field) -> Field:
|
||||
use_type = original_type
|
||||
if lenient_issubclass(original_type, BaseModel):
|
||||
original_type = cast(Type[BaseModel], original_type)
|
||||
use_type = create_model( # type: ignore
|
||||
use_type = create_model(
|
||||
original_type.__name__,
|
||||
__config__=original_type.__config__,
|
||||
__validators__=original_type.__validators__,
|
||||
__validators__=original_type.__validators__, # type: ignore
|
||||
)
|
||||
for f in original_type.__fields__.values():
|
||||
use_type.__fields__[f.name] = f
|
||||
|
||||
@@ -39,6 +39,7 @@ test = [
|
||||
"email_validator",
|
||||
"sqlalchemy",
|
||||
"databases[sqlite]",
|
||||
"orjson"
|
||||
]
|
||||
doc = [
|
||||
"mkdocs",
|
||||
|
||||
216
tests/test_default_response_class.py
Normal file
216
tests/test_default_response_class.py
Normal file
@@ -0,0 +1,216 @@
|
||||
from typing import Any
|
||||
|
||||
import orjson
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from starlette.responses import HTMLResponse, JSONResponse, PlainTextResponse
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
|
||||
class ORJSONResponse(JSONResponse):
|
||||
media_type = "application/x-orjson"
|
||||
|
||||
def render(self, content: Any) -> bytes:
|
||||
return orjson.dumps(content)
|
||||
|
||||
|
||||
class OverrideResponse(JSONResponse):
|
||||
media_type = "application/x-override"
|
||||
|
||||
|
||||
app = FastAPI(default_response_class=ORJSONResponse)
|
||||
router_a = APIRouter()
|
||||
router_a_a = APIRouter()
|
||||
router_a_b_override = APIRouter() # Overrides default class
|
||||
router_b_override = APIRouter() # Overrides default class
|
||||
router_b_a = APIRouter()
|
||||
router_b_a_c_override = APIRouter() # Overrides default class again
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def get_root():
|
||||
return {"msg": "Hello World"}
|
||||
|
||||
|
||||
@app.get("/override", response_class=PlainTextResponse)
|
||||
def get_path_override():
|
||||
return "Hello World"
|
||||
|
||||
|
||||
@router_a.get("/")
|
||||
def get_a():
|
||||
return {"msg": "Hello A"}
|
||||
|
||||
|
||||
@router_a.get("/override", response_class=PlainTextResponse)
|
||||
def get_a_path_override():
|
||||
return "Hello A"
|
||||
|
||||
|
||||
@router_a_a.get("/")
|
||||
def get_a_a():
|
||||
return {"msg": "Hello A A"}
|
||||
|
||||
|
||||
@router_a_a.get("/override", response_class=PlainTextResponse)
|
||||
def get_a_a_path_override():
|
||||
return "Hello A A"
|
||||
|
||||
|
||||
@router_a_b_override.get("/")
|
||||
def get_a_b():
|
||||
return "Hello A B"
|
||||
|
||||
|
||||
@router_a_b_override.get("/override", response_class=HTMLResponse)
|
||||
def get_a_b_path_override():
|
||||
return "Hello A B"
|
||||
|
||||
|
||||
@router_b_override.get("/")
|
||||
def get_b():
|
||||
return "Hello B"
|
||||
|
||||
|
||||
@router_b_override.get("/override", response_class=HTMLResponse)
|
||||
def get_b_path_override():
|
||||
return "Hello B"
|
||||
|
||||
|
||||
@router_b_a.get("/")
|
||||
def get_b_a():
|
||||
return "Hello B A"
|
||||
|
||||
|
||||
@router_b_a.get("/override", response_class=HTMLResponse)
|
||||
def get_b_a_path_override():
|
||||
return "Hello B A"
|
||||
|
||||
|
||||
@router_b_a_c_override.get("/")
|
||||
def get_b_a_c():
|
||||
return "Hello B A C"
|
||||
|
||||
|
||||
@router_b_a_c_override.get("/override", response_class=OverrideResponse)
|
||||
def get_b_a_c_path_override():
|
||||
return {"msg": "Hello B A C"}
|
||||
|
||||
|
||||
router_b_a.include_router(
|
||||
router_b_a_c_override, prefix="/c", default_response_class=HTMLResponse
|
||||
)
|
||||
router_b_override.include_router(router_b_a, prefix="/a")
|
||||
router_a.include_router(router_a_a, prefix="/a")
|
||||
router_a.include_router(
|
||||
router_a_b_override, prefix="/b", default_response_class=PlainTextResponse
|
||||
)
|
||||
app.include_router(router_a, prefix="/a")
|
||||
app.include_router(
|
||||
router_b_override, prefix="/b", default_response_class=PlainTextResponse
|
||||
)
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
orjson_type = "application/x-orjson"
|
||||
text_type = "text/plain; charset=utf-8"
|
||||
html_type = "text/html; charset=utf-8"
|
||||
override_type = "application/x-override"
|
||||
|
||||
|
||||
def test_app():
|
||||
with client:
|
||||
response = client.get("/")
|
||||
assert response.json() == {"msg": "Hello World"}
|
||||
assert response.headers["content-type"] == orjson_type
|
||||
|
||||
|
||||
def test_app_override():
|
||||
with client:
|
||||
response = client.get("/override")
|
||||
assert response.content == b"Hello World"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_a():
|
||||
with client:
|
||||
response = client.get("/a")
|
||||
assert response.json() == {"msg": "Hello A"}
|
||||
assert response.headers["content-type"] == orjson_type
|
||||
|
||||
|
||||
def test_router_a_override():
|
||||
with client:
|
||||
response = client.get("/a/override")
|
||||
assert response.content == b"Hello A"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_a_a():
|
||||
with client:
|
||||
response = client.get("/a/a")
|
||||
assert response.json() == {"msg": "Hello A A"}
|
||||
assert response.headers["content-type"] == orjson_type
|
||||
|
||||
|
||||
def test_router_a_a_override():
|
||||
with client:
|
||||
response = client.get("/a/a/override")
|
||||
assert response.content == b"Hello A A"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_a_b():
|
||||
with client:
|
||||
response = client.get("/a/b")
|
||||
assert response.content == b"Hello A B"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_a_b_override():
|
||||
with client:
|
||||
response = client.get("/a/b/override")
|
||||
assert response.content == b"Hello A B"
|
||||
assert response.headers["content-type"] == html_type
|
||||
|
||||
|
||||
def test_router_b():
|
||||
with client:
|
||||
response = client.get("/b")
|
||||
assert response.content == b"Hello B"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_b_override():
|
||||
with client:
|
||||
response = client.get("/b/override")
|
||||
assert response.content == b"Hello B"
|
||||
assert response.headers["content-type"] == html_type
|
||||
|
||||
|
||||
def test_router_b_a():
|
||||
with client:
|
||||
response = client.get("/b/a")
|
||||
assert response.content == b"Hello B A"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_b_a_override():
|
||||
with client:
|
||||
response = client.get("/b/a/override")
|
||||
assert response.content == b"Hello B A"
|
||||
assert response.headers["content-type"] == html_type
|
||||
|
||||
|
||||
def test_router_b_a_c():
|
||||
with client:
|
||||
response = client.get("/b/a/c")
|
||||
assert response.content == b"Hello B A C"
|
||||
assert response.headers["content-type"] == html_type
|
||||
|
||||
|
||||
def test_router_b_a_c_override():
|
||||
with client:
|
||||
response = client.get("/b/a/c/override")
|
||||
assert response.json() == {"msg": "Hello B A C"}
|
||||
assert response.headers["content-type"] == override_type
|
||||
206
tests/test_default_response_class_router.py
Normal file
206
tests/test_default_response_class_router.py
Normal file
@@ -0,0 +1,206 @@
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from starlette.responses import HTMLResponse, JSONResponse, PlainTextResponse
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
|
||||
class OverrideResponse(JSONResponse):
|
||||
media_type = "application/x-override"
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
router_a = APIRouter()
|
||||
router_a_a = APIRouter()
|
||||
router_a_b_override = APIRouter() # Overrides default class
|
||||
router_b_override = APIRouter() # Overrides default class
|
||||
router_b_a = APIRouter()
|
||||
router_b_a_c_override = APIRouter() # Overrides default class again
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def get_root():
|
||||
return {"msg": "Hello World"}
|
||||
|
||||
|
||||
@app.get("/override", response_class=PlainTextResponse)
|
||||
def get_path_override():
|
||||
return "Hello World"
|
||||
|
||||
|
||||
@router_a.get("/")
|
||||
def get_a():
|
||||
return {"msg": "Hello A"}
|
||||
|
||||
|
||||
@router_a.get("/override", response_class=PlainTextResponse)
|
||||
def get_a_path_override():
|
||||
return "Hello A"
|
||||
|
||||
|
||||
@router_a_a.get("/")
|
||||
def get_a_a():
|
||||
return {"msg": "Hello A A"}
|
||||
|
||||
|
||||
@router_a_a.get("/override", response_class=PlainTextResponse)
|
||||
def get_a_a_path_override():
|
||||
return "Hello A A"
|
||||
|
||||
|
||||
@router_a_b_override.get("/")
|
||||
def get_a_b():
|
||||
return "Hello A B"
|
||||
|
||||
|
||||
@router_a_b_override.get("/override", response_class=HTMLResponse)
|
||||
def get_a_b_path_override():
|
||||
return "Hello A B"
|
||||
|
||||
|
||||
@router_b_override.get("/")
|
||||
def get_b():
|
||||
return "Hello B"
|
||||
|
||||
|
||||
@router_b_override.get("/override", response_class=HTMLResponse)
|
||||
def get_b_path_override():
|
||||
return "Hello B"
|
||||
|
||||
|
||||
@router_b_a.get("/")
|
||||
def get_b_a():
|
||||
return "Hello B A"
|
||||
|
||||
|
||||
@router_b_a.get("/override", response_class=HTMLResponse)
|
||||
def get_b_a_path_override():
|
||||
return "Hello B A"
|
||||
|
||||
|
||||
@router_b_a_c_override.get("/")
|
||||
def get_b_a_c():
|
||||
return "Hello B A C"
|
||||
|
||||
|
||||
@router_b_a_c_override.get("/override", response_class=OverrideResponse)
|
||||
def get_b_a_c_path_override():
|
||||
return {"msg": "Hello B A C"}
|
||||
|
||||
|
||||
router_b_a.include_router(
|
||||
router_b_a_c_override, prefix="/c", default_response_class=HTMLResponse
|
||||
)
|
||||
router_b_override.include_router(router_b_a, prefix="/a")
|
||||
router_a.include_router(router_a_a, prefix="/a")
|
||||
router_a.include_router(
|
||||
router_a_b_override, prefix="/b", default_response_class=PlainTextResponse
|
||||
)
|
||||
app.include_router(router_a, prefix="/a")
|
||||
app.include_router(
|
||||
router_b_override, prefix="/b", default_response_class=PlainTextResponse
|
||||
)
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
json_type = "application/json"
|
||||
text_type = "text/plain; charset=utf-8"
|
||||
html_type = "text/html; charset=utf-8"
|
||||
override_type = "application/x-override"
|
||||
|
||||
|
||||
def test_app():
|
||||
with client:
|
||||
response = client.get("/")
|
||||
assert response.json() == {"msg": "Hello World"}
|
||||
assert response.headers["content-type"] == json_type
|
||||
|
||||
|
||||
def test_app_override():
|
||||
with client:
|
||||
response = client.get("/override")
|
||||
assert response.content == b"Hello World"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_a():
|
||||
with client:
|
||||
response = client.get("/a")
|
||||
assert response.json() == {"msg": "Hello A"}
|
||||
assert response.headers["content-type"] == json_type
|
||||
|
||||
|
||||
def test_router_a_override():
|
||||
with client:
|
||||
response = client.get("/a/override")
|
||||
assert response.content == b"Hello A"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_a_a():
|
||||
with client:
|
||||
response = client.get("/a/a")
|
||||
assert response.json() == {"msg": "Hello A A"}
|
||||
assert response.headers["content-type"] == json_type
|
||||
|
||||
|
||||
def test_router_a_a_override():
|
||||
with client:
|
||||
response = client.get("/a/a/override")
|
||||
assert response.content == b"Hello A A"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_a_b():
|
||||
with client:
|
||||
response = client.get("/a/b")
|
||||
assert response.content == b"Hello A B"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_a_b_override():
|
||||
with client:
|
||||
response = client.get("/a/b/override")
|
||||
assert response.content == b"Hello A B"
|
||||
assert response.headers["content-type"] == html_type
|
||||
|
||||
|
||||
def test_router_b():
|
||||
with client:
|
||||
response = client.get("/b")
|
||||
assert response.content == b"Hello B"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_b_override():
|
||||
with client:
|
||||
response = client.get("/b/override")
|
||||
assert response.content == b"Hello B"
|
||||
assert response.headers["content-type"] == html_type
|
||||
|
||||
|
||||
def test_router_b_a():
|
||||
with client:
|
||||
response = client.get("/b/a")
|
||||
assert response.content == b"Hello B A"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_b_a_override():
|
||||
with client:
|
||||
response = client.get("/b/a/override")
|
||||
assert response.content == b"Hello B A"
|
||||
assert response.headers["content-type"] == html_type
|
||||
|
||||
|
||||
def test_router_b_a_c():
|
||||
with client:
|
||||
response = client.get("/b/a/c")
|
||||
assert response.content == b"Hello B A C"
|
||||
assert response.headers["content-type"] == html_type
|
||||
|
||||
|
||||
def test_router_b_a_c_override():
|
||||
with client:
|
||||
response = client.get("/b/a/c/override")
|
||||
assert response.json() == {"msg": "Hello B A C"}
|
||||
assert response.headers["content-type"] == override_type
|
||||
136
tests/test_infer_param_optionality.py
Normal file
136
tests/test_infer_param_optionality.py
Normal file
@@ -0,0 +1,136 @@
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
user_router = APIRouter()
|
||||
item_router = APIRouter()
|
||||
|
||||
|
||||
@user_router.get("/")
|
||||
def get_users():
|
||||
return [{"user_id": "u1"}, {"user_id": "u2"}]
|
||||
|
||||
|
||||
@user_router.get("/{user_id}")
|
||||
def get_user(user_id: str):
|
||||
return {"user_id": user_id}
|
||||
|
||||
|
||||
@item_router.get("/")
|
||||
def get_items(user_id: str = None):
|
||||
if user_id is None:
|
||||
return [{"item_id": "i1", "user_id": "u1"}, {"item_id": "i2", "user_id": "u2"}]
|
||||
else:
|
||||
return [{"item_id": "i2", "user_id": user_id}]
|
||||
|
||||
|
||||
@item_router.get("/{item_id}")
|
||||
def get_item(item_id: str, user_id: str = None):
|
||||
if user_id is None:
|
||||
return {"item_id": item_id}
|
||||
else:
|
||||
return {"item_id": item_id, "user_id": user_id}
|
||||
|
||||
|
||||
app.include_router(user_router, prefix="/users")
|
||||
app.include_router(item_router, prefix="/items")
|
||||
|
||||
app.include_router(item_router, prefix="/users/{user_id}/items")
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_get_users():
|
||||
"""Check that /users returns expected data"""
|
||||
response = client.get("/users")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [{"user_id": "u1"}, {"user_id": "u2"}]
|
||||
|
||||
|
||||
def test_get_user():
|
||||
"""Check that /users/{user_id} returns expected data"""
|
||||
response = client.get("/users/abc123")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"user_id": "abc123"}
|
||||
|
||||
|
||||
def test_get_items_1():
|
||||
"""Check that /items returns expected data"""
|
||||
response = client.get("/items")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [
|
||||
{"item_id": "i1", "user_id": "u1"},
|
||||
{"item_id": "i2", "user_id": "u2"},
|
||||
]
|
||||
|
||||
|
||||
def test_get_items_2():
|
||||
"""Check that /items returns expected data with user_id specified"""
|
||||
response = client.get("/items?user_id=abc123")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [{"item_id": "i2", "user_id": "abc123"}]
|
||||
|
||||
|
||||
def test_get_item_1():
|
||||
"""Check that /items/{item_id} returns expected data"""
|
||||
response = client.get("/items/item01")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": "item01"}
|
||||
|
||||
|
||||
def test_get_item_2():
|
||||
"""Check that /items/{item_id} returns expected data with user_id specified"""
|
||||
response = client.get("/items/item01?user_id=abc123")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": "item01", "user_id": "abc123"}
|
||||
|
||||
|
||||
def test_get_users_items():
|
||||
"""Check that /users/{user_id}/items returns expected data"""
|
||||
response = client.get("/users/abc123/items")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [{"item_id": "i2", "user_id": "abc123"}]
|
||||
|
||||
|
||||
def test_get_users_item():
|
||||
"""Check that /users/{user_id}/items returns expected data"""
|
||||
response = client.get("/users/abc123/items/item01")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": "item01", "user_id": "abc123"}
|
||||
|
||||
|
||||
def test_schema_1():
|
||||
"""Check that the user_id is a required path parameter under /users"""
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
r = response.json()
|
||||
|
||||
d = {
|
||||
"required": True,
|
||||
"schema": {"title": "User_Id", "type": "string"},
|
||||
"name": "user_id",
|
||||
"in": "path",
|
||||
}
|
||||
|
||||
assert d in r["paths"]["/users/{user_id}"]["get"]["parameters"]
|
||||
assert d in r["paths"]["/users/{user_id}/items/"]["get"]["parameters"]
|
||||
|
||||
|
||||
def test_schema_2():
|
||||
"""Check that the user_id is an optional query parameter under /items"""
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
r = response.json()
|
||||
|
||||
d = {
|
||||
"required": False,
|
||||
"schema": {"title": "User_Id", "type": "string"},
|
||||
"name": "user_id",
|
||||
"in": "query",
|
||||
}
|
||||
|
||||
assert d in r["paths"]["/items/{item_id}"]["get"]["parameters"]
|
||||
assert d in r["paths"]["/items/"]["get"]["parameters"]
|
||||
@@ -21,18 +21,21 @@ class User(BaseModel):
|
||||
username: str
|
||||
|
||||
|
||||
def get_current_user(oauth_header: str = Security(reusable_oauth2)):
|
||||
# Here we use string annotations to test them
|
||||
def get_current_user(oauth_header: "str" = Security(reusable_oauth2)):
|
||||
user = User(username=oauth_header)
|
||||
return user
|
||||
|
||||
|
||||
@app.post("/login")
|
||||
def read_current_user(form_data: OAuth2PasswordRequestFormStrict = Depends()):
|
||||
# Here we use string annotations to test them
|
||||
def read_current_user(form_data: "OAuth2PasswordRequestFormStrict" = Depends()):
|
||||
return form_data
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(current_user: User = Depends(get_current_user)):
|
||||
# Here we use string annotations to test them
|
||||
def read_current_user(current_user: "User" = Depends(get_current_user)):
|
||||
return current_user
|
||||
|
||||
|
||||
|
||||
28
tests/test_swagger_ui_init_oauth.py
Normal file
28
tests/test_swagger_ui_init_oauth.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from fastapi import FastAPI
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
swagger_ui_init_oauth = {"clientId": "the-foo-clients", "appName": "The Predendapp"}
|
||||
|
||||
app = FastAPI(swagger_ui_init_oauth=swagger_ui_init_oauth)
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_items():
|
||||
return {"id": "foo"}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_swagger_ui():
|
||||
response = client.get("/docs")
|
||||
assert response.status_code == 200
|
||||
print(response.text)
|
||||
assert f"ui.initOAuth" in response.text
|
||||
assert f'"appName": "The Predendapp"' in response.text
|
||||
assert f'"clientId": "the-foo-clients"' in response.text
|
||||
|
||||
|
||||
def test_response():
|
||||
response = client.get("/items/")
|
||||
assert response.json() == {"id": "foo"}
|
||||
@@ -0,0 +1,112 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from path_operation_advanced_configuration.tutorial003 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/": {
|
||||
"post": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Item"}
|
||||
}
|
||||
},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Create an item",
|
||||
"description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item\n",
|
||||
"operationId": "create_item_items__post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Item"}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Item": {
|
||||
"title": "Item",
|
||||
"required": ["name", "price"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"price": {"title": "Price", "type": "number"},
|
||||
"description": {"title": "Description", "type": "string"},
|
||||
"tax": {"title": "Tax", "type": "number"},
|
||||
"tags": {
|
||||
"title": "Tags",
|
||||
"uniqueItems": True,
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"default": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
"title": "ValidationError",
|
||||
"required": ["loc", "msg", "type"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loc": {
|
||||
"title": "Location",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"msg": {"title": "Message", "type": "string"},
|
||||
"type": {"title": "Error Type", "type": "string"},
|
||||
},
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_query_params_str_validations():
|
||||
response = client.post("/items/", json={"name": "Foo", "price": 42})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"name": "Foo",
|
||||
"price": 42,
|
||||
"description": None,
|
||||
"tax": None,
|
||||
"tags": [],
|
||||
}
|
||||
Reference in New Issue
Block a user