Compare commits

..

27 Commits

Author SHA1 Message Date
Sebastián Ramírez
90a5796b94 🔖 Release 0.43.0 2019-11-24 18:56:11 +01:00
Sebastián Ramírez
bb8a630fc3 📝 Update release notes 2019-11-24 15:12:56 +01:00
Nicolas Delaby
f5a503afae 📝 Replace guys by developers when a group of people is targeted (#645)
Just to make sure we include everyone, disregarding their gender.
2019-11-24 15:09:45 +01:00
Sebastián Ramírez
49fba853c2 📝 Update release notes 2019-11-24 15:06:31 +01:00
Steven Kalt
bac2f587b7 📝 Document overriding operationId for all path operations using their function names (#642) 2019-11-24 15:00:51 +01:00
Sebastián Ramírez
e1fd6785aa 📝 Update release notes 2019-11-24 14:25:51 +01:00
James Addison
4e50f53459 🐛 Fixing validator-caused incorrect output key order (#637) 2019-11-24 14:23:33 +01:00
Sebastián Ramírez
933d4327fb 📝 Update release notes 2019-11-24 14:18:03 +01:00
Daniel Brotsky
c7902dd23a Generate correct OpenAPI docs for responses with no content (#621) 2019-11-24 14:15:39 +01:00
Sebastián Ramírez
c5f5e63810 📝 Update release notes 2019-11-23 23:00:52 +01:00
Nico Stapelbroek
c3cc077fa9 📝 Remove $ sign from bash codeblocs in markdown (#613) 2019-11-23 22:59:15 +01:00
Sebastián Ramírez
c6f98c009f 📝 Update release notes 2019-11-23 22:57:47 +01:00
Sebastián Ramírez
e4206772cb 📝 Update release notes 2019-11-23 22:54:06 +01:00
svalouch
723ef07ccf 📝 Add documentation for self-serving static Swagger UI (#112) (#557) 2019-11-23 22:50:58 +01:00
François Voron
8609beb9ab 🚨 Fix black linting (#682) 2019-11-23 22:43:43 +01:00
Sebastián Ramírez
65536cbf63 🔖 Release version 0.42.0: Answer to the Ultimate Question of Life, the Universe, and Everything 2019-10-09 13:16:45 -05:00
Sebastián Ramírez
0192eab557 📝 Update release notes 2019-10-09 13:13:04 -05:00
Sebastián Ramírez
3f9f4a0f8f Add dependencies with yield (used as context managers) (#595)
*  Add development/testing dependencies for Python 3.6

*  Add concurrency submodule with contextmanager_in_threadpool

*  Add AsyncExitStack to ASGI scope in FastAPI app call

*  Use async stack for contextmanager-able dependencies

including running in threadpool sync dependencies

*  Add tests for contextmanager dependencies

including internal raise checks when exceptions should be handled and when not

*  Add test for fake asynccontextmanager raiser

* 🐛 Fix mypy errors and coverage

* 🔇 Remove development logs and prints

*  Add tests for sub-contextmanagers, background tasks, and sync functions

* 🐛 Fix mypy errors for Python 3.7

* 💬 Fix error texts for clarity

* 📝 Add docs for dependencies with yield

*  Update SQL with SQLAlchemy tutorial to use dependencies with yield

and add an alternative with a middleware (from the old tutorial)

*  Update SQL tests to remove DB file during the same tests

*  Add tests for example with middleware

as a copy from the tests with dependencies with yield, removing the DB in the tests

* ✏️ Fix typos with suggestions from code review

Co-Authored-By: dmontagu <35119617+dmontagu@users.noreply.github.com>
2019-10-09 13:01:58 -05:00
Sebastián Ramírez
380e3731a8 📝 Update release notes 2019-10-09 12:48:01 -05:00
Samuel Colvin
d6d99b86cb 🐛 Fix sitemap.xml in website, fix #597 (#598) 2019-10-09 12:45:44 -05:00
Sebastián Ramírez
5592fa0f6f 🔖 Release version 0.41.0 2019-10-07 06:44:07 -05:00
Sebastián Ramírez
b65be5d496 📝 Update release notes 2019-10-05 13:19:10 -05:00
Sebastián Ramírez
6c7da43e51 ⬆️ Upgrade Starlette to 0.12.9 and add State (#593) 2019-10-05 13:17:15 -05:00
Sebastián Ramírez
dfec2d7644 📝 Update release notes 2019-10-04 20:21:53 -05:00
dmontagu
8c3ef76139 Add better support for request body access/manipulation with custom classes (#589) 2019-10-04 19:23:34 -05:00
Sebastián Ramírez
7a504a721c 📝 Update release notes 2019-10-04 16:36:54 -05:00
dmontagu
dd963511d6 🐛 Fix preserving route_class when calling include_router (#538) 2019-10-04 16:35:20 -05:00
56 changed files with 2440 additions and 271 deletions

View File

@@ -25,7 +25,7 @@ sqlalchemy = "*"
uvicorn = "*"
[packages]
starlette = "==0.12.8"
starlette = "==0.12.9"
pydantic = "==0.32.2"
databases = {extras = ["sqlite"],version = "*"}
hypercorn = "*"

View File

@@ -90,13 +90,13 @@ FastAPI stands on the shoulders of giants:
## Installation
```bash
$ pip install fastapi
pip install fastapi
```
You will also need an ASGI server, for production such as <a href="http://www.uvicorn.org" target="_blank">Uvicorn</a> or <a href="https://gitlab.com/pgjones/hypercorn" target="_blank">Hypercorn</a>.
```bash
$ pip install uvicorn
pip install uvicorn
```
## Example

View File

@@ -142,12 +142,12 @@ Another big feature required by APIs is <abbr title="reading and converting to P
Webargs is a tool that was made to provide that on top of several frameworks, including Flask.
It uses Marshmallow underneath to do the data validation. And it was created by the same guys.
It uses Marshmallow underneath to do the data validation. And it was created by the same developers.
It's a great tool and I have used it a lot too, before having **FastAPI**.
!!! info
Webargs was created by the same Marshmallow guys.
Webargs was created by the same Marshmallow developers.
!!! check "Inspired **FastAPI** to"
Have automatic validation of incoming request data.
@@ -171,7 +171,7 @@ But then, we have again the problem of having a micro-syntax, inside of a Python
The editor can't help much with that. And if we modify parameters or Marshmallow schemas and forget to also modify that YAML docstring, the generated schema would be obsolete.
!!! info
APISpec was created by the same Marshmallow guys.
APISpec was created by the same Marshmallow developers.
!!! check "Inspired **FastAPI** to"
@@ -198,7 +198,7 @@ Using it led to the creation of several Flask full-stack generators. These are t
And these same full-stack generators were the base of the <a href="/project-generation/" target="_blank">**FastAPI** project generator</a>.
!!! info
Flask-apispec was created by the same Marshmallow guys.
Flask-apispec was created by the same Marshmallow developers.
!!! check "Inspired **FastAPI** to"
Generate the OpenAPI schema automatically, from the same code that defines serialization and validation.

View File

@@ -90,13 +90,13 @@ FastAPI stands on the shoulders of giants:
## Installation
```bash
$ pip install fastapi
pip install fastapi
```
You will also need an ASGI server, for production such as <a href="http://www.uvicorn.org" target="_blank">Uvicorn</a> or <a href="https://gitlab.com/pgjones/hypercorn" target="_blank">Hypercorn</a>.
```bash
$ pip install uvicorn
pip install uvicorn
```
## Example

View File

@@ -1,5 +1,45 @@
## Latest changes
## 0.43.0
* Update docs to reduce gender bias. PR [#645](https://github.com/tiangolo/fastapi/pull/645) by [@ticosax](https://github.com/ticosax).
* Add docs about [overriding the `operationId` for all the *path operations*](https://fastapi.tiangolo.com/tutorial/path-operation-advanced-configuration/#using-the-path-operation-function-name-as-the-operationid) based on their function name. PR [#642](https://github.com/tiangolo/fastapi/pull/642) by [@SKalt](https://github.com/SKalt).
* Fix validators in models generating an incorrect key order. PR [#637](https://github.com/tiangolo/fastapi/pull/637) by [@jaddison](https://github.com/jaddison).
* Generate correct OpenAPI docs for responses with no content. PR [#621](https://github.com/tiangolo/fastapi/pull/621) by [@brotskydotcom](https://github.com/brotskydotcom).
* Remove `$` from Bash code blocks in docs for consistency. PR [#613](https://github.com/tiangolo/fastapi/pull/613) by [@nstapelbroek](https://github.com/nstapelbroek).
* Add docs for [self-serving docs' (Swagger UI) static assets](https://fastapi.tiangolo.com/tutorial/extending-openapi/#self-hosting-javascript-and-css-for-docs), e.g. to use the docs offline, or without Internet. Initial PR [#557](https://github.com/tiangolo/fastapi/pull/557) by [@svalouch](https://github.com/svalouch).
* Fix `black` linting after upgrade. PR [#682](https://github.com/tiangolo/fastapi/pull/682) by [@frankie567](https://github.com/frankie567).
## 0.42.0
* Add dependencies with `yield`, a.k.a. exit steps, context managers, cleanup, teardown, ...
* This allows adding extra code after a dependency is done. It can be used, for example, to close database connections.
* Dependencies with `yield` can be normal or `async`, **FastAPI** will run normal dependencies in a threadpool.
* They can be combined with normal dependencies.
* It's possible to have arbitrary trees/levels of dependencies with `yield` and exit steps are handled in the correct order automatically.
* It works by default in Python 3.7 or above. For Python 3.6, it requires the extra backport dependencies:
* `async-exit-stack`
* `async-generator`
* New docs at [Dependencies with `yield`](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/).
* Updated database docs [SQL (Relational) Databases: Main **FastAPI** app](https://fastapi.tiangolo.com/tutorial/sql-databases/#main-fastapi-app).
* PR [#595](https://github.com/tiangolo/fastapi/pull/595).
* Fix `sitemap.xml` in website. PR [#598](https://github.com/tiangolo/fastapi/pull/598) by [@samuelcolvin](https://github.com/samuelcolvin).
## 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).

View 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)}

View 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)

View 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)

View File

@@ -1,21 +1,6 @@
from fastapi import Depends, FastAPI
app = FastAPI()
class FixedContentQueryChecker:
def __init__(self, fixed_content: str):
self.fixed_content = fixed_content
def __call__(self, q: str = ""):
if q:
return self.fixed_content in q
return False
checker = FixedContentQueryChecker("bar")
@app.get("/query-checker/")
async def read_query_check(fixed_content_included: bool = Depends(checker)):
return {"fixed_content_in_query": fixed_content_included}
async def get_db():
db = DBSession()
try:
yield db
finally:
db.close()

View File

@@ -0,0 +1,25 @@
from fastapi import Depends
async def dependency_a():
dep_a = generate_dep_a()
try:
yield dep_a
finally:
dep_a.close()
async def dependency_b(dep_a=Depends(dependency_a)):
dep_b = generate_dep_b()
try:
yield dep_b
finally:
dep_b.close(dep_a)
async def dependency_c(dep_b=Depends(dependency_b)):
dep_c = generate_dep_c()
try:
yield dep_c
finally:
dep_c.close(dep_b)

View File

@@ -0,0 +1,25 @@
from fastapi import Depends
async def dependency_a():
dep_a = generate_dep_a()
try:
yield dep_a
finally:
dep_a.close()
async def dependency_b(dep_a=Depends(dependency_a)):
dep_b = generate_dep_b()
try:
yield dep_b
finally:
dep_b.close(dep_a)
async def dependency_c(dep_b=Depends(dependency_b)):
dep_c = generate_dep_c()
try:
yield dep_c
finally:
dep_c.close(dep_b)

View File

@@ -0,0 +1,14 @@
class MySuperContextManager:
def __init__(self):
self.db = DBSession()
def __enter__(self):
return self.db
def __exit__(self, exc_type, exc_value, traceback):
self.db.close()
async def get_db():
with MySuperContextManager() as db:
yield db

View File

@@ -0,0 +1,21 @@
from fastapi import Depends, FastAPI
app = FastAPI()
class FixedContentQueryChecker:
def __init__(self, fixed_content: str):
self.fixed_content = fixed_content
def __call__(self, q: str = ""):
if q:
return self.fixed_content in q
return False
checker = FixedContentQueryChecker("bar")
@app.get("/query-checker/")
async def read_query_check(fixed_content_included: bool = Depends(checker)):
return {"fixed_content_in_query": fixed_content_included}

View File

@@ -0,0 +1,41 @@
from fastapi import FastAPI
from fastapi.openapi.docs import (
get_redoc_html,
get_swagger_ui_html,
get_swagger_ui_oauth2_redirect_html,
)
from starlette.staticfiles import StaticFiles
app = FastAPI(docs_url=None, redoc_url=None)
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/docs", include_in_schema=False)
async def custom_swagger_ui_html():
return get_swagger_ui_html(
openapi_url=app.openapi_url,
title=app.title + " - Swagger UI",
oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url,
swagger_js_url="/static/swagger-ui-bundle.js",
swagger_css_url="/static/swagger-ui.css",
)
@app.get(app.swagger_ui_oauth2_redirect_url, include_in_schema=False)
async def swagger_ui_redirect():
return get_swagger_ui_oauth2_redirect_html()
@app.get("/redoc", include_in_schema=False)
async def redoc_html():
return get_redoc_html(
openapi_url=app.openapi_url,
title=app.title + " - ReDoc",
redoc_js_url="/static/redoc.standalone.js",
)
@app.get("/users/{username}")
async def read_user(username: str):
return {"message": f"Hello {username}"}

View File

@@ -1,8 +1,24 @@
from fastapi import FastAPI
from fastapi.routing import APIRoute
app = FastAPI()
@app.get("/items/", include_in_schema=False)
@app.get("/items/")
async def read_items():
return [{"item_id": "Foo"}]
def use_route_names_as_operation_ids(app: FastAPI) -> None:
"""
Simplify operation IDs so that generated API clients have simpler function
names.
Should be called only after all routes have been added.
"""
for route in app.routes:
if isinstance(route, APIRoute):
route.operation_id = route.name # in this case, 'read_items'
use_route_names_as_operation_ids(app)

View File

@@ -1,30 +1,8 @@
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
@app.get("/items/", include_in_schema=False)
async def read_items():
return [{"item_id": "Foo"}]

View File

@@ -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

View File

@@ -0,0 +1,64 @@
from typing import List
from fastapi import Depends, FastAPI, HTTPException
from sqlalchemy.orm import Session
from starlette.requests import Request
from starlette.responses import Response
from . import crud, models, schemas
from .database import SessionLocal, engine
models.Base.metadata.create_all(bind=engine)
app = FastAPI()
@app.middleware("http")
async def db_session_middleware(request: Request, call_next):
response = Response("Internal server error", status_code=500)
try:
request.state.db = SessionLocal()
response = await call_next(request)
finally:
request.state.db.close()
return response
# Dependency
def get_db(request: Request):
return request.state.db
@app.post("/users/", response_model=schemas.User)
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
db_user = crud.get_user_by_email(db, email=user.email)
if db_user:
raise HTTPException(status_code=400, detail="Email already registered")
return crud.create_user(db=db, user=user)
@app.get("/users/", response_model=List[schemas.User])
def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
users = crud.get_users(db, skip=skip, limit=limit)
return users
@app.get("/users/{user_id}", response_model=schemas.User)
def read_user(user_id: int, db: Session = Depends(get_db)):
db_user = crud.get_user(db, user_id=user_id)
if db_user is None:
raise HTTPException(status_code=404, detail="User not found")
return db_user
@app.post("/users/{user_id}/items/", response_model=schemas.Item)
def create_item_for_user(
user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db)
):
return crud.create_user_item(db=db, item=item, user_id=user_id)
@app.get("/items/", response_model=List[schemas.Item])
def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
items = crud.get_items(db, skip=skip, limit=limit)
return items

View File

@@ -2,8 +2,6 @@ from typing import List
from fastapi import Depends, FastAPI, HTTPException
from sqlalchemy.orm import Session
from starlette.requests import Request
from starlette.responses import Response
from . import crud, models, schemas
from .database import SessionLocal, engine
@@ -13,20 +11,13 @@ models.Base.metadata.create_all(bind=engine)
app = FastAPI()
@app.middleware("http")
async def db_session_middleware(request: Request, call_next):
response = Response("Internal server error", status_code=500)
try:
request.state.db = SessionLocal()
response = await call_next(request)
finally:
request.state.db.close()
return response
# Dependency
def get_db(request: Request):
return request.state.db
def get_db():
try:
db = SessionLocal()
yield db
finally:
db.close()
@app.post("/users/", response_model=schemas.User)

View File

@@ -174,6 +174,11 @@ For example, you can add an additional media type of `image/png`, declaring that
!!! note
Notice that you have to return the image using a `FileResponse` directly.
!!! info
Unless you specify a different media type explicitly in your `responses` parameter, FastAPI will assume the response has the same media type as the main response class (default `application/json`).
But if you have specified a custom response class with `None` as its media type, FastAPI will use `application/json` for any additional response that has an associated model.
## Combining information
You can also combine response information from multiple places, including the `response_model`, `status_code`, and `responses` parameters.

View 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!}
```

View File

@@ -15,6 +15,9 @@ The contents that you return from your *path operation function* will be put ins
And if that `Response` has a JSON media type (`application/json`), like is the case with the `JSONResponse` and `UJSONResponse`, the data you return will be automatically converted (and filtered) with any Pydantic `response_model` that you declared in the *path operation decorator*.
!!! note
If you use a response class with no media type, FastAPI will expect your response to have no content, so it will not document the response format in its generated OpenAPI docs.
## Use `UJSONResponse`
For example, if you are squeezing performance, you can install and use `ujson` and set the response to be Starlette's `UJSONResponse`.

View File

@@ -1,4 +1,4 @@
!!! danger
!!! warning
This is, more or less, an "advanced" chapter.
If you are just starting with **FastAPI** you might want to skip this chapter and come back to it later.
@@ -22,7 +22,7 @@ Not the class itself (which is already a callable), but an instance of that clas
To do that, we declare a method `__call__`:
```Python hl_lines="10"
{!./src/dependencies/tutorial007.py!}
{!./src/dependencies/tutorial011.py!}
```
In this case, this `__call__` is what **FastAPI** will use to check for additional parameters and sub-dependencies, and this is what will be called to pass a value to the parameter in your *path operation function* later.
@@ -32,7 +32,7 @@ In this case, this `__call__` is what **FastAPI** will use to check for addition
And now, we can use `__init__` to declare the parameters of the instance that we can use to "parameterize" the dependency:
```Python hl_lines="7"
{!./src/dependencies/tutorial007.py!}
{!./src/dependencies/tutorial011.py!}
```
In this case, **FastAPI** won't ever touch or care about `__init__`, we will use it directly in our code.
@@ -42,7 +42,7 @@ In this case, **FastAPI** won't ever touch or care about `__init__`, we will use
We could create an instance of this class with:
```Python hl_lines="16"
{!./src/dependencies/tutorial007.py!}
{!./src/dependencies/tutorial011.py!}
```
And that way we are able to "parameterize" our dependency, that now has `"bar"` inside of it, as the attribute `checker.fixed_content`.
@@ -60,7 +60,7 @@ checker(q="somequery")
...and pass whatever that returns as the value of the dependency in our path operation function as the parameter `fixed_content_included`:
```Python hl_lines="20"
{!./src/dependencies/tutorial007.py!}
{!./src/dependencies/tutorial011.py!}
```
!!! tip

View File

@@ -0,0 +1,153 @@
# Dependencies with `yield`
FastAPI supports dependencies that do some <abbr title='sometimes also called "exit", "cleanup", "teardown", "close", "context managers", ...'>extra steps after finishing</abbr>.
To do this, use `yield` instead of `return`, and write the extra steps after.
!!! tip
Make sure to use `yield` one single time.
!!! info
For this to work, you need to use **Python 3.7** or above, or in **Python 3.6**, install the "backports":
```bash
pip install async-exit-stack async-generator
```
This installs <a href="https://github.com/sorcio/async_exit_stack" target="_blank">async-exit-stack</a> and <a href="https://github.com/python-trio/async_generator" target="_blank">async-generator</a>.
!!! note "Technical Details"
Any function that is valid to use with:
* <a href="https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager" target="_blank">`@contextlib.contextmanager`</a> or
* <a href="https://docs.python.org/3/library/contextlib.html#contextlib.asynccontextmanager" target="_blank">`@contextlib.asynccontextmanager`</a>
would be valid to use as a **FastAPI** dependency.
In fact, FastAPI uses those two decorators internally.
## A database dependency with `yield`
For example, you could use this to create a database session and close it after finishing.
Only the code prior to and including the `yield` statement is executed before sending a response:
```Python hl_lines="2 3 4"
{!./src/dependencies/tutorial007.py!}
```
The yielded value is what is injected into *path operations* and other dependencies:
```Python hl_lines="4"
{!./src/dependencies/tutorial007.py!}
```
The code following the `yield` statement is executed after the response has been delivered:
```Python hl_lines="5 6"
{!./src/dependencies/tutorial007.py!}
```
!!! tip
You can use `async` or normal functions.
**FastAPI** will do the right thing with each, the same as with normal dependencies.
## A dependency with `yield` and `try`
If you use a `try` block in a dependency with `yield`, you'll receive any exception that was thrown when using the dependency.
For example, if some code at some point in the middle, in another dependency or in a *path operation*, made a database transaction "rollback" or create any other error, you will receive the exception in your dependency.
So, you can look for that specific exception inside the dependency with `except SomeException`.
In the same way, you can use `finally` to make sure the exit steps are executed, no matter if there was an exception or not.
```Python hl_lines="3 5"
{!./src/dependencies/tutorial007.py!}
```
## Sub-dependencies with `yield`
You can have sub-dependencies and "trees" of sub-dependencies of any size and shape, and any or all of them can use `yield`.
**FastAPI** will make sure that the "exit code" in each dependency with `yield` is run in the correct order.
For example, `dependency_c` can have a dependency on `dependency_b`, and `dependency_b` on `dependency_a`:
```Python hl_lines="4 12 20"
{!./src/dependencies/tutorial008.py!}
```
And all of them can use `yield`.
In this case `dependency_c`, to execute its exit code, needs the value from `dependency_b` (here named `dep_b`) to still be available.
And, in turn, `dependency_b` needs the value from `dependency_a` (here named `dep_a`) to be available for its exit code.
```Python hl_lines="16 17 24 25"
{!./src/dependencies/tutorial008.py!}
```
The same way, you could have dependencies with `yield` and `return` mixed.
And you could have a single dependency that requires several other dependencies with `yield`, etc.
You can have any combinations of dependencies that you want.
**FastAPI** will make sure everything is run in the correct order.
!!! note "Technical Details"
This works thanks to Python's <a href="https://docs.python.org/3/library/contextlib.html" target="_blank">Context Managers</a>.
**FastAPI** uses them internally to achieve this.
## Context Managers
### What are "Context Managers"
"Context Managers" are any of those Python objects that you can use in a `with` statement.
For example, <a href="https://docs.python.org/3/tutorial/inputoutput.html#reading-and-writing-files" target="_blank">you can use `with` to read a file</a>:
```Python
with open("./somefile.txt") as f:
contents = f.read()
print(contents)
```
Underneath, the `open("./somefile.txt")` returns an object that is a called a "Context Manager".
When the `with` block finishes, it makes sure to close the file, even if there were exceptions.
When you create a dependency with `yield`, **FastAPI** will internally convert it to a context manager, and combine it with some other related tools.
### Using context managers in dependencies with `yield`
!!! warning
This is, more or less, an "advanced" idea.
If you are just starting with **FastAPI** you might want to skip it for now.
In Python, you can create context managers by <a href="https://docs.python.org/3/reference/datamodel.html#context-managers" target="_blank">creating a class with two methods: `__enter__()` and `__exit__()`</a>.
You can also use them with **FastAPI** dependencies with `yield` by using
`with` or `async with` statements inside of the dependency function:
```Python hl_lines="1 2 3 4 5 6 7 8 9 13"
{!./src/dependencies/tutorial010.py!}
```
!!! tip
Another way to create a context manager is with:
* <a href="https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager" target="_blank">`@contextlib.contextmanager`</a> or
* <a href="https://docs.python.org/3/library/contextlib.html#contextlib.asynccontextmanager" target="_blank">`@contextlib.asynccontextmanager`</a>
using them to decorate a function with a single `yield`.
That's what **FastAPI** uses internally for dependencies with `yield`.
But you don't have to use the decorators for FastAPI dependencies (and you shouldn't).
FastAPI will do it for you internally.

View File

@@ -88,3 +88,156 @@ Now you can replace the `.openapi()` method with your new function.
Once you go to <a href="http://127.0.0.1:8000/redoc" target="_blank">http://127.0.0.1:8000/redoc</a> you will see that you are using your custom logo (in this example, **FastAPI**'s logo):
<img src="/img/tutorial/extending-openapi/image01.png">
## Self-hosting JavaScript and CSS for docs
The API docs use **Swagger UI** and **ReDoc**, and each of those need some JavaScript and CSS files.
By default, those files are served from a <abbr title="Content Delivery Network: A service, normally composed of several servers, that provides static files, like JavaScript and CSS. It's commonly used to serve those files from the server closer to the client, improving performance.">CDN</abbr>.
But it's possible to customize it, you can set a specific CDN, or serve the files yourself.
That's useful, for example, if you need your app to keep working even while offline, without open Internet access, or in a local network.
Here you'll see how to serve those files yourself, in the same FastAPI app, and configure the docs to use them.
### Project file structure
Let's say your project file structure looks like this:
```
.
├── app
│ ├── __init__.py
│ ├── main.py
```
Now create a directory to store those static files.
Your new file structure could look like this:
```
.
├── app
│   ├── __init__.py
│   ├── main.py
└── static/
```
### Download the files
Download the static files needed for the docs and put them on that `static/` directory.
You can probably right-click each link and select an option similar to `Save link as...`.
**Swagger UI** uses the files:
* <a href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui-bundle.js">`swagger-ui-bundle.js`</a>
* <a href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui.css" target="_blank">`swagger-ui.css`</a>
And **ReDoc** uses the file:
* <a href="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js" target="_blank">`redoc.standalone.js`</a>
After that, your file structure could look like:
```
.
├── app
│   ├── __init__.py
│   ├── main.py
└── static
├── redoc.standalone.js
├── swagger-ui-bundle.js
└── swagger-ui.css
```
### Install `aiofiles`
Now you need to install `aiofiles`:
```bash
pip install aiofiles
```
### Serve the static files
* Import `StaticFiles` from Starlette.
* "Mount" it the same way you would <a href="https://fastapi.tiangolo.com/tutorial/sub-applications-proxy/" target="_blank">mount a Sub-Application</a>.
```Python hl_lines="7 11"
{!./src/extending_openapi/tutorial002.py!}
```
### Test the static files
Start your application and go to <a href="http://127.0.0.1:8000/static/redoc.standalone.js" target="_blank">http://127.0.0.1:8000/static/redoc.standalone.js</a>.
You should see a very long JavaScript file for **ReDoc**.
It could start with something like:
```JavaScript
/*!
* ReDoc - OpenAPI/Swagger-generated API Reference Documentation
* -------------------------------------------------------------
* Version: "2.0.0-rc.18"
* Repo: https://github.com/Redocly/redoc
*/
!function(e,t){"object"==typeof exports&&"object"==typeof m
...
```
That confirms that you are being able to serve static files from your app, and that you placed the static files for the docs in the correct place.
Now we can configure the app to use those static files for the docs.
### Disable the automatic docs
The first step is to disable the automatic docs, as those use the CDN by default.
To disable them, set their URLs to `None` when creating your `FastAPI` app:
```Python hl_lines="9"
{!./src/extending_openapi/tutorial002.py!}
```
### Include the custom docs
Now you can create the *path operations* for the custom docs.
You can re-use FastAPI's internal functions to create the HTML pages for the docs, and pass them the needed arguments:
* `openapi_url`: the URL where the HTML page for the docs can get the OpenAPI schema for your API. You can use here the attribute `app.openapi_url`.
* `title`: the title of your API.
* `oauth2_redirect_url`: you can use `app.swagger_ui_oauth2_redirect_url` here to use the default.
* `swagger_js_url`: the URL where the HTML for your Swagger UI docs can get the **JavaScript** file. This is the one that your own app is now serving.
* `swagger_css_url`: the URL where the HTML for your Swagger UI docs can get the **CSS** file. This is the one that your own app is now serving.
And similarly for ReDoc...
```Python hl_lines="2 3 4 5 6 14 15 16 17 18 19 20 21 22 25 26 27 30 31 32 33 34 35 36"
{!./src/extending_openapi/tutorial002.py!}
```
!!! tip
The *path operation* for `swagger_ui_redirect` is a helper for when you use OAuth2.
If you integrate your API with an OAuth2 provider, you will be able to authenticate and come back to the API docs with the acquired credentials. And interact with it using the real OAuth2 authentication.
Swagger UI will handle it behind the scenes for you, but it needs this "redirect" helper.
### Create a *path operation* to test it
Now, to be able to test that everything works, create a path operation:
```Python hl_lines="39 40 41"
{!./src/extending_openapi/tutorial002.py!}
```
### Test it
Now, you should be able to disconnect your WiFi, go to your docs at <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a>, and reload the page.
And even without Internet, you would be able to see the docs for your API and interact with it.

View File

@@ -11,12 +11,30 @@ You would have to make sure that it is unique for each operation.
{!./src/path_operation_advanced_configuration/tutorial001.py!}
```
### Using the *path operation function* name as the operationId
If you want to use your APIs' function names as `operationId`s, you can iterate over all of them and override each *path operation's* `operation_id` using their `APIRoute.name`.
You should do it after adding all your *path operations*.
```Python hl_lines="2 12 13 14 15 16 17 18 19 20 21 24"
{!./src/path_operation_advanced_configuration/tutorial002.py!}
```
!!! tip
If you manually call `app.openapi()`, you should update the `operationId`s before that.
!!! warning
If you do this, you have to make sure each one of your *path operation functions* has a unique name.
Even if they are in different modules (Python files).
## Exclude from OpenAPI
To exclude a path operation from the generated OpenAPI schema (and thus, from the automatic documentation systems), use the parameter `include_in_schema` and set it to `False`;
```Python hl_lines="6"
{!./src/path_operation_advanced_configuration/tutorial002.py!}
{!./src/path_operation_advanced_configuration/tutorial003.py!}
```
## Advanced description from docstring
@@ -28,5 +46,5 @@ Adding an `\f` (an escaped "form feed" character) causes **FastAPI** to truncate
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!}
{!./src/path_operation_advanced_configuration/tutorial004.py!}
```

View File

@@ -22,6 +22,10 @@ It will:
<img src="/img/tutorial/response-status-code/image01.png">
!!! note
Some response codes (see the next section) indicate that the response does not have a body.
FastAPI knows this, and will produce OpenAPI docs that state there is no response body.
## About HTTP status codes
@@ -34,11 +38,12 @@ These status codes have a name associated to recognize them, but the important p
In short:
* `100` and above are for "Information". You rarely use them directly.
* `100` and above are for "Information". You rarely use them directly. Responses with these status codes cannot have a body.
* **`200`** and above are for "Successful" responses. These are the ones you would use the most.
* `200` is the default status code, which means everything was "OK".
* Another example would be `201`, "Created". It is commonly used after creating a new record in the database.
* `300` and above are for "Redirection".
* A special case is `204`, "No Content". This response is used when there is no content to return to the client, and so the response must not have a body.
* **`300`** and above are for "Redirection". Responses with these status codes may or may not have a body, except for `304`, "Not Modified", which must not have one.
* **`400`** and above are for "Client error" responses. These are the second type you would probably use the most.
* An example is `404`, for a "Not Found" response.
* For generic errors from the client, you can just use `400`.

View File

@@ -427,21 +427,30 @@ And you would also use Alembic for "migrations" (that's its main job).
A "migration" is the set of steps needed whenever you change the structure of your SQLAlchemy models, add a new attribute, etc. to replicate those changes in the database, add a new column, a new table, etc.
### Create a middleware to handle sessions
### Create a dependency
Now use the `SessionLocal` class we created in the `sql_app/databases.py` file.
!!! info
For this to work, you need to use **Python 3.7** or above, or in **Python 3.6**, install the "backports":
```bash
pip install async-exit-stack async-generator
```
This installs <a href="https://github.com/sorcio/async_exit_stack" target="_blank">async-exit-stack</a> and <a href="https://github.com/python-trio/async_generator" target="_blank">async-generator</a>.
You can also use the alternative method with a "middleware" explained at the end.
Now use the `SessionLocal` class we created in the `sql_app/databases.py` file to create a dependency.
We need to have an independent database session/connection (`SessionLocal`) per request, use the same session through all the request and then close it after the request is finished.
And then a new session will be created for the next request.
For that, we will create a new middleware.
For that, we will create a new dependency with `yield`, as explained before in the section about <a href="https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/" target="_blank">Dependencies with `yield`</a>.
A "middleware" is a function that is always executed for each request, and have code before and after the request.
Our dependency will create a new SQLAlchemy `SessionLocal` that will be used in a single request, and then close it once the request is finished.
This middleware (just a function) will create a new SQLAlchemy `SessionLocal` for each request, add it to the request and then close it once the request is finished.
```Python hl_lines="16 17 18 19 20 21 22 23 24"
```Python hl_lines="15 16 17 18 19 20"
{!./src/sql_databases/sql_app/main.py!}
```
@@ -452,21 +461,11 @@ This middleware (just a function) will create a new SQLAlchemy `SessionLocal` fo
This way we make sure the database session is always closed after the request. Even if there was an exception while processing the request.
#### About `request.state`
And then, when using the dependency in a *path operation function*, we declare it with the type `Session` we imported directly from SQLAlchemy.
<a href="https://www.starlette.io/requests/#other-state" target="_blank">`request.state` is a property of each Starlette `Request` object</a>, it is there to store arbitrary objects attached to the request itself, like the database session in this case.
This will then give us better editor support inside the *path operation function*, because the editor will know that the `db` parameter is of type `Session`:
For us in this case, it helps us ensuring a single database session is used through all the request, and then closed afterwards (in the middleware).
### Create a dependency
To simplify the code, reduce repetition and get better editor support, we will create a dependency that returns this same database session from the request.
And when using the dependency in a path operation function, we declare it with the type `Session` we imported directly from SQLAlchemy.
This will then give us better editor support inside the path operation function, because the editor will know that the `db` parameter is of type `Session`.
```Python hl_lines="28 29"
```Python hl_lines="24 32 38 47 53"
{!./src/sql_databases/sql_app/main.py!}
```
@@ -479,22 +478,16 @@ This will then give us better editor support inside the path operation function,
Now, finally, here's the standard **FastAPI** *path operations* code.
```Python hl_lines="32 33 34 35 36 37 40 41 42 43 46 47 48 49 50 51 54 55 56 57 58 61 62 63 64 65"
```Python hl_lines="23 24 25 26 27 28 31 32 33 34 37 38 39 40 41 42 45 46 47 48 49 52 53 54 55"
{!./src/sql_databases/sql_app/main.py!}
```
We are creating the database session before each request, attaching it to the request, and then closing it afterwards.
We are creating the database session before each request in the dependency with `yield`, and then closing it afterwards.
All of this is done in the middleware explained above.
Then, in the dependency `get_db()` we are extracting the database session from the request.
And then we can create the dependency in the path operation function, to get that session directly.
And then we can create the required dependency in the path operation function, to get that session directly.
With that, we can just call `crud.get_user` directly from inside of the path operation function and use that session.
Having this 3-step process (middleware, dependency, path operation) you get better support/checks/completion in all the path operation functions while reducing code repetition.
!!! tip
Notice that the values you return are SQLAlchemy models, or lists of SQLAlchemy models.
@@ -507,7 +500,7 @@ Having this 3-step process (middleware, dependency, path operation) you get bett
### About `def` vs `async def`
Here we are using SQLAlchemy code inside of the path operation function, and, in turn, it will go and communicate with an external database.
Here we are using SQLAlchemy code inside of the path operation function and in the dependency, and, in turn, it will go and communicate with an external database.
That could potentially require some "waiting".
@@ -523,7 +516,7 @@ user = await db.query(User).first()
user = db.query(User).first()
```
Then we should declare the path operation without `async def`, just with a normal `def`, as:
Then we should declare the *path operation functions* and the dependency without `async def`, just with a normal `def`, as:
```Python hl_lines="2"
@app.get("/users/{user_id}", response_model=schemas.User)
@@ -548,8 +541,8 @@ For example, in a background task worker with <a href="http://www.celeryproject.
## Review all the files
Remember you should have a directory named `my_super_project` that contains a sub-directory called `sql_app`.
`sql_app` should have the following files:
`sql_app` should have the following files:
* `sql_app/__init__.py`: is an empty file.
@@ -591,9 +584,6 @@ You can copy this code and use it as is.
In fact, the code shown here is part of the tests. As most of the code in these docs.
You can copy it as is.
Then you can run it with Uvicorn:
```bash
@@ -615,3 +605,51 @@ It will look like this:
<img src="/img/tutorial/sql-databases/image02.png">
You can also use an online SQLite browser like <a href="https://inloop.github.io/sqlite-viewer/" target="_blank">SQLite Viewer</a> or <a href="https://extendsclass.com/sqlite-browser.html" target="_blank">ExtendsClass</a>.
## Alternative DB session with middleware
If you can't use dependencies with `yield` -- for example, if you are not using **Python 3.7** and can't install the "backports" mentioned above for **Python 3.6** -- you can set up the session in a "middleware" in a similar way.
A "middleware" is basically a function that is always executed for each request, with some code executed before, and some code executed after the endpoint function.
### Create a middleware
The middleware we'll add (just a function) will create a new SQLAlchemy `SessionLocal` for each request, add it to the request and then close it once the request is finished.
```Python hl_lines="16 17 18 19 20 21 22 23 24"
{!./src/sql_databases/sql_app/alt_main.py!}
```
!!! info
We put the creation of the `SessionLocal()` and handling of the requests in a `try` block.
And then we close it in the `finally` block.
This way we make sure the database session is always closed after the request. Even if there was an exception while processing the request.
### About `request.state`
<a href="https://www.starlette.io/requests/#other-state" target="_blank">`request.state` is a property of each Starlette `Request` object</a>. It is there to store arbitrary objects attached to the request itself, like the database session in this case.
For us in this case, it helps us ensure a single database session is used through all the request, and then closed afterwards (in the middleware).
### Dependencies with `yield` or middleware
Adding a **middleware** here is similar to what a dependency with `yield` does, with some differences:
* It requires more code and is a bit more complex.
* The middleware has to be an `async` function.
* If there is code in it that has to "wait" for the network, it could "block" your application there and degrade performance a bit.
* Although it's probably not very problematic here with the way `SQLAlchemy` works.
* But if you added more code to the middleware that had a lot of <abbr title="input and output">I/O</abbr> waiting, it could then be problematic.
* A middleware is run for *every* request.
* So, a connection will be created for every request.
* Even when the *path operation* that handles that request didn't need the DB.
!!! tip
It's probably better to use dependencies with `yield` when they are enough for the use case.
!!! info
Dependencies with `yield` were added recently to **FastAPI**.
A previous version of this tutorial only had the examples with a middleware and there are probably several applications using the middleware for database session management.

View File

@@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
__version__ = "0.40.0"
__version__ = "0.43.0"
from starlette.background import BackgroundTasks

View File

@@ -1,6 +1,7 @@
from typing import Any, Callable, Dict, List, Optional, Sequence, Type, Union
from fastapi import routing
from fastapi.concurrency import AsyncExitStack
from fastapi.encoders import DictIntStrAny, SetIntStr
from fastapi.exception_handlers import (
http_exception_handler,
@@ -15,11 +16,13 @@ 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
from starlette.responses import HTMLResponse, JSONResponse, Response
from starlette.routing import BaseRoute
from starlette.types import Receive, Scope, Send
class FastAPI(Starlette):
@@ -42,6 +45,7 @@ class FastAPI(Starlette):
) -> 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
)
@@ -128,6 +132,14 @@ class FastAPI(Starlette):
RequestValidationError, request_validation_exception_handler
)
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if AsyncExitStack:
async with AsyncExitStack() as stack:
scope["fastapi_astack"] = stack
await super().__call__(scope, receive, send)
else:
await super().__call__(scope, receive, send) # pragma: no cover
def add_api_route(
self,
path: str,

45
fastapi/concurrency.py Normal file
View File

@@ -0,0 +1,45 @@
from typing import Any, Callable
from starlette.concurrency import iterate_in_threadpool, run_in_threadpool # noqa
asynccontextmanager_error_message = """
FastAPI's contextmanager_in_threadpool require Python 3.7 or above,
or the backport for Python 3.6, installed with:
pip install async-generator
"""
def _fake_asynccontextmanager(func: Callable) -> Callable:
def raiser(*args: Any, **kwargs: Any) -> Any:
raise RuntimeError(asynccontextmanager_error_message)
return raiser
try:
from contextlib import asynccontextmanager # type: ignore
except ImportError:
try:
from async_generator import asynccontextmanager # type: ignore
except ImportError: # pragma: no cover
asynccontextmanager = _fake_asynccontextmanager
try:
from contextlib import AsyncExitStack # type: ignore
except ImportError:
try:
from async_exit_stack import AsyncExitStack # type: ignore
except ImportError: # pragma: no cover
AsyncExitStack = None # type: ignore
@asynccontextmanager
async def contextmanager_in_threadpool(cm: Any) -> Any:
try:
yield await run_in_threadpool(cm.__enter__)
except Exception as e:
ok = await run_in_threadpool(cm.__exit__, type(e), e, None)
if not ok:
raise e
else:
await run_in_threadpool(cm.__exit__, None, None, None)

View File

@@ -1,5 +1,6 @@
import asyncio
import inspect
from contextlib import contextmanager
from copy import deepcopy
from typing import (
Any,
@@ -16,6 +17,12 @@ from typing import (
)
from fastapi import params
from fastapi.concurrency import (
AsyncExitStack,
_fake_asynccontextmanager,
asynccontextmanager,
contextmanager_in_threadpool,
)
from fastapi.dependencies.models import Dependant, SecurityRequirement
from fastapi.security.base import SecurityBase
from fastapi.security.oauth2 import OAuth2, SecurityScopes
@@ -195,6 +202,18 @@ def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) ->
return annotation
async_contextmanager_dependencies_error = """
FastAPI dependencies with yield require Python 3.7 or above,
or the backports for Python 3.6, installed with:
pip install async-exit-stack async-generator
"""
def check_dependency_contextmanagers() -> None:
if AsyncExitStack is None or asynccontextmanager == _fake_asynccontextmanager:
raise RuntimeError(async_contextmanager_dependencies_error) # pragma: no cover
def get_dependant(
*,
path: str,
@@ -206,6 +225,8 @@ def get_dependant(
path_param_names = get_path_param_names(path)
endpoint_signature = get_typed_signature(call)
signature_params = endpoint_signature.parameters
if inspect.isgeneratorfunction(call) or inspect.isasyncgenfunction(call):
check_dependency_contextmanagers()
dependant = Dependant(call=call, name=name, path=path, use_cache=use_cache)
for param_name, param in signature_params.items():
if isinstance(param.default, params.Depends):
@@ -338,6 +359,16 @@ def is_coroutine_callable(call: Callable) -> bool:
return asyncio.iscoroutinefunction(call)
async def solve_generator(
*, call: Callable, stack: AsyncExitStack, sub_values: Dict[str, Any]
) -> Any:
if inspect.isgeneratorfunction(call):
cm = contextmanager_in_threadpool(contextmanager(call)(**sub_values))
elif inspect.isasyncgenfunction(call):
cm = asynccontextmanager(call)(**sub_values)
return await stack.enter_async_context(cm)
async def solve_dependencies(
*,
request: Union[Request, WebSocket],
@@ -397,9 +428,13 @@ async def solve_dependencies(
dependency_overrides_provider=dependency_overrides_provider,
dependency_cache=dependency_cache,
)
sub_values, sub_errors, background_tasks, sub_response, sub_dependency_cache = (
solved_result
)
(
sub_values,
sub_errors,
background_tasks,
sub_response,
sub_dependency_cache,
) = solved_result
sub_response = cast(Response, sub_response)
response.headers.raw.extend(sub_response.headers.raw)
if sub_response.status_code:
@@ -410,6 +445,15 @@ async def solve_dependencies(
continue
if sub_dependant.use_cache and sub_dependant.cache_key in dependency_cache:
solved = dependency_cache[sub_dependant.cache_key]
elif inspect.isgeneratorfunction(call) or inspect.isasyncgenfunction(call):
stack = request.scope.get("fastapi_astack")
if stack is None:
raise RuntimeError(
async_contextmanager_dependencies_error
) # pragma: no cover
solved = await solve_generator(
call=call, stack=stack, sub_values=sub_values
)
elif is_coroutine_callable(call):
solved = await call(**sub_values)
else:
@@ -436,7 +480,10 @@ 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( # 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)

View File

@@ -1,2 +1,3 @@
METHODS_WITH_BODY = set(("POST", "PUT", "DELETE", "PATCH"))
STATUS_CODES_WITH_NO_BODY = set((100, 101, 102, 103, 204, 304))
REF_PREFIX = "#/components/schemas/"

View File

@@ -5,7 +5,11 @@ from fastapi import routing
from fastapi.dependencies.models import Dependant
from fastapi.dependencies.utils import get_flat_dependant
from fastapi.encoders import jsonable_encoder
from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX
from fastapi.openapi.constants import (
METHODS_WITH_BODY,
REF_PREFIX,
STATUS_CODES_WITH_NO_BODY,
)
from fastapi.openapi.models import OpenAPI
from fastapi.params import Body, Param
from fastapi.utils import (
@@ -79,7 +83,7 @@ def get_openapi_security_definitions(flat_dependant: Dependant) -> Tuple[Dict, L
def get_openapi_operation_parameters(
all_route_params: Sequence[Field]
all_route_params: Sequence[Field],
) -> List[Dict[str, Any]]:
parameters = []
for param in all_route_params:
@@ -151,10 +155,8 @@ 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
assert route.response_class, "A response class is needed to generate OpenAPI"
route_response_media_type: Optional[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)
@@ -189,7 +191,7 @@ def get_openapi_path(
field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
)
response.setdefault("content", {}).setdefault(
route_response_media_type, {}
route_response_media_type or "application/json", {}
)["schema"] = response_schema
status_text: Optional[str] = status_code_ranges.get(
str(additional_status_code).upper()
@@ -202,24 +204,28 @@ def get_openapi_path(
status_code_key = "default"
operation.setdefault("responses", {})[status_code_key] = response
status_code = str(route.status_code)
response_schema = {"type": "string"}
if lenient_issubclass(route.response_class, JSONResponse):
if route.response_field:
response_schema, _, _ = field_schema(
route.response_field,
model_name_map=model_name_map,
ref_prefix=REF_PREFIX,
)
else:
response_schema = {}
operation.setdefault("responses", {}).setdefault(status_code, {})[
"description"
] = route.response_description
operation.setdefault("responses", {}).setdefault(
status_code, {}
).setdefault("content", {}).setdefault(route_response_media_type, {})[
"schema"
] = response_schema
if (
route_response_media_type
and route.status_code not in STATUS_CODES_WITH_NO_BODY
):
response_schema = {"type": "string"}
if lenient_issubclass(route.response_class, JSONResponse):
if route.response_field:
response_schema, _, _ = field_schema(
route.response_field,
model_name_map=model_name_map,
ref_prefix=REF_PREFIX,
)
else:
response_schema = {}
operation.setdefault("responses", {}).setdefault(
status_code, {}
).setdefault("content", {}).setdefault(route_response_media_type, {})[
"schema"
] = response_schema
http422 = str(HTTP_422_UNPROCESSABLE_ENTITY)
if (all_route_params or route.body_field) and not any(

View File

@@ -13,6 +13,7 @@ from fastapi.dependencies.utils import (
)
from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
from fastapi.openapi.constants import STATUS_CODES_WITH_NO_BODY
from fastapi.utils import create_cloned_field, generate_operation_id_for_path
from pydantic import BaseConfig, BaseModel, Schema
from pydantic.error_wrappers import ErrorWrapper, ValidationError
@@ -65,7 +66,7 @@ def serialize_response(
return jsonable_encoder(response)
def get_app(
def get_request_handler(
dependant: Dependant,
body_field: Field = None,
status_code: int = 200,
@@ -215,6 +216,9 @@ class APIRoute(routing.Route):
)
self.response_model = response_model
if self.response_model:
assert (
status_code not in STATUS_CODES_WITH_NO_BODY
), f"Status code {status_code} must not have a response body"
response_name = "Response_" + self.unique_id
self.response_field: Optional[Field] = Field(
name=response_name,
@@ -256,6 +260,9 @@ class APIRoute(routing.Route):
assert isinstance(response, dict), "An additional response must be a dict"
model = response.get("model")
if model:
assert (
additional_status_code not in STATUS_CODES_WITH_NO_BODY
), f"Status code {additional_status_code} must not have a response body"
assert lenient_issubclass(
model, BaseModel
), "A response model must be a Pydantic model"
@@ -294,19 +301,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 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,
)
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,
)
@@ -348,8 +356,10 @@ class APIRouter(routing.Router):
include_in_schema: bool = True,
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,
@@ -487,6 +497,7 @@ class APIRouter(routing.Router):
include_in_schema=route.include_in_schema,
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(

View File

@@ -59,12 +59,11 @@ def create_cloned_field(field: Field) -> Field:
if lenient_issubclass(original_type, BaseModel):
original_type = cast(Type[BaseModel], original_type)
use_type = create_model(
original_type.__name__,
__config__=original_type.__config__,
__validators__=original_type.__validators__, # type: ignore
original_type.__name__, __config__=original_type.__config__
)
for f in original_type.__fields__.values():
use_type.__fields__[f.name] = f
use_type.__validators__ = original_type.__validators__
new_field = Field(
name=field.name,
type_=use_type,

View File

@@ -1,5 +1,6 @@
site_name: FastAPI
site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production
site_url: https://fastapi.tiangolo.com/
theme:
name: 'material'
@@ -57,6 +58,7 @@ nav:
- Classes as Dependencies: 'tutorial/dependencies/classes-as-dependencies.md'
- Sub-dependencies: 'tutorial/dependencies/sub-dependencies.md'
- Dependencies in path operation decorators: 'tutorial/dependencies/dependencies-in-path-operation-decorators.md'
- Dependencies with yield: 'tutorial/dependencies/dependencies-with-yield.md'
- Advanced Dependencies: 'tutorial/dependencies/advanced-dependencies.md'
- Security:
- Security Intro: 'tutorial/security/intro.md'
@@ -81,6 +83,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'

View File

@@ -19,7 +19,7 @@ classifiers = [
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
]
requires = [
"starlette >=0.11.1,<=0.12.8",
"starlette >=0.12.9,<=0.12.9",
"pydantic >=0.32.2,<=0.32.2"
]
description-file = "README.md"
@@ -39,7 +39,9 @@ test = [
"email_validator",
"sqlalchemy",
"databases[sqlite]",
"orjson"
"orjson",
"async_exit_stack",
"async_generator"
]
doc = [
"mkdocs",
@@ -61,4 +63,6 @@ all = [
"ujson",
"email_validator",
"uvicorn",
"async_exit_stack",
"async_generator"
]

View 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"

View File

@@ -0,0 +1,349 @@
from typing import Dict
import pytest
from fastapi import BackgroundTasks, Depends, FastAPI
from starlette.testclient import TestClient
app = FastAPI()
state = {
"/async": "asyncgen not started",
"/sync": "generator not started",
"/async_raise": "asyncgen raise not started",
"/sync_raise": "generator raise not started",
"context_a": "not started a",
"context_b": "not started b",
"bg": "not set",
"sync_bg": "not set",
}
errors = []
async def get_state():
return state
class AsyncDependencyError(Exception):
pass
class SyncDependencyError(Exception):
pass
class OtherDependencyError(Exception):
pass
async def asyncgen_state(state: Dict[str, str] = Depends(get_state)):
state["/async"] = "asyncgen started"
yield state["/async"]
state["/async"] = "asyncgen completed"
def generator_state(state: Dict[str, str] = Depends(get_state)):
state["/sync"] = "generator started"
yield state["/sync"]
state["/sync"] = "generator completed"
async def asyncgen_state_try(state: Dict[str, str] = Depends(get_state)):
state["/async_raise"] = "asyncgen raise started"
try:
yield state["/async_raise"]
except AsyncDependencyError:
errors.append("/async_raise")
finally:
state["/async_raise"] = "asyncgen raise finalized"
def generator_state_try(state: Dict[str, str] = Depends(get_state)):
state["/sync_raise"] = "generator raise started"
try:
yield state["/sync_raise"]
except SyncDependencyError:
errors.append("/sync_raise")
finally:
state["/sync_raise"] = "generator raise finalized"
async def context_a(state: dict = Depends(get_state)):
state["context_a"] = "started a"
try:
yield state
finally:
state["context_a"] = "finished a"
async def context_b(state: dict = Depends(context_a)):
state["context_b"] = "started b"
try:
yield state
finally:
state["context_b"] = f"finished b with a: {state['context_a']}"
@app.get("/async")
async def get_async(state: str = Depends(asyncgen_state)):
return state
@app.get("/sync")
async def get_sync(state: str = Depends(generator_state)):
return state
@app.get("/async_raise")
async def get_async_raise(state: str = Depends(asyncgen_state_try)):
assert state == "asyncgen raise started"
raise AsyncDependencyError()
@app.get("/sync_raise")
async def get_sync_raise(state: str = Depends(generator_state_try)):
assert state == "generator raise started"
raise SyncDependencyError()
@app.get("/async_raise_other")
async def get_async_raise_other(state: str = Depends(asyncgen_state_try)):
assert state == "asyncgen raise started"
raise OtherDependencyError()
@app.get("/sync_raise_other")
async def get_sync_raise_other(state: str = Depends(generator_state_try)):
assert state == "generator raise started"
raise OtherDependencyError()
@app.get("/context_b")
async def get_context_b(state: dict = Depends(context_b)):
return state
@app.get("/context_b_raise")
async def get_context_b_raise(state: dict = Depends(context_b)):
assert state["context_b"] == "started b"
assert state["context_a"] == "started a"
raise OtherDependencyError()
@app.get("/context_b_bg")
async def get_context_b_bg(tasks: BackgroundTasks, state: dict = Depends(context_b)):
async def bg(state: dict):
state["bg"] = f"bg set - b: {state['context_b']} - a: {state['context_a']}"
tasks.add_task(bg, state)
return state
# Sync versions
@app.get("/sync_async")
def get_sync_async(state: str = Depends(asyncgen_state)):
return state
@app.get("/sync_sync")
def get_sync_sync(state: str = Depends(generator_state)):
return state
@app.get("/sync_async_raise")
def get_sync_async_raise(state: str = Depends(asyncgen_state_try)):
assert state == "asyncgen raise started"
raise AsyncDependencyError()
@app.get("/sync_sync_raise")
def get_sync_sync_raise(state: str = Depends(generator_state_try)):
assert state == "generator raise started"
raise SyncDependencyError()
@app.get("/sync_async_raise_other")
def get_sync_async_raise_other(state: str = Depends(asyncgen_state_try)):
assert state == "asyncgen raise started"
raise OtherDependencyError()
@app.get("/sync_sync_raise_other")
def get_sync_sync_raise_other(state: str = Depends(generator_state_try)):
assert state == "generator raise started"
raise OtherDependencyError()
@app.get("/sync_context_b")
def get_sync_context_b(state: dict = Depends(context_b)):
return state
@app.get("/sync_context_b_raise")
def get_sync_context_b_raise(state: dict = Depends(context_b)):
assert state["context_b"] == "started b"
assert state["context_a"] == "started a"
raise OtherDependencyError()
@app.get("/sync_context_b_bg")
async def get_sync_context_b_bg(
tasks: BackgroundTasks, state: dict = Depends(context_b)
):
async def bg(state: dict):
state[
"sync_bg"
] = f"sync_bg set - b: {state['context_b']} - a: {state['context_a']}"
tasks.add_task(bg, state)
return state
client = TestClient(app)
def test_async_state():
assert state["/async"] == f"asyncgen not started"
response = client.get("/async")
assert response.status_code == 200
assert response.json() == f"asyncgen started"
assert state["/async"] == f"asyncgen completed"
def test_sync_state():
assert state["/sync"] == f"generator not started"
response = client.get("/sync")
assert response.status_code == 200
assert response.json() == f"generator started"
assert state["/sync"] == f"generator completed"
def test_async_raise_other():
assert state["/async_raise"] == "asyncgen raise not started"
with pytest.raises(OtherDependencyError):
client.get("/async_raise_other")
assert state["/async_raise"] == "asyncgen raise finalized"
assert "/async_raise" not in errors
def test_sync_raise_other():
assert state["/sync_raise"] == "generator raise not started"
with pytest.raises(OtherDependencyError):
client.get("/sync_raise_other")
assert state["/sync_raise"] == "generator raise finalized"
assert "/sync_raise" not in errors
def test_async_raise():
response = client.get("/async_raise")
assert response.status_code == 500
assert state["/async_raise"] == "asyncgen raise finalized"
assert "/async_raise" in errors
errors.clear()
def test_context_b():
response = client.get("/context_b")
data = response.json()
assert data["context_b"] == "started b"
assert data["context_a"] == "started a"
assert state["context_b"] == "finished b with a: started a"
assert state["context_a"] == "finished a"
def test_context_b_raise():
with pytest.raises(OtherDependencyError):
client.get("/context_b_raise")
assert state["context_b"] == "finished b with a: started a"
assert state["context_a"] == "finished a"
def test_background_tasks():
response = client.get("/context_b_bg")
data = response.json()
assert data["context_b"] == "started b"
assert data["context_a"] == "started a"
assert data["bg"] == "not set"
assert state["context_b"] == "finished b with a: started a"
assert state["context_a"] == "finished a"
assert state["bg"] == "bg set - b: started b - a: started a"
def test_sync_raise():
response = client.get("/sync_raise")
assert response.status_code == 500
assert state["/sync_raise"] == "generator raise finalized"
assert "/sync_raise" in errors
errors.clear()
def test_sync_async_state():
response = client.get("/sync_async")
assert response.status_code == 200
assert response.json() == f"asyncgen started"
assert state["/async"] == f"asyncgen completed"
def test_sync_sync_state():
response = client.get("/sync_sync")
assert response.status_code == 200
assert response.json() == f"generator started"
assert state["/sync"] == f"generator completed"
def test_sync_async_raise_other():
with pytest.raises(OtherDependencyError):
client.get("/sync_async_raise_other")
assert state["/async_raise"] == "asyncgen raise finalized"
assert "/async_raise" not in errors
def test_sync_sync_raise_other():
with pytest.raises(OtherDependencyError):
client.get("/sync_sync_raise_other")
assert state["/sync_raise"] == "generator raise finalized"
assert "/sync_raise" not in errors
def test_sync_async_raise():
response = client.get("/sync_async_raise")
assert response.status_code == 500
assert state["/async_raise"] == "asyncgen raise finalized"
assert "/async_raise" in errors
errors.clear()
def test_sync_sync_raise():
response = client.get("/sync_sync_raise")
assert response.status_code == 500
assert state["/sync_raise"] == "generator raise finalized"
assert "/sync_raise" in errors
errors.clear()
def test_sync_context_b():
response = client.get("/sync_context_b")
data = response.json()
assert data["context_b"] == "started b"
assert data["context_a"] == "started a"
assert state["context_b"] == "finished b with a: started a"
assert state["context_a"] == "finished a"
def test_sync_context_b_raise():
with pytest.raises(OtherDependencyError):
client.get("/sync_context_b_raise")
assert state["context_b"] == "finished b with a: started a"
assert state["context_a"] == "finished a"
def test_sync_background_tasks():
response = client.get("/sync_context_b_bg")
data = response.json()
assert data["context_b"] == "started b"
assert data["context_a"] == "started a"
assert data["sync_bg"] == "not set"
assert state["context_b"] == "finished b with a: started a"
assert state["context_a"] == "finished a"
assert state["sync_bg"] == "sync_bg set - b: started b - a: started a"

12
tests/test_fakeasync.py Normal file
View File

@@ -0,0 +1,12 @@
import pytest
from fastapi.concurrency import _fake_asynccontextmanager
@_fake_asynccontextmanager
def never_run():
pass # pragma: no cover
def test_fake_async():
with pytest.raises(RuntimeError):
never_run()

View File

@@ -0,0 +1,114 @@
import typing
from fastapi import FastAPI
from pydantic import BaseModel
from starlette.responses import JSONResponse, Response
from starlette.testclient import TestClient
app = FastAPI()
class JsonApiResponse(JSONResponse):
media_type = "application/vnd.api+json"
class Error(BaseModel):
status: str
title: str
class JsonApiError(BaseModel):
errors: typing.List[Error]
@app.get(
"/a",
response_class=Response,
responses={500: {"description": "Error", "model": JsonApiError}},
)
async def a():
pass # pragma: no cover
@app.get("/b", responses={500: {"description": "Error", "model": Error}})
async def b():
pass # pragma: no cover
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/a": {
"get": {
"responses": {
"500": {
"description": "Error",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/JsonApiError"}
}
},
},
"200": {"description": "Successful Response"},
},
"summary": "A",
"operationId": "a_a_get",
}
},
"/b": {
"get": {
"responses": {
"500": {
"description": "Error",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Error"}
}
},
},
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
},
"summary": "B",
"operationId": "b_b_get",
}
},
},
"components": {
"schemas": {
"Error": {
"title": "Error",
"required": ["status", "title"],
"type": "object",
"properties": {
"status": {"title": "Status", "type": "string"},
"title": {"title": "Title", "type": "string"},
},
},
"JsonApiError": {
"title": "JsonApiError",
"required": ["errors"],
"type": "object",
"properties": {
"errors": {
"title": "Errors",
"type": "array",
"items": {"$ref": "#/components/schemas/Error"},
}
},
},
}
},
}
client = TestClient(app)
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema

View File

@@ -0,0 +1,108 @@
import typing
from fastapi import FastAPI
from pydantic import BaseModel
from starlette.responses import JSONResponse
from starlette.testclient import TestClient
app = FastAPI()
class JsonApiResponse(JSONResponse):
media_type = "application/vnd.api+json"
class Error(BaseModel):
status: str
title: str
class JsonApiError(BaseModel):
errors: typing.List[Error]
@app.get(
"/a",
status_code=204,
response_class=JsonApiResponse,
responses={500: {"description": "Error", "model": JsonApiError}},
)
async def a():
pass # pragma: no cover
@app.get("/b", responses={204: {"description": "No Content"}})
async def b():
pass # pragma: no cover
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/a": {
"get": {
"responses": {
"500": {
"description": "Error",
"content": {
"application/vnd.api+json": {
"schema": {"$ref": "#/components/schemas/JsonApiError"}
}
},
},
"204": {"description": "Successful Response"},
},
"summary": "A",
"operationId": "a_a_get",
}
},
"/b": {
"get": {
"responses": {
"204": {"description": "No Content"},
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
},
"summary": "B",
"operationId": "b_b_get",
}
},
},
"components": {
"schemas": {
"Error": {
"title": "Error",
"required": ["status", "title"],
"type": "object",
"properties": {
"status": {"title": "Status", "type": "string"},
"title": {"title": "Title", "type": "string"},
},
},
"JsonApiError": {
"title": "JsonApiError",
"required": ["errors"],
"type": "object",
"properties": {
"errors": {
"title": "Errors",
"type": "array",
"items": {"$ref": "#/components/schemas/Error"},
}
},
},
}
},
}
client = TestClient(app)
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema

View File

@@ -11,7 +11,7 @@ security = HTTPBase(scheme="Other", auto_error=False)
@app.get("/users/me")
def read_current_user(
credentials: Optional[HTTPAuthorizationCredentials] = Security(security)
credentials: Optional[HTTPAuthorizationCredentials] = Security(security),
):
if credentials is None:
return {"msg": "Create an account first"}

View File

@@ -11,7 +11,7 @@ security = HTTPBearer(auto_error=False)
@app.get("/users/me")
def read_current_user(
credentials: Optional[HTTPAuthorizationCredentials] = Security(security)
credentials: Optional[HTTPAuthorizationCredentials] = Security(security),
):
if credentials is None:
return {"msg": "Create an account first"}

View File

@@ -11,7 +11,7 @@ security = HTTPDigest(auto_error=False)
@app.get("/users/me")
def read_current_user(
credentials: Optional[HTTPAuthorizationCredentials] = Security(security)
credentials: Optional[HTTPAuthorizationCredentials] = Security(security),
):
if credentials is None:
return {"msg": "Create an account first"}

View File

View File

@@ -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"}

View File

@@ -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",
}
],
}
}

View File

@@ -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

View File

@@ -0,0 +1,42 @@
import os
from pathlib import Path
import pytest
from starlette.testclient import TestClient
@pytest.fixture(scope="module")
def client():
static_dir: Path = Path(os.getcwd()) / "static"
print(static_dir)
static_dir.mkdir(exist_ok=True)
from extending_openapi.tutorial002 import app
with TestClient(app) as client:
yield client
static_dir.rmdir()
def test_swagger_ui_html(client: TestClient):
response = client.get("/docs")
assert response.status_code == 200
assert "/static/swagger-ui-bundle.js" in response.text
assert "/static/swagger-ui.css" in response.text
def test_swagger_ui_oauth2_redirect_html(client: TestClient):
response = client.get("/docs/oauth2-redirect")
assert response.status_code == 200
assert "window.opener.swaggerUIRedirectOauth2" in response.text
def test_redoc_html(client: TestClient):
response = client.get("/redoc")
assert response.status_code == 200
assert "/static/redoc.standalone.js" in response.text
def test_api(client: TestClient):
response = client.get("/users/john")
assert response.status_code == 200
assert response.json()["message"] == "Hello john"

View File

@@ -7,7 +7,20 @@ client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {},
"paths": {
"/items/": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
"summary": "Read Items",
"operationId": "read_items",
}
}
},
}

View File

@@ -7,90 +7,7 @@ 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"},
}
},
},
}
},
"paths": {},
}
@@ -100,13 +17,7 @@ def test_openapi_schema():
assert response.json() == openapi_schema
def test_query_params_str_validations():
response = client.post("/items/", json={"name": "Foo", "price": 42})
def test_get():
response = client.get("/items/")
assert response.status_code == 200
assert response.json() == {
"name": "Foo",
"price": 42,
"description": None,
"tax": None,
"tags": [],
}
assert response.json() == [{"item_id": "Foo"}]

View File

@@ -0,0 +1,112 @@
from starlette.testclient import TestClient
from path_operation_advanced_configuration.tutorial004 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": [],
}

View File

@@ -1,9 +1,8 @@
from pathlib import Path
import pytest
from starlette.testclient import TestClient
from sql_databases.sql_app.main import app
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
@@ -282,13 +281,24 @@ openapi_schema = {
}
def test_openapi_schema():
@pytest.fixture(scope="module")
def client():
# Import while creating the client to create the DB after starting the test session
from sql_databases.sql_app.main import app
test_db = Path("./test.db")
with TestClient(app) as c:
yield c
test_db.unlink()
def test_openapi_schema(client):
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema
def test_create_user():
def test_create_user(client):
test_user = {"email": "johndoe@example.com", "password": "secret"}
response = client.post("/users/", json=test_user)
assert response.status_code == 200
@@ -299,7 +309,7 @@ def test_create_user():
assert response.status_code == 400
def test_get_user():
def test_get_user(client):
response = client.get("/users/1")
assert response.status_code == 200
data = response.json()
@@ -307,12 +317,12 @@ def test_get_user():
assert "id" in data
def test_inexistent_user():
def test_inexistent_user(client):
response = client.get("/users/999")
assert response.status_code == 404
def test_get_users():
def test_get_users(client):
response = client.get("/users/")
assert response.status_code == 200
data = response.json()
@@ -320,7 +330,7 @@ def test_get_users():
assert "id" in data[0]
def test_create_item():
def test_create_item(client):
item = {"title": "Foo", "description": "Something that fights"}
response = client.post("/users/1/items/", json=item)
assert response.status_code == 200
@@ -343,7 +353,7 @@ def test_create_item():
assert item_to_check["description"] == item["description"]
def test_read_items():
def test_read_items(client):
response = client.get("/items/")
assert response.status_code == 200
data = response.json()

View File

@@ -0,0 +1,363 @@
from pathlib import Path
import pytest
from starlette.testclient import TestClient
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/users/": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"title": "Response_Read_Users_Users__Get",
"type": "array",
"items": {"$ref": "#/components/schemas/User"},
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"summary": "Read Users",
"operationId": "read_users_users__get",
"parameters": [
{
"required": False,
"schema": {"title": "Skip", "type": "integer", "default": 0},
"name": "skip",
"in": "query",
},
{
"required": False,
"schema": {"title": "Limit", "type": "integer", "default": 100},
"name": "limit",
"in": "query",
},
],
},
"post": {
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/User"}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"summary": "Create User",
"operationId": "create_user_users__post",
"requestBody": {
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/UserCreate"}
}
},
"required": True,
},
},
},
"/users/{user_id}": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/User"}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"summary": "Read User",
"operationId": "read_user_users__user_id__get",
"parameters": [
{
"required": True,
"schema": {"title": "User_Id", "type": "integer"},
"name": "user_id",
"in": "path",
}
],
}
},
"/users/{user_id}/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 Item For User",
"operationId": "create_item_for_user_users__user_id__items__post",
"parameters": [
{
"required": True,
"schema": {"title": "User_Id", "type": "integer"},
"name": "user_id",
"in": "path",
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/ItemCreate"}
}
},
"required": True,
},
}
},
"/items/": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"title": "Response_Read_Items_Items__Get",
"type": "array",
"items": {"$ref": "#/components/schemas/Item"},
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"summary": "Read Items",
"operationId": "read_items_items__get",
"parameters": [
{
"required": False,
"schema": {"title": "Skip", "type": "integer", "default": 0},
"name": "skip",
"in": "query",
},
{
"required": False,
"schema": {"title": "Limit", "type": "integer", "default": 100},
"name": "limit",
"in": "query",
},
],
}
},
},
"components": {
"schemas": {
"ItemCreate": {
"title": "ItemCreate",
"required": ["title"],
"type": "object",
"properties": {
"title": {"title": "Title", "type": "string"},
"description": {"title": "Description", "type": "string"},
},
},
"Item": {
"title": "Item",
"required": ["title", "id", "owner_id"],
"type": "object",
"properties": {
"title": {"title": "Title", "type": "string"},
"description": {"title": "Description", "type": "string"},
"id": {"title": "Id", "type": "integer"},
"owner_id": {"title": "Owner_Id", "type": "integer"},
},
},
"User": {
"title": "User",
"required": ["email", "id", "is_active"],
"type": "object",
"properties": {
"email": {"title": "Email", "type": "string"},
"id": {"title": "Id", "type": "integer"},
"is_active": {"title": "Is_Active", "type": "boolean"},
"items": {
"title": "Items",
"type": "array",
"items": {"$ref": "#/components/schemas/Item"},
"default": [],
},
},
},
"UserCreate": {
"title": "UserCreate",
"required": ["email", "password"],
"type": "object",
"properties": {
"email": {"title": "Email", "type": "string"},
"password": {"title": "Password", "type": "string"},
},
},
"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"},
}
},
},
}
},
}
@pytest.fixture(scope="module")
def client():
# Import while creating the client to create the DB after starting the test session
from sql_databases.sql_app.alt_main import app
test_db = Path("./test.db")
with TestClient(app) as c:
yield c
test_db.unlink()
def test_openapi_schema(client):
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema
def test_create_user(client):
test_user = {"email": "johndoe@example.com", "password": "secret"}
response = client.post("/users/", json=test_user)
assert response.status_code == 200
data = response.json()
assert test_user["email"] == data["email"]
assert "id" in data
response = client.post("/users/", json=test_user)
assert response.status_code == 400
def test_get_user(client):
response = client.get("/users/1")
assert response.status_code == 200
data = response.json()
assert "email" in data
assert "id" in data
def test_inexistent_user(client):
response = client.get("/users/999")
assert response.status_code == 404
def test_get_users(client):
response = client.get("/users/")
assert response.status_code == 200
data = response.json()
assert "email" in data[0]
assert "id" in data[0]
def test_create_item(client):
item = {"title": "Foo", "description": "Something that fights"}
response = client.post("/users/1/items/", json=item)
assert response.status_code == 200
item_data = response.json()
assert item["title"] == item_data["title"]
assert item["description"] == item_data["description"]
assert "id" in item_data
assert "owner_id" in item_data
response = client.get("/users/1")
assert response.status_code == 200
user_data = response.json()
item_to_check = [it for it in user_data["items"] if it["id"] == item_data["id"]][0]
assert item_to_check["title"] == item["title"]
assert item_to_check["description"] == item["description"]
response = client.get("/users/1")
assert response.status_code == 200
user_data = response.json()
item_to_check = [it for it in user_data["items"] if it["id"] == item_data["id"]][0]
assert item_to_check["title"] == item["title"]
assert item_to_check["description"] == item["description"]
def test_read_items(client):
response = client.get("/items/")
assert response.status_code == 200
data = response.json()
assert data
first_item = data[0]
assert "title" in first_item
assert "description" in first_item