mirror of
https://github.com/fastapi/fastapi.git
synced 2025-12-25 15:18:36 -05:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5592fa0f6f | ||
|
|
b65be5d496 | ||
|
|
6c7da43e51 | ||
|
|
dfec2d7644 | ||
|
|
8c3ef76139 | ||
|
|
7a504a721c | ||
|
|
dd963511d6 | ||
|
|
fdb6d43e10 | ||
|
|
a7c718e968 | ||
|
|
f4d753620b | ||
|
|
fadfe4c586 | ||
|
|
5fd83c5fa4 | ||
|
|
14daaf409f | ||
|
|
c7dc26b760 | ||
|
|
f5ccb3c35d | ||
|
|
4cea311e6e | ||
|
|
f8718072a0 | ||
|
|
3dbbecdd16 | ||
|
|
6d5530ec1c | ||
|
|
0761f11d1a | ||
|
|
f2e7ef7056 | ||
|
|
d5d9a20937 | ||
|
|
96f092179f | ||
|
|
8505b716af | ||
|
|
78272ac1f3 | ||
|
|
f1bee9a271 | ||
|
|
b20b2218cd | ||
|
|
b9cf69cd42 | ||
|
|
f803c77515 | ||
|
|
0c67022048 | ||
|
|
d8fe307d61 | ||
|
|
580cf8f4e2 | ||
|
|
af390af77c | ||
|
|
4642f63a1e | ||
|
|
203e10596f | ||
|
|
5a2278d09a | ||
|
|
47a8387a04 | ||
|
|
27ca0c9dca | ||
|
|
9418d78de6 | ||
|
|
4b74aef429 | ||
|
|
fc7d123347 |
5
Pipfile
5
Pipfile
@@ -25,10 +25,11 @@ sqlalchemy = "*"
|
||||
uvicorn = "*"
|
||||
|
||||
[packages]
|
||||
starlette = "==0.12.7"
|
||||
pydantic = "==0.30.0"
|
||||
starlette = "==0.12.9"
|
||||
pydantic = "==0.32.2"
|
||||
databases = {extras = ["sqlite"],version = "*"}
|
||||
hypercorn = "*"
|
||||
orjson = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.6"
|
||||
|
||||
11
README.md
11
README.md
@@ -63,8 +63,19 @@ The key features are:
|
||||
|
||||
---
|
||||
|
||||
"*If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]*"
|
||||
|
||||
"*We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]*"
|
||||
|
||||
<div style="text-align: right; margin-right: 10%;">Ines Montani - Matthew Honnibal - <strong><a href="https://explosion.ai" target="_blank">Explosion AI</a> founders - <a href="https://spacy.io" target="_blank">spaCy</a> creators</strong> <a href="https://twitter.com/_inesmontani/status/1144173225322143744" target="_blank"><small>(ref)</small></a> - <a href="https://twitter.com/honnibal/status/1144031421859655680" target="_blank"><small>(ref)</small></a></div>
|
||||
|
||||
---
|
||||
|
||||
"*We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]*"
|
||||
|
||||
<div style="text-align: right; margin-right: 10%;">Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - <strong>Uber</strong> <a href="https://eng.uber.com/ludwig-v0-2/" target="_blank"><small>(ref)</small></a></div>
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -27,6 +27,14 @@ Here's an incomplete list of some of them.
|
||||
|
||||
* <a href="https://medium.com/@nico.axtmann95/deploying-a-scikit-learn-model-with-onnx-und-fastapi-1af398268915" target="_blank">Deploying a scikit-learn model with ONNX and FastAPI</a> by <a href="https://www.linkedin.com/in/nico-axtmann" target="_blank">Nico Axtmann</a>.
|
||||
|
||||
* <a href="https://geekflare.com/python-asynchronous-web-frameworks/" target="_blank">Top 5 Asynchronous Web Frameworks for Python</a> by <a href="https://geekflare.com/author/ankush/" target="_blank">Ankush Thakur</a> on <a href="https://geekflare.com" target="_blank">GeekFlare</a>.
|
||||
|
||||
* <a href="https://medium.com/@gntrm/jwt-authentication-with-fastapi-and-aws-cognito-1333f7f2729e" target="_blank">JWT Authentication with FastAPI and AWS Cognito</a> by <a href="https://twitter.com/gntrm" target="_blank">Johannes Gontrum</a>.
|
||||
|
||||
* <a href="https://towardsdatascience.com/how-to-deploy-a-machine-learning-model-dc51200fe8cf" target="_blank">How to Deploy a Machine Learning Model</a> by <a href="https://www.linkedin.com/in/mgrootendorst/" target="_blank">Maarten Grootendorst</a> on <a href="https://towardsdatascience.com/" target="_blank">Towards Data Science</a>.
|
||||
|
||||
* <a href="https://eng.uber.com/ludwig-v0-2/" target="_blank">Uber: Ludwig v0.2 Adds New Features and Other Improvements to its Deep Learning Toolbox [including a FastAPI server]</a> on <a href="https://eng.uber.com" target="_blank">Uber Engineering</a>.
|
||||
|
||||
### Japanese
|
||||
|
||||
* <a href="https://qiita.com/mtitg/items/47770e9a562dd150631d" target="_blank">FastAPI|DB接続してCRUDするPython製APIサーバーを構築</a> by <a href="https://qiita.com/mtitg" target="_blank">@mtitg</a>.
|
||||
|
||||
@@ -63,8 +63,19 @@ The key features are:
|
||||
|
||||
---
|
||||
|
||||
"*If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]*"
|
||||
|
||||
"*We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]*"
|
||||
|
||||
<div style="text-align: right; margin-right: 10%;">Ines Montani - Matthew Honnibal - <strong><a href="https://explosion.ai" target="_blank">Explosion AI</a> founders - <a href="https://spacy.io" target="_blank">spaCy</a> creators</strong> <a href="https://twitter.com/_inesmontani/status/1144173225322143744" target="_blank"><small>(ref)</small></a> - <a href="https://twitter.com/honnibal/status/1144031421859655680" target="_blank"><small>(ref)</small></a></div>
|
||||
|
||||
---
|
||||
|
||||
"*We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]*"
|
||||
|
||||
<div style="text-align: right; margin-right: 10%;">Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - <strong>Uber</strong> <a href="https://eng.uber.com/ludwig-v0-2/" target="_blank"><small>(ref)</small></a></div>
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -1,5 +1,51 @@
|
||||
## Latest changes
|
||||
|
||||
## 0.41.0
|
||||
|
||||
* Upgrade required Starlette to `0.12.9`, the new range is `>=0.12.9,<=0.12.9`.
|
||||
* Add `State` to FastAPI apps at `app.state`.
|
||||
* PR [#593](https://github.com/tiangolo/fastapi/pull/593).
|
||||
* Improve handling of custom classes for `Request`s and `APIRoute`s.
|
||||
* This helps to more easily solve use cases like:
|
||||
* Reading a body before and/or after a request (equivalent to a middleware).
|
||||
* Run middleware-like code only for a subset of *path operations*.
|
||||
* Process a request before passing it to a *path operation function*. E.g. decompressing, deserializing, etc.
|
||||
* Processing a response after being generated by *path operation functions* but before returning it. E.g. adding custom headers, logging, adding extra metadata.
|
||||
* New docs section: [Custom Request and APIRoute class](https://fastapi.tiangolo.com/tutorial/custom-request-and-route/).
|
||||
* PR [#589](https://github.com/tiangolo/fastapi/pull/589) by [@dmontagu](https://github.com/dmontagu).
|
||||
* Fix preserving custom route class in routers when including other sub-routers. PR [#538](https://github.com/tiangolo/fastapi/pull/538) by [@dmontagu](https://github.com/dmontagu).
|
||||
|
||||
## 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.38.0
|
||||
|
||||
* Add recent articles to [External Links](https://fastapi.tiangolo.com/external-links/) and recent opinions. PR [#490](https://github.com/tiangolo/fastapi/pull/490).
|
||||
* Upgrade support range for Starlette to include `0.12.8`. The new range is `>=0.11.1,<=0.12.8"`. PR [#477](https://github.com/tiangolo/fastapi/pull/477) by [@dmontagu](https://github.com/dmontagu).
|
||||
* Upgrade support to Pydantic version 0.32.2 and update internal code to use it (breaking change). PR [#463](https://github.com/tiangolo/fastapi/pull/463) by [@dmontagu](https://github.com/dmontagu).
|
||||
|
||||
## 0.37.0
|
||||
|
||||
* Add support for custom route classes for advanced use cases. PR [#468](https://github.com/tiangolo/fastapi/pull/468) by [@dmontagu](https://github.com/dmontagu).
|
||||
|
||||
37
docs/src/custom_request_and_route/tutorial001.py
Normal file
37
docs/src/custom_request_and_route/tutorial001.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import gzip
|
||||
from typing import Callable, List
|
||||
|
||||
from fastapi import Body, FastAPI
|
||||
from fastapi.routing import APIRoute
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
|
||||
class GzipRequest(Request):
|
||||
async def body(self) -> bytes:
|
||||
if not hasattr(self, "_body"):
|
||||
body = await super().body()
|
||||
if "gzip" in self.headers.getlist("Content-Encoding"):
|
||||
body = gzip.decompress(body)
|
||||
self._body = body
|
||||
return self._body
|
||||
|
||||
|
||||
class GzipRoute(APIRoute):
|
||||
def get_route_handler(self) -> Callable:
|
||||
original_route_handler = super().get_route_handler()
|
||||
|
||||
async def custom_route_handler(request: Request) -> Response:
|
||||
request = GzipRequest(request.scope, request.receive)
|
||||
return await original_route_handler(request)
|
||||
|
||||
return custom_route_handler
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
app.router.route_class = GzipRoute
|
||||
|
||||
|
||||
@app.post("/sum")
|
||||
async def sum_numbers(numbers: List[int] = Body(...)):
|
||||
return {"sum": sum(numbers)}
|
||||
31
docs/src/custom_request_and_route/tutorial002.py
Normal file
31
docs/src/custom_request_and_route/tutorial002.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from typing import Callable, List
|
||||
|
||||
from fastapi import Body, FastAPI, HTTPException
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.routing import APIRoute
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
|
||||
class ValidationErrorLoggingRoute(APIRoute):
|
||||
def get_route_handler(self) -> Callable:
|
||||
original_route_handler = super().get_route_handler()
|
||||
|
||||
async def custom_route_handler(request: Request) -> Response:
|
||||
try:
|
||||
return await original_route_handler(request)
|
||||
except RequestValidationError as exc:
|
||||
body = await request.body()
|
||||
detail = {"errors": exc.errors(), "body": body.decode()}
|
||||
raise HTTPException(status_code=422, detail=detail)
|
||||
|
||||
return custom_route_handler
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
app.router.route_class = ValidationErrorLoggingRoute
|
||||
|
||||
|
||||
@app.post("/")
|
||||
async def sum_numbers(numbers: List[int] = Body(...)):
|
||||
return sum(numbers)
|
||||
41
docs/src/custom_request_and_route/tutorial003.py
Normal file
41
docs/src/custom_request_and_route/tutorial003.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import time
|
||||
from typing import Callable
|
||||
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from fastapi.routing import APIRoute
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
|
||||
class TimedRoute(APIRoute):
|
||||
def get_route_handler(self) -> Callable:
|
||||
original_route_handler = super().get_route_handler()
|
||||
|
||||
async def custom_route_handler(request: Request) -> Response:
|
||||
before = time.time()
|
||||
response: Response = await original_route_handler(request)
|
||||
duration = time.time() - before
|
||||
response.headers["X-Response-Time"] = str(duration)
|
||||
print(f"route duration: {duration}")
|
||||
print(f"route response: {response}")
|
||||
print(f"route response headers: {response.headers}")
|
||||
return response
|
||||
|
||||
return custom_route_handler
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
router = APIRouter(route_class=TimedRoute)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def not_timed():
|
||||
return {"message": "Not timed"}
|
||||
|
||||
|
||||
@router.get("/timed")
|
||||
async def timed():
|
||||
return {"message": "It's the time of my life"}
|
||||
|
||||
|
||||
app.include_router(router)
|
||||
@@ -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
|
||||
100
docs/tutorial/custom-request-and-route.md
Normal file
100
docs/tutorial/custom-request-and-route.md
Normal file
@@ -0,0 +1,100 @@
|
||||
In some cases, you may want to override the logic used by the `Request` and `APIRoute` classes.
|
||||
|
||||
In particular, this may be a good alternative to logic in a middleware.
|
||||
|
||||
For example, if you want to read or manipulate the request body before it is processed by your application.
|
||||
|
||||
!!! danger
|
||||
This is an "advanced" feature.
|
||||
|
||||
If you are just starting with **FastAPI** you might want to skip this section.
|
||||
|
||||
## Use cases
|
||||
|
||||
Some use cases include:
|
||||
|
||||
* Converting non-JSON request bodies to JSON (e.g. [`msgpack`](https://msgpack.org/index.html)).
|
||||
* Decompressing gzip-compressed request bodies.
|
||||
* Automatically logging all request bodies.
|
||||
* Accessing the request body in an exception handler.
|
||||
|
||||
## Handling custom request body encodings
|
||||
|
||||
Let's see how to make use of a custom `Request` subclass to decompress gzip requests.
|
||||
|
||||
And an `APIRoute` subclass to use that custom request class.
|
||||
|
||||
### Create a custom `GzipRequest` class
|
||||
|
||||
First, we create a `GzipRequest` class, which will overwrite the `Request.body()` method to decompress the body in the presence of an appropriate header.
|
||||
|
||||
If there's no `gzip` in the header, it will not try to decompress the body.
|
||||
|
||||
That way, the same route class can handle gzip compressed or uncompressed requests.
|
||||
|
||||
```Python hl_lines="10 11 12 13 14 15 16 17"
|
||||
{!./src/custom_request_and_route/tutorial001.py!}
|
||||
```
|
||||
|
||||
### Create a custom `GzipRoute` class
|
||||
|
||||
Next, we create a custom subclass of `fastapi.routing.APIRoute` that will make use of the `GzipRequest`.
|
||||
|
||||
This time, it will overwrite the method `APIRoute.get_route_handler()`.
|
||||
|
||||
This method returns a function. And that function is what will receive a request and return a response.
|
||||
|
||||
Here we use it to create a `GzipRequest` from the original request.
|
||||
|
||||
```Python hl_lines="20 21 22 23 24 25 26 27 28"
|
||||
{!./src/custom_request_and_route/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! note "Technical Details"
|
||||
A `Request` has a `request.scope` attribute, that's just a Python `dict` containing the metadata related to the request.
|
||||
|
||||
A `Request` also has a `request.receive`, that's a function to "receive" the body of the request.
|
||||
|
||||
The `scope` `dict` and `receive` function are both part of the ASGI specification.
|
||||
|
||||
And those two things, `scope` and `receive`, are what is needed to create a new `Request` instance.
|
||||
|
||||
To learn more about the `Request` check <a href="https://www.starlette.io/requests/" target="_blank">Starlette's docs about Requests</a>.
|
||||
|
||||
The only thing the function returned by `GzipRequest.get_route_handler` does differently is convert the `Request` to a `GzipRequest`.
|
||||
|
||||
Doing this, our `GzipRequest` will take care of decompressing the data (if necessary) before passing it to our *path operations*.
|
||||
|
||||
After that, all of the processing logic is the same.
|
||||
|
||||
But because of our changes in `GzipRequest.body`, the request body will be automatically decompressed when it is loaded by **FastAPI** when needed.
|
||||
|
||||
## Accessing the request body in an exception handler
|
||||
|
||||
We can also use this same approach to access the request body in an exception handler.
|
||||
|
||||
All we need to do is handle the request inside a `try`/`except` block:
|
||||
|
||||
```Python hl_lines="15 17"
|
||||
{!./src/custom_request_and_route/tutorial002.py!}
|
||||
```
|
||||
|
||||
If an exception occurs, the`Request` instance will still be in scope, so we can read and make use of the request body when handling the error:
|
||||
|
||||
```Python hl_lines="18 19 20"
|
||||
{!./src/custom_request_and_route/tutorial002.py!}
|
||||
```
|
||||
|
||||
## Custom `APIRoute` class in a router
|
||||
|
||||
You can also set the `route_class` parameter of an `APIRouter`:
|
||||
|
||||
```Python hl_lines="25"
|
||||
{!./src/custom_request_and_route/tutorial003.py!}
|
||||
```
|
||||
|
||||
In this example, the *path operations* under the `router` will use the custom `TimedRoute` class, and will have an extra `X-Response-Time` header in the response with the time it took to generate the response:
|
||||
|
||||
```Python hl_lines="15 16 17 18 19"
|
||||
{!./src/custom_request_and_route/tutorial003.py!}
|
||||
```
|
||||
@@ -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.37.0"
|
||||
__version__ = "0.41.0"
|
||||
|
||||
from starlette.background import BackgroundTasks
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Type, Union
|
||||
from typing import Any, Callable, Dict, List, Optional, Sequence, Type, Union
|
||||
|
||||
from fastapi import routing
|
||||
from fastapi.encoders import DictIntStrAny, SetIntStr
|
||||
from fastapi.exception_handlers import (
|
||||
http_exception_handler,
|
||||
request_validation_exception_handler,
|
||||
@@ -14,6 +15,7 @@ from fastapi.openapi.docs import (
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
from fastapi.params import Depends
|
||||
from starlette.applications import Starlette
|
||||
from starlette.datastructures import State
|
||||
from starlette.exceptions import ExceptionMiddleware, HTTPException
|
||||
from starlette.middleware.errors import ServerErrorMiddleware
|
||||
from starlette.requests import Request
|
||||
@@ -32,12 +34,16 @@ 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.state = State()
|
||||
self.router: routing.APIRouter = routing.APIRouter(
|
||||
routes, dependency_overrides_provider=self
|
||||
)
|
||||
@@ -54,6 +60,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] = {}
|
||||
|
||||
@@ -95,6 +102,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)
|
||||
@@ -138,12 +146,12 @@ class FastAPI(Starlette):
|
||||
deprecated: bool = None,
|
||||
methods: List[str] = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
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(
|
||||
@@ -165,7 +173,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,
|
||||
)
|
||||
|
||||
@@ -184,12 +192,12 @@ class FastAPI(Starlette):
|
||||
deprecated: bool = None,
|
||||
methods: List[str] = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
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:
|
||||
@@ -212,7 +220,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
|
||||
@@ -239,6 +247,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,
|
||||
@@ -246,6 +255,8 @@ class FastAPI(Starlette):
|
||||
tags=tags,
|
||||
dependencies=dependencies,
|
||||
responses=responses or {},
|
||||
default_response_class=default_response_class
|
||||
or self.default_response_class,
|
||||
)
|
||||
|
||||
def get(
|
||||
@@ -262,12 +273,12 @@ class FastAPI(Starlette):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
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(
|
||||
@@ -287,7 +298,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,
|
||||
)
|
||||
|
||||
@@ -305,12 +316,12 @@ class FastAPI(Starlette):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
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(
|
||||
@@ -330,7 +341,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,
|
||||
)
|
||||
|
||||
@@ -348,12 +359,12 @@ class FastAPI(Starlette):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
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(
|
||||
@@ -373,7 +384,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,
|
||||
)
|
||||
|
||||
@@ -391,12 +402,12 @@ class FastAPI(Starlette):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
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(
|
||||
@@ -416,7 +427,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,
|
||||
)
|
||||
|
||||
@@ -434,12 +445,12 @@ class FastAPI(Starlette):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
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(
|
||||
@@ -459,7 +470,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,
|
||||
)
|
||||
|
||||
@@ -477,12 +488,12 @@ class FastAPI(Starlette):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
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(
|
||||
@@ -502,7 +513,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,
|
||||
)
|
||||
|
||||
@@ -520,12 +531,12 @@ class FastAPI(Starlette):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
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(
|
||||
@@ -545,7 +556,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,
|
||||
)
|
||||
|
||||
@@ -563,12 +574,12 @@ class FastAPI(Starlette):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
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(
|
||||
@@ -588,6 +599,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,15 +1,18 @@
|
||||
from enum import Enum
|
||||
from types import GeneratorType
|
||||
from typing import Any, List, Set
|
||||
from typing import Any, Dict, List, Set, Union
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic.json import ENCODERS_BY_TYPE
|
||||
|
||||
SetIntStr = Set[Union[int, str]]
|
||||
DictIntStrAny = Dict[Union[int, str], Any]
|
||||
|
||||
|
||||
def jsonable_encoder(
|
||||
obj: Any,
|
||||
include: Set[str] = None,
|
||||
exclude: Set[str] = set(),
|
||||
include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
by_alias: bool = True,
|
||||
skip_defaults: bool = False,
|
||||
include_none: bool = True,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
from typing import Any
|
||||
from typing import Any, Sequence
|
||||
|
||||
from pydantic import ValidationError
|
||||
from pydantic.error_wrappers import ErrorList
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.websockets import WebSocket
|
||||
|
||||
|
||||
class HTTPException(StarletteHTTPException):
|
||||
@@ -13,8 +16,10 @@ class HTTPException(StarletteHTTPException):
|
||||
|
||||
|
||||
class RequestValidationError(ValidationError):
|
||||
pass
|
||||
def __init__(self, errors: Sequence[ErrorList]) -> None:
|
||||
super().__init__(errors, Request)
|
||||
|
||||
|
||||
class WebSocketRequestValidationError(ValidationError):
|
||||
pass
|
||||
def __init__(self, errors: Sequence[ErrorList]) -> None:
|
||||
super().__init__(errors, WebSocket)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,7 +11,7 @@ from fastapi.dependencies.utils import (
|
||||
get_parameterless_sub_dependant,
|
||||
solve_dependencies,
|
||||
)
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder
|
||||
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
|
||||
from fastapi.utils import create_cloned_field, generate_operation_id_for_path
|
||||
from pydantic import BaseConfig, BaseModel, Schema
|
||||
@@ -38,8 +38,8 @@ def serialize_response(
|
||||
*,
|
||||
field: Field = None,
|
||||
response: Response,
|
||||
include: Set[str] = None,
|
||||
exclude: Set[str] = set(),
|
||||
include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
by_alias: bool = True,
|
||||
skip_defaults: bool = False,
|
||||
) -> Any:
|
||||
@@ -53,7 +53,7 @@ def serialize_response(
|
||||
elif isinstance(errors_, list):
|
||||
errors.extend(errors_)
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
raise ValidationError(errors, field.type_)
|
||||
return jsonable_encoder(
|
||||
value,
|
||||
include=include,
|
||||
@@ -65,14 +65,14 @@ def serialize_response(
|
||||
return jsonable_encoder(response)
|
||||
|
||||
|
||||
def get_app(
|
||||
def get_request_handler(
|
||||
dependant: Dependant,
|
||||
body_field: Field = None,
|
||||
status_code: int = 200,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_field: Field = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
dependency_overrides_provider: Any = None,
|
||||
@@ -195,12 +195,12 @@ class APIRoute(routing.Route):
|
||||
name: str = None,
|
||||
methods: Optional[Union[Set[str], List[str]]] = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
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 = {}
|
||||
@@ -294,19 +294,20 @@ class APIRoute(routing.Route):
|
||||
)
|
||||
self.body_field = get_body_field(dependant=self.dependant, name=self.unique_id)
|
||||
self.dependency_overrides_provider = dependency_overrides_provider
|
||||
self.app = request_response(
|
||||
get_app(
|
||||
dependant=self.dependant,
|
||||
body_field=self.body_field,
|
||||
status_code=self.status_code,
|
||||
response_class=self.response_class,
|
||||
response_field=self.secure_cloned_response_field,
|
||||
response_model_include=self.response_model_include,
|
||||
response_model_exclude=self.response_model_exclude,
|
||||
response_model_by_alias=self.response_model_by_alias,
|
||||
response_model_skip_defaults=self.response_model_skip_defaults,
|
||||
dependency_overrides_provider=self.dependency_overrides_provider,
|
||||
)
|
||||
self.app = request_response(self.get_route_handler())
|
||||
|
||||
def get_route_handler(self) -> Callable:
|
||||
return get_request_handler(
|
||||
dependant=self.dependant,
|
||||
body_field=self.body_field,
|
||||
status_code=self.status_code,
|
||||
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,
|
||||
response_model_by_alias=self.response_model_by_alias,
|
||||
response_model_skip_defaults=self.response_model_skip_defaults,
|
||||
dependency_overrides_provider=self.dependency_overrides_provider,
|
||||
)
|
||||
|
||||
|
||||
@@ -341,15 +342,17 @@ class APIRouter(routing.Router):
|
||||
deprecated: bool = None,
|
||||
methods: Optional[Union[Set[str], List[str]]] = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
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,
|
||||
route_class_override: Optional[Type[APIRoute]] = None,
|
||||
) -> None:
|
||||
route = self.route_class(
|
||||
route_class = route_class_override or self.route_class
|
||||
route = route_class(
|
||||
path,
|
||||
endpoint=endpoint,
|
||||
response_model=response_model,
|
||||
@@ -389,12 +392,12 @@ class APIRouter(routing.Router):
|
||||
deprecated: bool = None,
|
||||
methods: List[str] = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
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 +448,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,8 +488,9 @@ 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,
|
||||
route_class_override=type(route),
|
||||
)
|
||||
elif isinstance(route, routing.Route):
|
||||
self.add_route(
|
||||
@@ -518,15 +523,14 @@ class APIRouter(routing.Router):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
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,
|
||||
@@ -563,12 +567,12 @@ class APIRouter(routing.Router):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
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(
|
||||
@@ -607,12 +611,12 @@ class APIRouter(routing.Router):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
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(
|
||||
@@ -651,12 +655,12 @@ class APIRouter(routing.Router):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
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(
|
||||
@@ -695,12 +699,12 @@ class APIRouter(routing.Router):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
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(
|
||||
@@ -739,12 +743,12 @@ class APIRouter(routing.Router):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
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(
|
||||
@@ -783,12 +787,12 @@ class APIRouter(routing.Router):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
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(
|
||||
@@ -827,12 +831,12 @@ class APIRouter(routing.Router):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
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
|
||||
|
||||
@@ -81,6 +81,7 @@ nav:
|
||||
- GraphQL: 'tutorial/graphql.md'
|
||||
- WebSockets: 'tutorial/websockets.md'
|
||||
- 'Events: startup - shutdown': 'tutorial/events.md'
|
||||
- Custom Request and APIRoute class: 'tutorial/custom-request-and-route.md'
|
||||
- Testing: 'tutorial/testing.md'
|
||||
- Testing Dependencies with Overrides: 'tutorial/testing-dependencies.md'
|
||||
- Debugging: 'tutorial/debugging.md'
|
||||
|
||||
@@ -19,8 +19,8 @@ classifiers = [
|
||||
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
|
||||
]
|
||||
requires = [
|
||||
"starlette >=0.11.1,<=0.12.7",
|
||||
"pydantic >=0.30,<=0.30.0"
|
||||
"starlette >=0.12.9,<=0.12.9",
|
||||
"pydantic >=0.32.2,<=0.32.2"
|
||||
]
|
||||
description-file = "README.md"
|
||||
requires-python = ">=3.6"
|
||||
@@ -39,6 +39,7 @@ test = [
|
||||
"email_validator",
|
||||
"sqlalchemy",
|
||||
"databases[sqlite]",
|
||||
"orjson"
|
||||
]
|
||||
doc = [
|
||||
"mkdocs",
|
||||
|
||||
114
tests/test_custom_route_class.py
Normal file
114
tests/test_custom_route_class.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import pytest
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from fastapi.routing import APIRoute
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class APIRouteA(APIRoute):
|
||||
x_type = "A"
|
||||
|
||||
|
||||
class APIRouteB(APIRoute):
|
||||
x_type = "B"
|
||||
|
||||
|
||||
class APIRouteC(APIRoute):
|
||||
x_type = "C"
|
||||
|
||||
|
||||
router_a = APIRouter(route_class=APIRouteA)
|
||||
router_b = APIRouter(route_class=APIRouteB)
|
||||
router_c = APIRouter(route_class=APIRouteC)
|
||||
|
||||
|
||||
@router_a.get("/")
|
||||
def get_a():
|
||||
return {"msg": "A"}
|
||||
|
||||
|
||||
@router_b.get("/")
|
||||
def get_b():
|
||||
return {"msg": "B"}
|
||||
|
||||
|
||||
@router_c.get("/")
|
||||
def get_c():
|
||||
return {"msg": "C"}
|
||||
|
||||
|
||||
router_b.include_router(router=router_c, prefix="/c")
|
||||
router_a.include_router(router=router_b, prefix="/b")
|
||||
app.include_router(router=router_a, prefix="/a")
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/a/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Get A",
|
||||
"operationId": "get_a_a__get",
|
||||
}
|
||||
},
|
||||
"/a/b/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Get B",
|
||||
"operationId": "get_b_a_b__get",
|
||||
}
|
||||
},
|
||||
"/a/b/c/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Get C",
|
||||
"operationId": "get_c_a_b_c__get",
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path,expected_status,expected_response",
|
||||
[
|
||||
("/a", 200, {"msg": "A"}),
|
||||
("/a/b", 200, {"msg": "B"}),
|
||||
("/a/b/c", 200, {"msg": "C"}),
|
||||
("/openapi.json", 200, openapi_schema),
|
||||
],
|
||||
)
|
||||
def test_get_path(path, expected_status, expected_response):
|
||||
response = client.get(path)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
|
||||
|
||||
def test_route_classes():
|
||||
routes = {}
|
||||
r: APIRoute
|
||||
for r in app.router.routes:
|
||||
routes[r.path] = r
|
||||
assert routes["/a/"].x_type == "A"
|
||||
assert routes["/a/b/"].x_type == "B"
|
||||
assert routes["/a/b/c/"].x_type == "C"
|
||||
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,34 @@
|
||||
import gzip
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from starlette.requests import Request
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from custom_request_and_route.tutorial001 import app
|
||||
|
||||
|
||||
@app.get("/check-class")
|
||||
async def check_gzip_request(request: Request):
|
||||
return {"request_class": type(request).__name__}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("compress", [True, False])
|
||||
def test_gzip_request(compress):
|
||||
n = 1000
|
||||
headers = {}
|
||||
body = [1] * n
|
||||
data = json.dumps(body).encode()
|
||||
if compress:
|
||||
data = gzip.compress(data)
|
||||
headers["Content-Encoding"] = "gzip"
|
||||
response = client.post("/sum", data=data, headers=headers)
|
||||
assert response.json() == {"sum": n}
|
||||
|
||||
|
||||
def test_request_class():
|
||||
response = client.get("/check-class")
|
||||
assert response.json() == {"request_class": "GzipRequest"}
|
||||
@@ -0,0 +1,27 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from custom_request_and_route.tutorial002 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_endpoint_works():
|
||||
response = client.post("/", json=[1, 2, 3])
|
||||
assert response.json() == 6
|
||||
|
||||
|
||||
def test_exception_handler_body_access():
|
||||
response = client.post("/", json={"numbers": [1, 2, 3]})
|
||||
|
||||
assert response.json() == {
|
||||
"detail": {
|
||||
"body": '{"numbers": [1, 2, 3]}',
|
||||
"errors": [
|
||||
{
|
||||
"loc": ["body", "numbers"],
|
||||
"msg": "value is not a valid list",
|
||||
"type": "type_error.list",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from custom_request_and_route.tutorial003 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_get():
|
||||
response = client.get("/")
|
||||
assert response.json() == {"message": "Not timed"}
|
||||
assert "X-Response-Time" not in response.headers
|
||||
|
||||
|
||||
def test_get_timed():
|
||||
response = client.get("/timed")
|
||||
assert response.json() == {"message": "It's the time of my life"}
|
||||
assert "X-Response-Time" in response.headers
|
||||
assert float(response.headers["X-Response-Time"]) > 0
|
||||
@@ -81,7 +81,7 @@ def test_get_validation_error():
|
||||
response = client.get("/items/foo")
|
||||
assert response.status_code == 400
|
||||
validation_error_str_lines = [
|
||||
b"1 validation error",
|
||||
b"1 validation error for Request",
|
||||
b"path -> item_id",
|
||||
b" value is not a valid integer (type=type_error.integer)",
|
||||
]
|
||||
|
||||
@@ -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": [],
|
||||
}
|
||||
@@ -106,8 +106,9 @@ def test_openapi():
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"ctx": {"enum_values": ["alexnet", "resnet", "lenet"]},
|
||||
"loc": ["path", "model_name"],
|
||||
"msg": "value is not a valid enumeration member",
|
||||
"msg": "value is not a valid enumeration member; permitted: 'alexnet', 'resnet', 'lenet'",
|
||||
"type": "type_error.enum",
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user