mirror of
https://github.com/fastapi/fastapi.git
synced 2025-12-27 08:10:57 -05:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65536cbf63 | ||
|
|
0192eab557 | ||
|
|
3f9f4a0f8f | ||
|
|
380e3731a8 | ||
|
|
d6d99b86cb | ||
|
|
5592fa0f6f | ||
|
|
b65be5d496 | ||
|
|
6c7da43e51 | ||
|
|
dfec2d7644 | ||
|
|
8c3ef76139 | ||
|
|
7a504a721c | ||
|
|
dd963511d6 | ||
|
|
fdb6d43e10 | ||
|
|
a7c718e968 | ||
|
|
f4d753620b | ||
|
|
fadfe4c586 | ||
|
|
5fd83c5fa4 | ||
|
|
14daaf409f | ||
|
|
c7dc26b760 | ||
|
|
f5ccb3c35d | ||
|
|
4cea311e6e | ||
|
|
f8718072a0 | ||
|
|
3dbbecdd16 | ||
|
|
6d5530ec1c | ||
|
|
0761f11d1a | ||
|
|
f2e7ef7056 | ||
|
|
d5d9a20937 | ||
|
|
96f092179f | ||
|
|
8505b716af | ||
|
|
78272ac1f3 | ||
|
|
f1bee9a271 | ||
|
|
b20b2218cd | ||
|
|
b9cf69cd42 | ||
|
|
f803c77515 | ||
|
|
0c67022048 | ||
|
|
d8fe307d61 | ||
|
|
580cf8f4e2 | ||
|
|
af390af77c | ||
|
|
4642f63a1e | ||
|
|
203e10596f | ||
|
|
5a2278d09a | ||
|
|
47a8387a04 | ||
|
|
27ca0c9dca | ||
|
|
9418d78de6 | ||
|
|
4b74aef429 | ||
|
|
fc7d123347 |
5
Pipfile
5
Pipfile
@@ -25,10 +25,11 @@ sqlalchemy = "*"
|
||||
uvicorn = "*"
|
||||
|
||||
[packages]
|
||||
starlette = "==0.12.7"
|
||||
pydantic = "==0.30.0"
|
||||
starlette = "==0.12.9"
|
||||
pydantic = "==0.32.2"
|
||||
databases = {extras = ["sqlite"],version = "*"}
|
||||
hypercorn = "*"
|
||||
orjson = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.6"
|
||||
|
||||
11
README.md
11
README.md
@@ -63,8 +63,19 @@ The key features are:
|
||||
|
||||
---
|
||||
|
||||
"*If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]*"
|
||||
|
||||
"*We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]*"
|
||||
|
||||
<div style="text-align: right; margin-right: 10%;">Ines Montani - Matthew Honnibal - <strong><a href="https://explosion.ai" target="_blank">Explosion AI</a> founders - <a href="https://spacy.io" target="_blank">spaCy</a> creators</strong> <a href="https://twitter.com/_inesmontani/status/1144173225322143744" target="_blank"><small>(ref)</small></a> - <a href="https://twitter.com/honnibal/status/1144031421859655680" target="_blank"><small>(ref)</small></a></div>
|
||||
|
||||
---
|
||||
|
||||
"*We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]*"
|
||||
|
||||
<div style="text-align: right; margin-right: 10%;">Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - <strong>Uber</strong> <a href="https://eng.uber.com/ludwig-v0-2/" target="_blank"><small>(ref)</small></a></div>
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -27,6 +27,14 @@ Here's an incomplete list of some of them.
|
||||
|
||||
* <a href="https://medium.com/@nico.axtmann95/deploying-a-scikit-learn-model-with-onnx-und-fastapi-1af398268915" target="_blank">Deploying a scikit-learn model with ONNX and FastAPI</a> by <a href="https://www.linkedin.com/in/nico-axtmann" target="_blank">Nico Axtmann</a>.
|
||||
|
||||
* <a href="https://geekflare.com/python-asynchronous-web-frameworks/" target="_blank">Top 5 Asynchronous Web Frameworks for Python</a> by <a href="https://geekflare.com/author/ankush/" target="_blank">Ankush Thakur</a> on <a href="https://geekflare.com" target="_blank">GeekFlare</a>.
|
||||
|
||||
* <a href="https://medium.com/@gntrm/jwt-authentication-with-fastapi-and-aws-cognito-1333f7f2729e" target="_blank">JWT Authentication with FastAPI and AWS Cognito</a> by <a href="https://twitter.com/gntrm" target="_blank">Johannes Gontrum</a>.
|
||||
|
||||
* <a href="https://towardsdatascience.com/how-to-deploy-a-machine-learning-model-dc51200fe8cf" target="_blank">How to Deploy a Machine Learning Model</a> by <a href="https://www.linkedin.com/in/mgrootendorst/" target="_blank">Maarten Grootendorst</a> on <a href="https://towardsdatascience.com/" target="_blank">Towards Data Science</a>.
|
||||
|
||||
* <a href="https://eng.uber.com/ludwig-v0-2/" target="_blank">Uber: Ludwig v0.2 Adds New Features and Other Improvements to its Deep Learning Toolbox [including a FastAPI server]</a> on <a href="https://eng.uber.com" target="_blank">Uber Engineering</a>.
|
||||
|
||||
### Japanese
|
||||
|
||||
* <a href="https://qiita.com/mtitg/items/47770e9a562dd150631d" target="_blank">FastAPI|DB接続してCRUDするPython製APIサーバーを構築</a> by <a href="https://qiita.com/mtitg" target="_blank">@mtitg</a>.
|
||||
|
||||
@@ -63,8 +63,19 @@ The key features are:
|
||||
|
||||
---
|
||||
|
||||
"*If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]*"
|
||||
|
||||
"*We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]*"
|
||||
|
||||
<div style="text-align: right; margin-right: 10%;">Ines Montani - Matthew Honnibal - <strong><a href="https://explosion.ai" target="_blank">Explosion AI</a> founders - <a href="https://spacy.io" target="_blank">spaCy</a> creators</strong> <a href="https://twitter.com/_inesmontani/status/1144173225322143744" target="_blank"><small>(ref)</small></a> - <a href="https://twitter.com/honnibal/status/1144031421859655680" target="_blank"><small>(ref)</small></a></div>
|
||||
|
||||
---
|
||||
|
||||
"*We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]*"
|
||||
|
||||
<div style="text-align: right; margin-right: 10%;">Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - <strong>Uber</strong> <a href="https://eng.uber.com/ludwig-v0-2/" target="_blank"><small>(ref)</small></a></div>
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -1,5 +1,66 @@
|
||||
## Latest changes
|
||||
|
||||
## 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).
|
||||
* Generate OpenAPI schemas in alphabetical order. PR [#554](https://github.com/tiangolo/fastapi/pull/554) by [@dmontagu](https://github.com/dmontagu).
|
||||
* Add support for truncating docstrings from *path operation functions*.
|
||||
* New docs at [Advanced description from docstring](https://fastapi.tiangolo.com/tutorial/path-operation-advanced-configuration/#advanced-description-from-docstring).
|
||||
* PR [#556](https://github.com/tiangolo/fastapi/pull/556) by [@svalouch](https://github.com/svalouch).
|
||||
* Fix `DOCTYPE` in HTML files generated for Swagger UI and ReDoc. PR [#537](https://github.com/tiangolo/fastapi/pull/537) by [@Trim21](https://github.com/Trim21).
|
||||
* Fix handling `4XX` responses overriding default `422` validation error responses. PR [#517](https://github.com/tiangolo/fastapi/pull/517) by [@tsouvarev](https://github.com/tsouvarev).
|
||||
* Fix typo in documentation for [Simple HTTP Basic Auth](https://fastapi.tiangolo.com/tutorial/security/http-basic-auth/#simple-http-basic-auth). PR [#514](https://github.com/tiangolo/fastapi/pull/514) by [@prostomarkeloff](https://github.com/prostomarkeloff).
|
||||
* Fix incorrect documentation example in [first steps](https://fastapi.tiangolo.com/tutorial/first-steps/). PR [#511](https://github.com/tiangolo/fastapi/pull/511) by [@IgnatovFedor](https://github.com/IgnatovFedor).
|
||||
* Add support for Swagger UI [initOauth](https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/oauth2.md) settings with the parameter `swagger_ui_init_oauth`. PR [#499](https://github.com/tiangolo/fastapi/pull/499) by [@zamiramir](https://github.com/zamiramir).
|
||||
|
||||
## 0.39.0
|
||||
|
||||
* Allow path parameters to have default values (e.g. `None`) and discard them instead of raising an error.
|
||||
* This allows declaring a parameter like `user_id: str = None` that can be taken from a query parameter, but the same path operation can be included in a router with a path `/users/{user_id}`, in which case will be taken from the path and will be required.
|
||||
* PR [#464](https://github.com/tiangolo/fastapi/pull/464) by [@jonathanunderwood](https://github.com/jonathanunderwood).
|
||||
* Add support for setting a `default_response_class` in the `FastAPI` instance or in `include_router`. Initial PR [#467](https://github.com/tiangolo/fastapi/pull/467) by [@toppk](https://github.com/toppk).
|
||||
* Add support for type annotations using strings and `from __future__ import annotations`. PR [#451](https://github.com/tiangolo/fastapi/pull/451) by [@dmontagu](https://github.com/dmontagu).
|
||||
|
||||
## 0.38.1
|
||||
|
||||
* Fix incorrect `Request` class import. PR [#493](https://github.com/tiangolo/fastapi/pull/493) by [@kamalgill](https://github.com/kamalgill).
|
||||
|
||||
## 0.38.0
|
||||
|
||||
* Add recent articles to [External Links](https://fastapi.tiangolo.com/external-links/) and recent opinions. PR [#490](https://github.com/tiangolo/fastapi/pull/490).
|
||||
* Upgrade support range for Starlette to include `0.12.8`. The new range is `>=0.11.1,<=0.12.8"`. PR [#477](https://github.com/tiangolo/fastapi/pull/477) by [@dmontagu](https://github.com/dmontagu).
|
||||
* Upgrade support to Pydantic version 0.32.2 and update internal code to use it (breaking change). PR [#463](https://github.com/tiangolo/fastapi/pull/463) by [@dmontagu](https://github.com/dmontagu).
|
||||
|
||||
## 0.37.0
|
||||
|
||||
* Add support for custom route classes for advanced use cases. PR [#468](https://github.com/tiangolo/fastapi/pull/468) by [@dmontagu](https://github.com/dmontagu).
|
||||
|
||||
37
docs/src/custom_request_and_route/tutorial001.py
Normal file
37
docs/src/custom_request_and_route/tutorial001.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import gzip
|
||||
from typing import Callable, List
|
||||
|
||||
from fastapi import Body, FastAPI
|
||||
from fastapi.routing import APIRoute
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
|
||||
class GzipRequest(Request):
|
||||
async def body(self) -> bytes:
|
||||
if not hasattr(self, "_body"):
|
||||
body = await super().body()
|
||||
if "gzip" in self.headers.getlist("Content-Encoding"):
|
||||
body = gzip.decompress(body)
|
||||
self._body = body
|
||||
return self._body
|
||||
|
||||
|
||||
class GzipRoute(APIRoute):
|
||||
def get_route_handler(self) -> Callable:
|
||||
original_route_handler = super().get_route_handler()
|
||||
|
||||
async def custom_route_handler(request: Request) -> Response:
|
||||
request = GzipRequest(request.scope, request.receive)
|
||||
return await original_route_handler(request)
|
||||
|
||||
return custom_route_handler
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
app.router.route_class = GzipRoute
|
||||
|
||||
|
||||
@app.post("/sum")
|
||||
async def sum_numbers(numbers: List[int] = Body(...)):
|
||||
return {"sum": sum(numbers)}
|
||||
31
docs/src/custom_request_and_route/tutorial002.py
Normal file
31
docs/src/custom_request_and_route/tutorial002.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from typing import Callable, List
|
||||
|
||||
from fastapi import Body, FastAPI, HTTPException
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.routing import APIRoute
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
|
||||
class ValidationErrorLoggingRoute(APIRoute):
|
||||
def get_route_handler(self) -> Callable:
|
||||
original_route_handler = super().get_route_handler()
|
||||
|
||||
async def custom_route_handler(request: Request) -> Response:
|
||||
try:
|
||||
return await original_route_handler(request)
|
||||
except RequestValidationError as exc:
|
||||
body = await request.body()
|
||||
detail = {"errors": exc.errors(), "body": body.decode()}
|
||||
raise HTTPException(status_code=422, detail=detail)
|
||||
|
||||
return custom_route_handler
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
app.router.route_class = ValidationErrorLoggingRoute
|
||||
|
||||
|
||||
@app.post("/")
|
||||
async def sum_numbers(numbers: List[int] = Body(...)):
|
||||
return sum(numbers)
|
||||
41
docs/src/custom_request_and_route/tutorial003.py
Normal file
41
docs/src/custom_request_and_route/tutorial003.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import time
|
||||
from typing import Callable
|
||||
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from fastapi.routing import APIRoute
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
|
||||
class TimedRoute(APIRoute):
|
||||
def get_route_handler(self) -> Callable:
|
||||
original_route_handler = super().get_route_handler()
|
||||
|
||||
async def custom_route_handler(request: Request) -> Response:
|
||||
before = time.time()
|
||||
response: Response = await original_route_handler(request)
|
||||
duration = time.time() - before
|
||||
response.headers["X-Response-Time"] = str(duration)
|
||||
print(f"route duration: {duration}")
|
||||
print(f"route response: {response}")
|
||||
print(f"route response headers: {response.headers}")
|
||||
return response
|
||||
|
||||
return custom_route_handler
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
router = APIRouter(route_class=TimedRoute)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def not_timed():
|
||||
return {"message": "Not timed"}
|
||||
|
||||
|
||||
@router.get("/timed")
|
||||
async def timed():
|
||||
return {"message": "It's the time of my life"}
|
||||
|
||||
|
||||
app.include_router(router)
|
||||
@@ -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()
|
||||
|
||||
25
docs/src/dependencies/tutorial008.py
Normal file
25
docs/src/dependencies/tutorial008.py
Normal 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)
|
||||
25
docs/src/dependencies/tutorial009.py
Normal file
25
docs/src/dependencies/tutorial009.py
Normal 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)
|
||||
14
docs/src/dependencies/tutorial010.py
Normal file
14
docs/src/dependencies/tutorial010.py
Normal 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
|
||||
21
docs/src/dependencies/tutorial011.py
Normal file
21
docs/src/dependencies/tutorial011.py
Normal 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}
|
||||
@@ -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
|
||||
64
docs/src/sql_databases/sql_app/alt_main.py
Normal file
64
docs/src/sql_databases/sql_app/alt_main.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
100
docs/tutorial/custom-request-and-route.md
Normal file
100
docs/tutorial/custom-request-and-route.md
Normal file
@@ -0,0 +1,100 @@
|
||||
In some cases, you may want to override the logic used by the `Request` and `APIRoute` classes.
|
||||
|
||||
In particular, this may be a good alternative to logic in a middleware.
|
||||
|
||||
For example, if you want to read or manipulate the request body before it is processed by your application.
|
||||
|
||||
!!! danger
|
||||
This is an "advanced" feature.
|
||||
|
||||
If you are just starting with **FastAPI** you might want to skip this section.
|
||||
|
||||
## Use cases
|
||||
|
||||
Some use cases include:
|
||||
|
||||
* Converting non-JSON request bodies to JSON (e.g. [`msgpack`](https://msgpack.org/index.html)).
|
||||
* Decompressing gzip-compressed request bodies.
|
||||
* Automatically logging all request bodies.
|
||||
* Accessing the request body in an exception handler.
|
||||
|
||||
## Handling custom request body encodings
|
||||
|
||||
Let's see how to make use of a custom `Request` subclass to decompress gzip requests.
|
||||
|
||||
And an `APIRoute` subclass to use that custom request class.
|
||||
|
||||
### Create a custom `GzipRequest` class
|
||||
|
||||
First, we create a `GzipRequest` class, which will overwrite the `Request.body()` method to decompress the body in the presence of an appropriate header.
|
||||
|
||||
If there's no `gzip` in the header, it will not try to decompress the body.
|
||||
|
||||
That way, the same route class can handle gzip compressed or uncompressed requests.
|
||||
|
||||
```Python hl_lines="10 11 12 13 14 15 16 17"
|
||||
{!./src/custom_request_and_route/tutorial001.py!}
|
||||
```
|
||||
|
||||
### Create a custom `GzipRoute` class
|
||||
|
||||
Next, we create a custom subclass of `fastapi.routing.APIRoute` that will make use of the `GzipRequest`.
|
||||
|
||||
This time, it will overwrite the method `APIRoute.get_route_handler()`.
|
||||
|
||||
This method returns a function. And that function is what will receive a request and return a response.
|
||||
|
||||
Here we use it to create a `GzipRequest` from the original request.
|
||||
|
||||
```Python hl_lines="20 21 22 23 24 25 26 27 28"
|
||||
{!./src/custom_request_and_route/tutorial001.py!}
|
||||
```
|
||||
|
||||
!!! note "Technical Details"
|
||||
A `Request` has a `request.scope` attribute, that's just a Python `dict` containing the metadata related to the request.
|
||||
|
||||
A `Request` also has a `request.receive`, that's a function to "receive" the body of the request.
|
||||
|
||||
The `scope` `dict` and `receive` function are both part of the ASGI specification.
|
||||
|
||||
And those two things, `scope` and `receive`, are what is needed to create a new `Request` instance.
|
||||
|
||||
To learn more about the `Request` check <a href="https://www.starlette.io/requests/" target="_blank">Starlette's docs about Requests</a>.
|
||||
|
||||
The only thing the function returned by `GzipRequest.get_route_handler` does differently is convert the `Request` to a `GzipRequest`.
|
||||
|
||||
Doing this, our `GzipRequest` will take care of decompressing the data (if necessary) before passing it to our *path operations*.
|
||||
|
||||
After that, all of the processing logic is the same.
|
||||
|
||||
But because of our changes in `GzipRequest.body`, the request body will be automatically decompressed when it is loaded by **FastAPI** when needed.
|
||||
|
||||
## Accessing the request body in an exception handler
|
||||
|
||||
We can also use this same approach to access the request body in an exception handler.
|
||||
|
||||
All we need to do is handle the request inside a `try`/`except` block:
|
||||
|
||||
```Python hl_lines="15 17"
|
||||
{!./src/custom_request_and_route/tutorial002.py!}
|
||||
```
|
||||
|
||||
If an exception occurs, the`Request` instance will still be in scope, so we can read and make use of the request body when handling the error:
|
||||
|
||||
```Python hl_lines="18 19 20"
|
||||
{!./src/custom_request_and_route/tutorial002.py!}
|
||||
```
|
||||
|
||||
## Custom `APIRoute` class in a router
|
||||
|
||||
You can also set the `route_class` parameter of an `APIRouter`:
|
||||
|
||||
```Python hl_lines="25"
|
||||
{!./src/custom_request_and_route/tutorial003.py!}
|
||||
```
|
||||
|
||||
In this example, the *path operations* under the `router` will use the custom `TimedRoute` class, and will have an extra `X-Response-Time` header in the response with the time it took to generate the response:
|
||||
|
||||
```Python hl_lines="15 16 17 18 19"
|
||||
{!./src/custom_request_and_route/tutorial003.py!}
|
||||
```
|
||||
@@ -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
|
||||
|
||||
153
docs/tutorial/dependencies/dependencies-with-yield.md
Normal file
153
docs/tutorial/dependencies/dependencies-with-yield.md
Normal 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.
|
||||
@@ -37,7 +37,7 @@ Open your browser at <a href="http://127.0.0.1:8000" target="_blank">http://127.
|
||||
You will see the JSON response as:
|
||||
|
||||
```JSON
|
||||
{"hello": "world"}
|
||||
{"message": "Hello World"}
|
||||
```
|
||||
|
||||
### Interactive API docs
|
||||
|
||||
@@ -18,3 +18,15 @@ To exclude a path operation from the generated OpenAPI schema (and thus, from th
|
||||
```Python hl_lines="6"
|
||||
{!./src/path_operation_advanced_configuration/tutorial002.py!}
|
||||
```
|
||||
|
||||
## Advanced description from docstring
|
||||
|
||||
You can limit the lines used from the docstring of a *path operation function* for OpenAPI.
|
||||
|
||||
Adding an `\f` (an escaped "form feed" character) causes **FastAPI** to truncate the output used for OpenAPI at this point.
|
||||
|
||||
It won't show up in the documentation, but other tools (such as Sphinx) will be able to use the rest.
|
||||
|
||||
```Python hl_lines="19 20 21 22 23 24 25 26 27 28 29"
|
||||
{!./src/path_operation_advanced_configuration/tutorial003.py!}
|
||||
```
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
You can define files to be uploaded by the client using `File`.
|
||||
|
||||
!!! info
|
||||
To receive uploaded files, first install [`python-multipart`](https://andrew-d.github.io/python-multipart/).
|
||||
|
||||
E.g. `pip install python-multipart`.
|
||||
|
||||
This is because uploaded files are sent as "form data".
|
||||
|
||||
## Import `File`
|
||||
|
||||
Import `File` and `UploadFile` from `fastapi`:
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
You can define files and form fields at the same time using `File` and `Form`.
|
||||
|
||||
!!! info
|
||||
To receive uploaded files and/or form data, first install [`python-multipart`](https://andrew-d.github.io/python-multipart/).
|
||||
|
||||
E.g. `pip install python-multipart`.
|
||||
|
||||
## Import `File` and `Form`
|
||||
|
||||
```Python hl_lines="1"
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
When you need to receive form fields instead of JSON, you can use `Form`.
|
||||
|
||||
!!! info
|
||||
To use forms, first install [`python-multipart`](https://andrew-d.github.io/python-multipart/).
|
||||
|
||||
E.g. `pip install python-multipart`.
|
||||
|
||||
## Import `Form`
|
||||
|
||||
Import `Form` from `fastapi`:
|
||||
|
||||
@@ -24,6 +24,13 @@ Copy the example in a file `main.py`:
|
||||
|
||||
## Run it
|
||||
|
||||
!!! info
|
||||
First install [`python-multipart`](https://andrew-d.github.io/python-multipart/).
|
||||
|
||||
E.g. `pip install python-multipart`.
|
||||
|
||||
This is because **OAuth2** uses "form data" for sending the `username` and `password`.
|
||||
|
||||
Run the example with:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -12,8 +12,8 @@ Then, when you type that username and password, the browser sends them in the he
|
||||
|
||||
## Simple HTTP Basic Auth
|
||||
|
||||
* Import `HTTPBAsic` and `HTTPBasicCredentials`.
|
||||
* Create a "`security` scheme" using `HTTPBAsic`.
|
||||
* Import `HTTPBasic` and `HTTPBasicCredentials`.
|
||||
* Create a "`security` scheme" using `HTTPBasic`.
|
||||
* Use that `security` with a dependency in your *path operation*.
|
||||
* It returns an object of type `HTTPBasicCredentials`:
|
||||
* It contains the `username` and `password` sent.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.37.0"
|
||||
__version__ = "0.42.0"
|
||||
|
||||
from starlette.background import BackgroundTasks
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Type, Union
|
||||
from typing import Any, Callable, Dict, List, Optional, Sequence, Type, Union
|
||||
|
||||
from fastapi import routing
|
||||
from fastapi.concurrency import AsyncExitStack
|
||||
from fastapi.encoders import DictIntStrAny, SetIntStr
|
||||
from fastapi.exception_handlers import (
|
||||
http_exception_handler,
|
||||
request_validation_exception_handler,
|
||||
@@ -14,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):
|
||||
@@ -32,12 +36,16 @@ class FastAPI(Starlette):
|
||||
version: str = "0.1.0",
|
||||
openapi_url: Optional[str] = "/openapi.json",
|
||||
openapi_prefix: str = "",
|
||||
default_response_class: Type[Response] = JSONResponse,
|
||||
docs_url: Optional[str] = "/docs",
|
||||
redoc_url: Optional[str] = "/redoc",
|
||||
swagger_ui_oauth2_redirect_url: Optional[str] = "/docs/oauth2-redirect",
|
||||
swagger_ui_init_oauth: Optional[dict] = None,
|
||||
**extra: Dict[str, Any],
|
||||
) -> None:
|
||||
self.default_response_class = default_response_class
|
||||
self._debug = debug
|
||||
self.state = State()
|
||||
self.router: routing.APIRouter = routing.APIRouter(
|
||||
routes, dependency_overrides_provider=self
|
||||
)
|
||||
@@ -54,6 +62,7 @@ class FastAPI(Starlette):
|
||||
self.docs_url = docs_url
|
||||
self.redoc_url = redoc_url
|
||||
self.swagger_ui_oauth2_redirect_url = swagger_ui_oauth2_redirect_url
|
||||
self.swagger_ui_init_oauth = swagger_ui_init_oauth
|
||||
self.extra = extra
|
||||
self.dependency_overrides: Dict[Callable, Callable] = {}
|
||||
|
||||
@@ -95,6 +104,7 @@ class FastAPI(Starlette):
|
||||
openapi_url=openapi_url,
|
||||
title=self.title + " - Swagger UI",
|
||||
oauth2_redirect_url=self.swagger_ui_oauth2_redirect_url,
|
||||
init_oauth=self.swagger_ui_init_oauth,
|
||||
)
|
||||
|
||||
self.add_route(self.docs_url, swagger_ui_html, include_in_schema=False)
|
||||
@@ -122,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,
|
||||
@@ -138,12 +156,12 @@ class FastAPI(Starlette):
|
||||
deprecated: bool = None,
|
||||
methods: List[str] = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> None:
|
||||
self.router.add_api_route(
|
||||
@@ -165,7 +183,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias=response_model_by_alias,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -184,12 +202,12 @@ class FastAPI(Starlette):
|
||||
deprecated: bool = None,
|
||||
methods: List[str] = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@@ -212,7 +230,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias=response_model_by_alias,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
return func
|
||||
@@ -239,6 +257,7 @@ class FastAPI(Starlette):
|
||||
tags: List[str] = None,
|
||||
dependencies: Sequence[Depends] = None,
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
default_response_class: Optional[Type[Response]] = None,
|
||||
) -> None:
|
||||
self.router.include_router(
|
||||
router,
|
||||
@@ -246,6 +265,8 @@ class FastAPI(Starlette):
|
||||
tags=tags,
|
||||
dependencies=dependencies,
|
||||
responses=responses or {},
|
||||
default_response_class=default_response_class
|
||||
or self.default_response_class,
|
||||
)
|
||||
|
||||
def get(
|
||||
@@ -262,12 +283,12 @@ class FastAPI(Starlette):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.get(
|
||||
@@ -287,7 +308,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias=response_model_by_alias,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -305,12 +326,12 @@ class FastAPI(Starlette):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.put(
|
||||
@@ -330,7 +351,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias=response_model_by_alias,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -348,12 +369,12 @@ class FastAPI(Starlette):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.post(
|
||||
@@ -373,7 +394,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias=response_model_by_alias,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -391,12 +412,12 @@ class FastAPI(Starlette):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.delete(
|
||||
@@ -416,7 +437,7 @@ class FastAPI(Starlette):
|
||||
operation_id=operation_id,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -434,12 +455,12 @@ class FastAPI(Starlette):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.options(
|
||||
@@ -459,7 +480,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias=response_model_by_alias,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -477,12 +498,12 @@ class FastAPI(Starlette):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.head(
|
||||
@@ -502,7 +523,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias=response_model_by_alias,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -520,12 +541,12 @@ class FastAPI(Starlette):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.patch(
|
||||
@@ -545,7 +566,7 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias=response_model_by_alias,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
@@ -563,12 +584,12 @@ class FastAPI(Starlette):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.router.trace(
|
||||
@@ -588,6 +609,6 @@ class FastAPI(Starlette):
|
||||
response_model_by_alias=response_model_by_alias,
|
||||
response_model_skip_defaults=response_model_skip_defaults,
|
||||
include_in_schema=include_in_schema,
|
||||
response_class=response_class,
|
||||
response_class=response_class or self.default_response_class,
|
||||
name=name,
|
||||
)
|
||||
|
||||
45
fastapi/concurrency.py
Normal file
45
fastapi/concurrency.py
Normal 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)
|
||||
@@ -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
|
||||
@@ -26,7 +33,7 @@ from pydantic.error_wrappers import ErrorWrapper
|
||||
from pydantic.errors import MissingError
|
||||
from pydantic.fields import Field, Required, Shape
|
||||
from pydantic.schema import get_annotation_from_schema
|
||||
from pydantic.utils import lenient_issubclass
|
||||
from pydantic.utils import ForwardRef, evaluate_forwardref, lenient_issubclass
|
||||
from starlette.background import BackgroundTasks
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
from starlette.datastructures import FormData, Headers, QueryParams, UploadFile
|
||||
@@ -171,6 +178,42 @@ def is_scalar_sequence_field(field: Field) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def get_typed_signature(call: Callable) -> inspect.Signature:
|
||||
signature = inspect.signature(call)
|
||||
globalns = getattr(call, "__globals__", {})
|
||||
typed_params = [
|
||||
inspect.Parameter(
|
||||
name=param.name,
|
||||
kind=param.kind,
|
||||
default=param.default,
|
||||
annotation=get_typed_annotation(param, globalns),
|
||||
)
|
||||
for param in signature.parameters.values()
|
||||
]
|
||||
typed_signature = inspect.Signature(typed_params)
|
||||
return typed_signature
|
||||
|
||||
|
||||
def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) -> Any:
|
||||
annotation = param.annotation
|
||||
if isinstance(annotation, str):
|
||||
annotation = ForwardRef(annotation)
|
||||
annotation = evaluate_forwardref(annotation, globalns, globalns)
|
||||
return annotation
|
||||
|
||||
|
||||
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,
|
||||
@@ -180,8 +223,10 @@ def get_dependant(
|
||||
use_cache: bool = True,
|
||||
) -> Dependant:
|
||||
path_param_names = get_path_param_names(path)
|
||||
endpoint_signature = inspect.signature(call)
|
||||
endpoint_signature = get_typed_signature(call)
|
||||
signature_params = endpoint_signature.parameters
|
||||
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):
|
||||
@@ -196,16 +241,18 @@ def get_dependant(
|
||||
continue
|
||||
param_field = get_param_field(param=param, default_schema=params.Query)
|
||||
if param_name in path_param_names:
|
||||
assert param.default == param.empty or isinstance(
|
||||
param.default, params.Path
|
||||
), "Path params must have no defaults or use Path(...)"
|
||||
assert is_scalar_field(
|
||||
field=param_field
|
||||
), f"Path params must be of one of the supported types"
|
||||
if isinstance(param.default, params.Path):
|
||||
ignore_default = False
|
||||
else:
|
||||
ignore_default = True
|
||||
param_field = get_param_field(
|
||||
param=param,
|
||||
default_schema=params.Path,
|
||||
force_type=params.ParamTypes.path,
|
||||
ignore_default=ignore_default,
|
||||
)
|
||||
add_param_to_fields(field=param_field, dependant=dependant)
|
||||
elif is_scalar_field(field=param_field):
|
||||
@@ -248,10 +295,11 @@ def get_param_field(
|
||||
param: inspect.Parameter,
|
||||
default_schema: Type[params.Param] = params.Param,
|
||||
force_type: params.ParamTypes = None,
|
||||
ignore_default: bool = False,
|
||||
) -> Field:
|
||||
default_value = Required
|
||||
had_schema = False
|
||||
if not param.default == param.empty:
|
||||
if not param.default == param.empty and ignore_default is False:
|
||||
default_value = param.default
|
||||
if isinstance(default_value, Schema):
|
||||
had_schema = True
|
||||
@@ -311,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],
|
||||
@@ -329,8 +387,12 @@ async def solve_dependencies(
|
||||
]:
|
||||
values: Dict[str, Any] = {}
|
||||
errors: List[ErrorWrapper] = []
|
||||
response = response or Response( # type: ignore
|
||||
content=None, status_code=None, headers=None, media_type=None, background=None
|
||||
response = response or Response(
|
||||
content=None,
|
||||
status_code=None, # type: ignore
|
||||
headers=None,
|
||||
media_type=None,
|
||||
background=None,
|
||||
)
|
||||
dependency_cache = dependency_cache or {}
|
||||
sub_dependant: Dependant
|
||||
@@ -379,6 +441,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:
|
||||
@@ -405,7 +476,7 @@ async def solve_dependencies(
|
||||
values.update(cookie_values)
|
||||
errors += path_errors + query_errors + header_errors + cookie_errors
|
||||
if dependant.body_params:
|
||||
body_values, body_errors = await request_body_to_args( # type: ignore # body_params checked above
|
||||
body_values, body_errors = await request_body_to_args( # body_params checked above
|
||||
required_params=dependant.body_params, received_body=body
|
||||
)
|
||||
values.update(body_values)
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
from enum import Enum
|
||||
from types import GeneratorType
|
||||
from typing import Any, List, Set
|
||||
from typing import Any, Dict, List, Set, Union
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic.json import ENCODERS_BY_TYPE
|
||||
|
||||
SetIntStr = Set[Union[int, str]]
|
||||
DictIntStrAny = Dict[Union[int, str], Any]
|
||||
|
||||
|
||||
def jsonable_encoder(
|
||||
obj: Any,
|
||||
include: Set[str] = None,
|
||||
exclude: Set[str] = set(),
|
||||
include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
by_alias: bool = True,
|
||||
skip_defaults: bool = False,
|
||||
include_none: bool = True,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
from typing import Any
|
||||
from typing import Any, Sequence
|
||||
|
||||
from pydantic import ValidationError
|
||||
from pydantic.error_wrappers import ErrorList
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.websockets import WebSocket
|
||||
|
||||
|
||||
class HTTPException(StarletteHTTPException):
|
||||
@@ -13,8 +16,10 @@ class HTTPException(StarletteHTTPException):
|
||||
|
||||
|
||||
class RequestValidationError(ValidationError):
|
||||
pass
|
||||
def __init__(self, errors: Sequence[ErrorList]) -> None:
|
||||
super().__init__(errors, Request)
|
||||
|
||||
|
||||
class WebSocketRequestValidationError(ValidationError):
|
||||
pass
|
||||
def __init__(self, errors: Sequence[ErrorList]) -> None:
|
||||
super().__init__(errors, WebSocket)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
|
||||
@@ -11,10 +13,11 @@ def get_swagger_ui_html(
|
||||
swagger_css_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui.css",
|
||||
swagger_favicon_url: str = "https://fastapi.tiangolo.com/img/favicon.png",
|
||||
oauth2_redirect_url: Optional[str] = None,
|
||||
init_oauth: Optional[dict] = None,
|
||||
) -> HTMLResponse:
|
||||
|
||||
html = f"""
|
||||
<! doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link type="text/css" rel="stylesheet" href="{swagger_css_url}">
|
||||
@@ -42,7 +45,14 @@ def get_swagger_ui_html(
|
||||
],
|
||||
layout: "BaseLayout",
|
||||
deepLinking: true
|
||||
})
|
||||
})"""
|
||||
|
||||
if init_oauth:
|
||||
html += f"""
|
||||
ui.initOAuth({json.dumps(jsonable_encoder(init_oauth))})
|
||||
"""
|
||||
|
||||
html += """
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -94,7 +104,7 @@ def get_redoc_html(
|
||||
|
||||
def get_swagger_ui_oauth2_redirect_html() -> HTMLResponse:
|
||||
html = """
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-US">
|
||||
<body onload="run()">
|
||||
</body>
|
||||
|
||||
@@ -11,7 +11,7 @@ try:
|
||||
import email_validator
|
||||
|
||||
assert email_validator # make autoflake ignore the unused import
|
||||
from pydantic.types import EmailStr # type: ignore
|
||||
from pydantic.types import EmailStr
|
||||
except ImportError: # pragma: no cover
|
||||
logger.warning(
|
||||
"email-validator not installed, email fields will be treated as str.\n"
|
||||
|
||||
@@ -151,6 +151,10 @@ def get_openapi_path(
|
||||
security_schemes: Dict[str, Any] = {}
|
||||
definitions: Dict[str, Any] = {}
|
||||
assert route.methods is not None, "Methods must be a list"
|
||||
assert (
|
||||
route.response_class and route.response_class.media_type
|
||||
), "A response class with media_type is needed to generate OpenAPI"
|
||||
route_response_media_type: str = route.response_class.media_type
|
||||
if route.include_in_schema:
|
||||
for method in route.methods:
|
||||
operation = get_openapi_operation_metadata(route=route, method=method)
|
||||
@@ -185,7 +189,7 @@ def get_openapi_path(
|
||||
field, model_name_map=model_name_map, ref_prefix=REF_PREFIX
|
||||
)
|
||||
response.setdefault("content", {}).setdefault(
|
||||
route.response_class.media_type, {}
|
||||
route_response_media_type, {}
|
||||
)["schema"] = response_schema
|
||||
status_text: Optional[str] = status_code_ranges.get(
|
||||
str(additional_status_code).upper()
|
||||
@@ -213,7 +217,7 @@ def get_openapi_path(
|
||||
] = route.response_description
|
||||
operation.setdefault("responses", {}).setdefault(
|
||||
status_code, {}
|
||||
).setdefault("content", {}).setdefault(route.response_class.media_type, {})[
|
||||
).setdefault("content", {}).setdefault(route_response_media_type, {})[
|
||||
"schema"
|
||||
] = response_schema
|
||||
|
||||
@@ -221,7 +225,7 @@ def get_openapi_path(
|
||||
if (all_route_params or route.body_field) and not any(
|
||||
[
|
||||
status in operation["responses"]
|
||||
for status in [http422, "4xx", "default"]
|
||||
for status in [http422, "4XX", "default"]
|
||||
]
|
||||
):
|
||||
operation["responses"][http422] = {
|
||||
@@ -279,7 +283,7 @@ def get_openapi(
|
||||
if path_definitions:
|
||||
definitions.update(path_definitions)
|
||||
if definitions:
|
||||
components.setdefault("schemas", {}).update(definitions)
|
||||
components["schemas"] = {k: definitions[k] for k in sorted(definitions)}
|
||||
if components:
|
||||
output["components"] = components
|
||||
output["paths"] = paths
|
||||
|
||||
@@ -11,7 +11,7 @@ from fastapi.dependencies.utils import (
|
||||
get_parameterless_sub_dependant,
|
||||
solve_dependencies,
|
||||
)
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder
|
||||
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
|
||||
from fastapi.utils import create_cloned_field, generate_operation_id_for_path
|
||||
from pydantic import BaseConfig, BaseModel, Schema
|
||||
@@ -38,8 +38,8 @@ def serialize_response(
|
||||
*,
|
||||
field: Field = None,
|
||||
response: Response,
|
||||
include: Set[str] = None,
|
||||
exclude: Set[str] = set(),
|
||||
include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
by_alias: bool = True,
|
||||
skip_defaults: bool = False,
|
||||
) -> Any:
|
||||
@@ -53,7 +53,7 @@ def serialize_response(
|
||||
elif isinstance(errors_, list):
|
||||
errors.extend(errors_)
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
raise ValidationError(errors, field.type_)
|
||||
return jsonable_encoder(
|
||||
value,
|
||||
include=include,
|
||||
@@ -65,14 +65,14 @@ def serialize_response(
|
||||
return jsonable_encoder(response)
|
||||
|
||||
|
||||
def get_app(
|
||||
def get_request_handler(
|
||||
dependant: Dependant,
|
||||
body_field: Field = None,
|
||||
status_code: int = 200,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_field: Field = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
dependency_overrides_provider: Any = None,
|
||||
@@ -195,12 +195,12 @@ class APIRoute(routing.Route):
|
||||
name: str = None,
|
||||
methods: Optional[Union[Set[str], List[str]]] = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Optional[Type[Response]] = None,
|
||||
dependency_overrides_provider: Any = None,
|
||||
) -> None:
|
||||
self.path = path
|
||||
@@ -215,9 +215,6 @@ class APIRoute(routing.Route):
|
||||
)
|
||||
self.response_model = response_model
|
||||
if self.response_model:
|
||||
assert lenient_issubclass(
|
||||
response_class, JSONResponse
|
||||
), "To declare a type the response must be a JSON response"
|
||||
response_name = "Response_" + self.unique_id
|
||||
self.response_field: Optional[Field] = Field(
|
||||
name=response_name,
|
||||
@@ -249,6 +246,9 @@ class APIRoute(routing.Route):
|
||||
self.dependencies = []
|
||||
self.summary = summary
|
||||
self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "")
|
||||
# if a "form feed" character (page break) is found in the description text,
|
||||
# truncate description text to the content preceding the first "form feed"
|
||||
self.description = self.description.split("\f")[0]
|
||||
self.response_description = response_description
|
||||
self.responses = responses or {}
|
||||
response_fields = {}
|
||||
@@ -294,19 +294,20 @@ class APIRoute(routing.Route):
|
||||
)
|
||||
self.body_field = get_body_field(dependant=self.dependant, name=self.unique_id)
|
||||
self.dependency_overrides_provider = dependency_overrides_provider
|
||||
self.app = request_response(
|
||||
get_app(
|
||||
dependant=self.dependant,
|
||||
body_field=self.body_field,
|
||||
status_code=self.status_code,
|
||||
response_class=self.response_class,
|
||||
response_field=self.secure_cloned_response_field,
|
||||
response_model_include=self.response_model_include,
|
||||
response_model_exclude=self.response_model_exclude,
|
||||
response_model_by_alias=self.response_model_by_alias,
|
||||
response_model_skip_defaults=self.response_model_skip_defaults,
|
||||
dependency_overrides_provider=self.dependency_overrides_provider,
|
||||
)
|
||||
self.app = request_response(self.get_route_handler())
|
||||
|
||||
def get_route_handler(self) -> Callable:
|
||||
return get_request_handler(
|
||||
dependant=self.dependant,
|
||||
body_field=self.body_field,
|
||||
status_code=self.status_code,
|
||||
response_class=self.response_class or JSONResponse,
|
||||
response_field=self.secure_cloned_response_field,
|
||||
response_model_include=self.response_model_include,
|
||||
response_model_exclude=self.response_model_exclude,
|
||||
response_model_by_alias=self.response_model_by_alias,
|
||||
response_model_skip_defaults=self.response_model_skip_defaults,
|
||||
dependency_overrides_provider=self.dependency_overrides_provider,
|
||||
)
|
||||
|
||||
|
||||
@@ -341,15 +342,17 @@ class APIRouter(routing.Router):
|
||||
deprecated: bool = None,
|
||||
methods: Optional[Union[Set[str], List[str]]] = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
route_class_override: Optional[Type[APIRoute]] = None,
|
||||
) -> None:
|
||||
route = self.route_class(
|
||||
route_class = route_class_override or self.route_class
|
||||
route = route_class(
|
||||
path,
|
||||
endpoint=endpoint,
|
||||
response_model=response_model,
|
||||
@@ -389,12 +392,12 @@ class APIRouter(routing.Router):
|
||||
deprecated: bool = None,
|
||||
methods: List[str] = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@@ -445,6 +448,7 @@ class APIRouter(routing.Router):
|
||||
tags: List[str] = None,
|
||||
dependencies: Sequence[params.Depends] = None,
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
default_response_class: Optional[Type[Response]] = None,
|
||||
) -> None:
|
||||
if prefix:
|
||||
assert prefix.startswith("/"), "A path prefix must start with '/'"
|
||||
@@ -484,8 +488,9 @@ class APIRouter(routing.Router):
|
||||
response_model_by_alias=route.response_model_by_alias,
|
||||
response_model_skip_defaults=route.response_model_skip_defaults,
|
||||
include_in_schema=route.include_in_schema,
|
||||
response_class=route.response_class,
|
||||
response_class=route.response_class or default_response_class,
|
||||
name=route.name,
|
||||
route_class_override=type(route),
|
||||
)
|
||||
elif isinstance(route, routing.Route):
|
||||
self.add_route(
|
||||
@@ -518,15 +523,14 @@ class APIRouter(routing.Router):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
|
||||
return self.api_route(
|
||||
path=path,
|
||||
response_model=response_model,
|
||||
@@ -563,12 +567,12 @@ class APIRouter(routing.Router):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
@@ -607,12 +611,12 @@ class APIRouter(routing.Router):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
@@ -651,12 +655,12 @@ class APIRouter(routing.Router):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
@@ -695,12 +699,12 @@ class APIRouter(routing.Router):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
@@ -739,12 +743,12 @@ class APIRouter(routing.Router):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
@@ -783,12 +787,12 @@ class APIRouter(routing.Router):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
@@ -827,12 +831,12 @@ class APIRouter(routing.Router):
|
||||
responses: Dict[Union[int, str], Dict[str, Any]] = None,
|
||||
deprecated: bool = None,
|
||||
operation_id: str = None,
|
||||
response_model_include: Set[str] = None,
|
||||
response_model_exclude: Set[str] = set(),
|
||||
response_model_include: Union[SetIntStr, DictIntStrAny] = None,
|
||||
response_model_exclude: Union[SetIntStr, DictIntStrAny] = set(),
|
||||
response_model_by_alias: bool = True,
|
||||
response_model_skip_defaults: bool = False,
|
||||
include_in_schema: bool = True,
|
||||
response_class: Type[Response] = JSONResponse,
|
||||
response_class: Type[Response] = None,
|
||||
name: str = None,
|
||||
) -> Callable:
|
||||
return self.api_route(
|
||||
|
||||
@@ -58,10 +58,10 @@ def create_cloned_field(field: Field) -> Field:
|
||||
use_type = original_type
|
||||
if lenient_issubclass(original_type, BaseModel):
|
||||
original_type = cast(Type[BaseModel], original_type)
|
||||
use_type = create_model( # type: ignore
|
||||
use_type = create_model(
|
||||
original_type.__name__,
|
||||
__config__=original_type.__config__,
|
||||
__validators__=original_type.__validators__,
|
||||
__validators__=original_type.__validators__, # type: ignore
|
||||
)
|
||||
for f in original_type.__fields__.values():
|
||||
use_type.__fields__[f.name] = f
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -19,8 +19,8 @@ classifiers = [
|
||||
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
|
||||
]
|
||||
requires = [
|
||||
"starlette >=0.11.1,<=0.12.7",
|
||||
"pydantic >=0.30,<=0.30.0"
|
||||
"starlette >=0.12.9,<=0.12.9",
|
||||
"pydantic >=0.32.2,<=0.32.2"
|
||||
]
|
||||
description-file = "README.md"
|
||||
requires-python = ">=3.6"
|
||||
@@ -39,6 +39,9 @@ test = [
|
||||
"email_validator",
|
||||
"sqlalchemy",
|
||||
"databases[sqlite]",
|
||||
"orjson",
|
||||
"async_exit_stack",
|
||||
"async_generator"
|
||||
]
|
||||
doc = [
|
||||
"mkdocs",
|
||||
@@ -60,4 +63,6 @@ all = [
|
||||
"ujson",
|
||||
"email_validator",
|
||||
"uvicorn",
|
||||
"async_exit_stack",
|
||||
"async_generator"
|
||||
]
|
||||
|
||||
114
tests/test_custom_route_class.py
Normal file
114
tests/test_custom_route_class.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import pytest
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from fastapi.routing import APIRoute
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class APIRouteA(APIRoute):
|
||||
x_type = "A"
|
||||
|
||||
|
||||
class APIRouteB(APIRoute):
|
||||
x_type = "B"
|
||||
|
||||
|
||||
class APIRouteC(APIRoute):
|
||||
x_type = "C"
|
||||
|
||||
|
||||
router_a = APIRouter(route_class=APIRouteA)
|
||||
router_b = APIRouter(route_class=APIRouteB)
|
||||
router_c = APIRouter(route_class=APIRouteC)
|
||||
|
||||
|
||||
@router_a.get("/")
|
||||
def get_a():
|
||||
return {"msg": "A"}
|
||||
|
||||
|
||||
@router_b.get("/")
|
||||
def get_b():
|
||||
return {"msg": "B"}
|
||||
|
||||
|
||||
@router_c.get("/")
|
||||
def get_c():
|
||||
return {"msg": "C"}
|
||||
|
||||
|
||||
router_b.include_router(router=router_c, prefix="/c")
|
||||
router_a.include_router(router=router_b, prefix="/b")
|
||||
app.include_router(router=router_a, prefix="/a")
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/a/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Get A",
|
||||
"operationId": "get_a_a__get",
|
||||
}
|
||||
},
|
||||
"/a/b/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Get B",
|
||||
"operationId": "get_b_a_b__get",
|
||||
}
|
||||
},
|
||||
"/a/b/c/": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Get C",
|
||||
"operationId": "get_c_a_b_c__get",
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path,expected_status,expected_response",
|
||||
[
|
||||
("/a", 200, {"msg": "A"}),
|
||||
("/a/b", 200, {"msg": "B"}),
|
||||
("/a/b/c", 200, {"msg": "C"}),
|
||||
("/openapi.json", 200, openapi_schema),
|
||||
],
|
||||
)
|
||||
def test_get_path(path, expected_status, expected_response):
|
||||
response = client.get(path)
|
||||
assert response.status_code == expected_status
|
||||
assert response.json() == expected_response
|
||||
|
||||
|
||||
def test_route_classes():
|
||||
routes = {}
|
||||
r: APIRoute
|
||||
for r in app.router.routes:
|
||||
routes[r.path] = r
|
||||
assert routes["/a/"].x_type == "A"
|
||||
assert routes["/a/b/"].x_type == "B"
|
||||
assert routes["/a/b/c/"].x_type == "C"
|
||||
216
tests/test_default_response_class.py
Normal file
216
tests/test_default_response_class.py
Normal file
@@ -0,0 +1,216 @@
|
||||
from typing import Any
|
||||
|
||||
import orjson
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from starlette.responses import HTMLResponse, JSONResponse, PlainTextResponse
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
|
||||
class ORJSONResponse(JSONResponse):
|
||||
media_type = "application/x-orjson"
|
||||
|
||||
def render(self, content: Any) -> bytes:
|
||||
return orjson.dumps(content)
|
||||
|
||||
|
||||
class OverrideResponse(JSONResponse):
|
||||
media_type = "application/x-override"
|
||||
|
||||
|
||||
app = FastAPI(default_response_class=ORJSONResponse)
|
||||
router_a = APIRouter()
|
||||
router_a_a = APIRouter()
|
||||
router_a_b_override = APIRouter() # Overrides default class
|
||||
router_b_override = APIRouter() # Overrides default class
|
||||
router_b_a = APIRouter()
|
||||
router_b_a_c_override = APIRouter() # Overrides default class again
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def get_root():
|
||||
return {"msg": "Hello World"}
|
||||
|
||||
|
||||
@app.get("/override", response_class=PlainTextResponse)
|
||||
def get_path_override():
|
||||
return "Hello World"
|
||||
|
||||
|
||||
@router_a.get("/")
|
||||
def get_a():
|
||||
return {"msg": "Hello A"}
|
||||
|
||||
|
||||
@router_a.get("/override", response_class=PlainTextResponse)
|
||||
def get_a_path_override():
|
||||
return "Hello A"
|
||||
|
||||
|
||||
@router_a_a.get("/")
|
||||
def get_a_a():
|
||||
return {"msg": "Hello A A"}
|
||||
|
||||
|
||||
@router_a_a.get("/override", response_class=PlainTextResponse)
|
||||
def get_a_a_path_override():
|
||||
return "Hello A A"
|
||||
|
||||
|
||||
@router_a_b_override.get("/")
|
||||
def get_a_b():
|
||||
return "Hello A B"
|
||||
|
||||
|
||||
@router_a_b_override.get("/override", response_class=HTMLResponse)
|
||||
def get_a_b_path_override():
|
||||
return "Hello A B"
|
||||
|
||||
|
||||
@router_b_override.get("/")
|
||||
def get_b():
|
||||
return "Hello B"
|
||||
|
||||
|
||||
@router_b_override.get("/override", response_class=HTMLResponse)
|
||||
def get_b_path_override():
|
||||
return "Hello B"
|
||||
|
||||
|
||||
@router_b_a.get("/")
|
||||
def get_b_a():
|
||||
return "Hello B A"
|
||||
|
||||
|
||||
@router_b_a.get("/override", response_class=HTMLResponse)
|
||||
def get_b_a_path_override():
|
||||
return "Hello B A"
|
||||
|
||||
|
||||
@router_b_a_c_override.get("/")
|
||||
def get_b_a_c():
|
||||
return "Hello B A C"
|
||||
|
||||
|
||||
@router_b_a_c_override.get("/override", response_class=OverrideResponse)
|
||||
def get_b_a_c_path_override():
|
||||
return {"msg": "Hello B A C"}
|
||||
|
||||
|
||||
router_b_a.include_router(
|
||||
router_b_a_c_override, prefix="/c", default_response_class=HTMLResponse
|
||||
)
|
||||
router_b_override.include_router(router_b_a, prefix="/a")
|
||||
router_a.include_router(router_a_a, prefix="/a")
|
||||
router_a.include_router(
|
||||
router_a_b_override, prefix="/b", default_response_class=PlainTextResponse
|
||||
)
|
||||
app.include_router(router_a, prefix="/a")
|
||||
app.include_router(
|
||||
router_b_override, prefix="/b", default_response_class=PlainTextResponse
|
||||
)
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
orjson_type = "application/x-orjson"
|
||||
text_type = "text/plain; charset=utf-8"
|
||||
html_type = "text/html; charset=utf-8"
|
||||
override_type = "application/x-override"
|
||||
|
||||
|
||||
def test_app():
|
||||
with client:
|
||||
response = client.get("/")
|
||||
assert response.json() == {"msg": "Hello World"}
|
||||
assert response.headers["content-type"] == orjson_type
|
||||
|
||||
|
||||
def test_app_override():
|
||||
with client:
|
||||
response = client.get("/override")
|
||||
assert response.content == b"Hello World"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_a():
|
||||
with client:
|
||||
response = client.get("/a")
|
||||
assert response.json() == {"msg": "Hello A"}
|
||||
assert response.headers["content-type"] == orjson_type
|
||||
|
||||
|
||||
def test_router_a_override():
|
||||
with client:
|
||||
response = client.get("/a/override")
|
||||
assert response.content == b"Hello A"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_a_a():
|
||||
with client:
|
||||
response = client.get("/a/a")
|
||||
assert response.json() == {"msg": "Hello A A"}
|
||||
assert response.headers["content-type"] == orjson_type
|
||||
|
||||
|
||||
def test_router_a_a_override():
|
||||
with client:
|
||||
response = client.get("/a/a/override")
|
||||
assert response.content == b"Hello A A"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_a_b():
|
||||
with client:
|
||||
response = client.get("/a/b")
|
||||
assert response.content == b"Hello A B"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_a_b_override():
|
||||
with client:
|
||||
response = client.get("/a/b/override")
|
||||
assert response.content == b"Hello A B"
|
||||
assert response.headers["content-type"] == html_type
|
||||
|
||||
|
||||
def test_router_b():
|
||||
with client:
|
||||
response = client.get("/b")
|
||||
assert response.content == b"Hello B"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_b_override():
|
||||
with client:
|
||||
response = client.get("/b/override")
|
||||
assert response.content == b"Hello B"
|
||||
assert response.headers["content-type"] == html_type
|
||||
|
||||
|
||||
def test_router_b_a():
|
||||
with client:
|
||||
response = client.get("/b/a")
|
||||
assert response.content == b"Hello B A"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_b_a_override():
|
||||
with client:
|
||||
response = client.get("/b/a/override")
|
||||
assert response.content == b"Hello B A"
|
||||
assert response.headers["content-type"] == html_type
|
||||
|
||||
|
||||
def test_router_b_a_c():
|
||||
with client:
|
||||
response = client.get("/b/a/c")
|
||||
assert response.content == b"Hello B A C"
|
||||
assert response.headers["content-type"] == html_type
|
||||
|
||||
|
||||
def test_router_b_a_c_override():
|
||||
with client:
|
||||
response = client.get("/b/a/c/override")
|
||||
assert response.json() == {"msg": "Hello B A C"}
|
||||
assert response.headers["content-type"] == override_type
|
||||
206
tests/test_default_response_class_router.py
Normal file
206
tests/test_default_response_class_router.py
Normal file
@@ -0,0 +1,206 @@
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from starlette.responses import HTMLResponse, JSONResponse, PlainTextResponse
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
|
||||
class OverrideResponse(JSONResponse):
|
||||
media_type = "application/x-override"
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
router_a = APIRouter()
|
||||
router_a_a = APIRouter()
|
||||
router_a_b_override = APIRouter() # Overrides default class
|
||||
router_b_override = APIRouter() # Overrides default class
|
||||
router_b_a = APIRouter()
|
||||
router_b_a_c_override = APIRouter() # Overrides default class again
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def get_root():
|
||||
return {"msg": "Hello World"}
|
||||
|
||||
|
||||
@app.get("/override", response_class=PlainTextResponse)
|
||||
def get_path_override():
|
||||
return "Hello World"
|
||||
|
||||
|
||||
@router_a.get("/")
|
||||
def get_a():
|
||||
return {"msg": "Hello A"}
|
||||
|
||||
|
||||
@router_a.get("/override", response_class=PlainTextResponse)
|
||||
def get_a_path_override():
|
||||
return "Hello A"
|
||||
|
||||
|
||||
@router_a_a.get("/")
|
||||
def get_a_a():
|
||||
return {"msg": "Hello A A"}
|
||||
|
||||
|
||||
@router_a_a.get("/override", response_class=PlainTextResponse)
|
||||
def get_a_a_path_override():
|
||||
return "Hello A A"
|
||||
|
||||
|
||||
@router_a_b_override.get("/")
|
||||
def get_a_b():
|
||||
return "Hello A B"
|
||||
|
||||
|
||||
@router_a_b_override.get("/override", response_class=HTMLResponse)
|
||||
def get_a_b_path_override():
|
||||
return "Hello A B"
|
||||
|
||||
|
||||
@router_b_override.get("/")
|
||||
def get_b():
|
||||
return "Hello B"
|
||||
|
||||
|
||||
@router_b_override.get("/override", response_class=HTMLResponse)
|
||||
def get_b_path_override():
|
||||
return "Hello B"
|
||||
|
||||
|
||||
@router_b_a.get("/")
|
||||
def get_b_a():
|
||||
return "Hello B A"
|
||||
|
||||
|
||||
@router_b_a.get("/override", response_class=HTMLResponse)
|
||||
def get_b_a_path_override():
|
||||
return "Hello B A"
|
||||
|
||||
|
||||
@router_b_a_c_override.get("/")
|
||||
def get_b_a_c():
|
||||
return "Hello B A C"
|
||||
|
||||
|
||||
@router_b_a_c_override.get("/override", response_class=OverrideResponse)
|
||||
def get_b_a_c_path_override():
|
||||
return {"msg": "Hello B A C"}
|
||||
|
||||
|
||||
router_b_a.include_router(
|
||||
router_b_a_c_override, prefix="/c", default_response_class=HTMLResponse
|
||||
)
|
||||
router_b_override.include_router(router_b_a, prefix="/a")
|
||||
router_a.include_router(router_a_a, prefix="/a")
|
||||
router_a.include_router(
|
||||
router_a_b_override, prefix="/b", default_response_class=PlainTextResponse
|
||||
)
|
||||
app.include_router(router_a, prefix="/a")
|
||||
app.include_router(
|
||||
router_b_override, prefix="/b", default_response_class=PlainTextResponse
|
||||
)
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
json_type = "application/json"
|
||||
text_type = "text/plain; charset=utf-8"
|
||||
html_type = "text/html; charset=utf-8"
|
||||
override_type = "application/x-override"
|
||||
|
||||
|
||||
def test_app():
|
||||
with client:
|
||||
response = client.get("/")
|
||||
assert response.json() == {"msg": "Hello World"}
|
||||
assert response.headers["content-type"] == json_type
|
||||
|
||||
|
||||
def test_app_override():
|
||||
with client:
|
||||
response = client.get("/override")
|
||||
assert response.content == b"Hello World"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_a():
|
||||
with client:
|
||||
response = client.get("/a")
|
||||
assert response.json() == {"msg": "Hello A"}
|
||||
assert response.headers["content-type"] == json_type
|
||||
|
||||
|
||||
def test_router_a_override():
|
||||
with client:
|
||||
response = client.get("/a/override")
|
||||
assert response.content == b"Hello A"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_a_a():
|
||||
with client:
|
||||
response = client.get("/a/a")
|
||||
assert response.json() == {"msg": "Hello A A"}
|
||||
assert response.headers["content-type"] == json_type
|
||||
|
||||
|
||||
def test_router_a_a_override():
|
||||
with client:
|
||||
response = client.get("/a/a/override")
|
||||
assert response.content == b"Hello A A"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_a_b():
|
||||
with client:
|
||||
response = client.get("/a/b")
|
||||
assert response.content == b"Hello A B"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_a_b_override():
|
||||
with client:
|
||||
response = client.get("/a/b/override")
|
||||
assert response.content == b"Hello A B"
|
||||
assert response.headers["content-type"] == html_type
|
||||
|
||||
|
||||
def test_router_b():
|
||||
with client:
|
||||
response = client.get("/b")
|
||||
assert response.content == b"Hello B"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_b_override():
|
||||
with client:
|
||||
response = client.get("/b/override")
|
||||
assert response.content == b"Hello B"
|
||||
assert response.headers["content-type"] == html_type
|
||||
|
||||
|
||||
def test_router_b_a():
|
||||
with client:
|
||||
response = client.get("/b/a")
|
||||
assert response.content == b"Hello B A"
|
||||
assert response.headers["content-type"] == text_type
|
||||
|
||||
|
||||
def test_router_b_a_override():
|
||||
with client:
|
||||
response = client.get("/b/a/override")
|
||||
assert response.content == b"Hello B A"
|
||||
assert response.headers["content-type"] == html_type
|
||||
|
||||
|
||||
def test_router_b_a_c():
|
||||
with client:
|
||||
response = client.get("/b/a/c")
|
||||
assert response.content == b"Hello B A C"
|
||||
assert response.headers["content-type"] == html_type
|
||||
|
||||
|
||||
def test_router_b_a_c_override():
|
||||
with client:
|
||||
response = client.get("/b/a/c/override")
|
||||
assert response.json() == {"msg": "Hello B A C"}
|
||||
assert response.headers["content-type"] == override_type
|
||||
349
tests/test_dependency_contextmanager.py
Normal file
349
tests/test_dependency_contextmanager.py
Normal 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
12
tests/test_fakeasync.py
Normal 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()
|
||||
136
tests/test_infer_param_optionality.py
Normal file
136
tests/test_infer_param_optionality.py
Normal file
@@ -0,0 +1,136 @@
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
user_router = APIRouter()
|
||||
item_router = APIRouter()
|
||||
|
||||
|
||||
@user_router.get("/")
|
||||
def get_users():
|
||||
return [{"user_id": "u1"}, {"user_id": "u2"}]
|
||||
|
||||
|
||||
@user_router.get("/{user_id}")
|
||||
def get_user(user_id: str):
|
||||
return {"user_id": user_id}
|
||||
|
||||
|
||||
@item_router.get("/")
|
||||
def get_items(user_id: str = None):
|
||||
if user_id is None:
|
||||
return [{"item_id": "i1", "user_id": "u1"}, {"item_id": "i2", "user_id": "u2"}]
|
||||
else:
|
||||
return [{"item_id": "i2", "user_id": user_id}]
|
||||
|
||||
|
||||
@item_router.get("/{item_id}")
|
||||
def get_item(item_id: str, user_id: str = None):
|
||||
if user_id is None:
|
||||
return {"item_id": item_id}
|
||||
else:
|
||||
return {"item_id": item_id, "user_id": user_id}
|
||||
|
||||
|
||||
app.include_router(user_router, prefix="/users")
|
||||
app.include_router(item_router, prefix="/items")
|
||||
|
||||
app.include_router(item_router, prefix="/users/{user_id}/items")
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_get_users():
|
||||
"""Check that /users returns expected data"""
|
||||
response = client.get("/users")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [{"user_id": "u1"}, {"user_id": "u2"}]
|
||||
|
||||
|
||||
def test_get_user():
|
||||
"""Check that /users/{user_id} returns expected data"""
|
||||
response = client.get("/users/abc123")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"user_id": "abc123"}
|
||||
|
||||
|
||||
def test_get_items_1():
|
||||
"""Check that /items returns expected data"""
|
||||
response = client.get("/items")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [
|
||||
{"item_id": "i1", "user_id": "u1"},
|
||||
{"item_id": "i2", "user_id": "u2"},
|
||||
]
|
||||
|
||||
|
||||
def test_get_items_2():
|
||||
"""Check that /items returns expected data with user_id specified"""
|
||||
response = client.get("/items?user_id=abc123")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [{"item_id": "i2", "user_id": "abc123"}]
|
||||
|
||||
|
||||
def test_get_item_1():
|
||||
"""Check that /items/{item_id} returns expected data"""
|
||||
response = client.get("/items/item01")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": "item01"}
|
||||
|
||||
|
||||
def test_get_item_2():
|
||||
"""Check that /items/{item_id} returns expected data with user_id specified"""
|
||||
response = client.get("/items/item01?user_id=abc123")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": "item01", "user_id": "abc123"}
|
||||
|
||||
|
||||
def test_get_users_items():
|
||||
"""Check that /users/{user_id}/items returns expected data"""
|
||||
response = client.get("/users/abc123/items")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [{"item_id": "i2", "user_id": "abc123"}]
|
||||
|
||||
|
||||
def test_get_users_item():
|
||||
"""Check that /users/{user_id}/items returns expected data"""
|
||||
response = client.get("/users/abc123/items/item01")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": "item01", "user_id": "abc123"}
|
||||
|
||||
|
||||
def test_schema_1():
|
||||
"""Check that the user_id is a required path parameter under /users"""
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
r = response.json()
|
||||
|
||||
d = {
|
||||
"required": True,
|
||||
"schema": {"title": "User_Id", "type": "string"},
|
||||
"name": "user_id",
|
||||
"in": "path",
|
||||
}
|
||||
|
||||
assert d in r["paths"]["/users/{user_id}"]["get"]["parameters"]
|
||||
assert d in r["paths"]["/users/{user_id}/items/"]["get"]["parameters"]
|
||||
|
||||
|
||||
def test_schema_2():
|
||||
"""Check that the user_id is an optional query parameter under /items"""
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
r = response.json()
|
||||
|
||||
d = {
|
||||
"required": False,
|
||||
"schema": {"title": "User_Id", "type": "string"},
|
||||
"name": "user_id",
|
||||
"in": "query",
|
||||
}
|
||||
|
||||
assert d in r["paths"]["/items/{item_id}"]["get"]["parameters"]
|
||||
assert d in r["paths"]["/items/"]["get"]["parameters"]
|
||||
@@ -21,18 +21,21 @@ class User(BaseModel):
|
||||
username: str
|
||||
|
||||
|
||||
def get_current_user(oauth_header: str = Security(reusable_oauth2)):
|
||||
# Here we use string annotations to test them
|
||||
def get_current_user(oauth_header: "str" = Security(reusable_oauth2)):
|
||||
user = User(username=oauth_header)
|
||||
return user
|
||||
|
||||
|
||||
@app.post("/login")
|
||||
def read_current_user(form_data: OAuth2PasswordRequestFormStrict = Depends()):
|
||||
# Here we use string annotations to test them
|
||||
def read_current_user(form_data: "OAuth2PasswordRequestFormStrict" = Depends()):
|
||||
return form_data
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(current_user: User = Depends(get_current_user)):
|
||||
# Here we use string annotations to test them
|
||||
def read_current_user(current_user: "User" = Depends(get_current_user)):
|
||||
return current_user
|
||||
|
||||
|
||||
|
||||
28
tests/test_swagger_ui_init_oauth.py
Normal file
28
tests/test_swagger_ui_init_oauth.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from fastapi import FastAPI
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
swagger_ui_init_oauth = {"clientId": "the-foo-clients", "appName": "The Predendapp"}
|
||||
|
||||
app = FastAPI(swagger_ui_init_oauth=swagger_ui_init_oauth)
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
async def read_items():
|
||||
return {"id": "foo"}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_swagger_ui():
|
||||
response = client.get("/docs")
|
||||
assert response.status_code == 200
|
||||
print(response.text)
|
||||
assert f"ui.initOAuth" in response.text
|
||||
assert f'"appName": "The Predendapp"' in response.text
|
||||
assert f'"clientId": "the-foo-clients"' in response.text
|
||||
|
||||
|
||||
def test_response():
|
||||
response = client.get("/items/")
|
||||
assert response.json() == {"id": "foo"}
|
||||
@@ -0,0 +1,34 @@
|
||||
import gzip
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from starlette.requests import Request
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from custom_request_and_route.tutorial001 import app
|
||||
|
||||
|
||||
@app.get("/check-class")
|
||||
async def check_gzip_request(request: Request):
|
||||
return {"request_class": type(request).__name__}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("compress", [True, False])
|
||||
def test_gzip_request(compress):
|
||||
n = 1000
|
||||
headers = {}
|
||||
body = [1] * n
|
||||
data = json.dumps(body).encode()
|
||||
if compress:
|
||||
data = gzip.compress(data)
|
||||
headers["Content-Encoding"] = "gzip"
|
||||
response = client.post("/sum", data=data, headers=headers)
|
||||
assert response.json() == {"sum": n}
|
||||
|
||||
|
||||
def test_request_class():
|
||||
response = client.get("/check-class")
|
||||
assert response.json() == {"request_class": "GzipRequest"}
|
||||
@@ -0,0 +1,27 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from custom_request_and_route.tutorial002 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_endpoint_works():
|
||||
response = client.post("/", json=[1, 2, 3])
|
||||
assert response.json() == 6
|
||||
|
||||
|
||||
def test_exception_handler_body_access():
|
||||
response = client.post("/", json={"numbers": [1, 2, 3]})
|
||||
|
||||
assert response.json() == {
|
||||
"detail": {
|
||||
"body": '{"numbers": [1, 2, 3]}',
|
||||
"errors": [
|
||||
{
|
||||
"loc": ["body", "numbers"],
|
||||
"msg": "value is not a valid list",
|
||||
"type": "type_error.list",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from custom_request_and_route.tutorial003 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_get():
|
||||
response = client.get("/")
|
||||
assert response.json() == {"message": "Not timed"}
|
||||
assert "X-Response-Time" not in response.headers
|
||||
|
||||
|
||||
def test_get_timed():
|
||||
response = client.get("/timed")
|
||||
assert response.json() == {"message": "It's the time of my life"}
|
||||
assert "X-Response-Time" in response.headers
|
||||
assert float(response.headers["X-Response-Time"]) > 0
|
||||
@@ -81,7 +81,7 @@ def test_get_validation_error():
|
||||
response = client.get("/items/foo")
|
||||
assert response.status_code == 400
|
||||
validation_error_str_lines = [
|
||||
b"1 validation error",
|
||||
b"1 validation error for Request",
|
||||
b"path -> item_id",
|
||||
b" value is not a valid integer (type=type_error.integer)",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from path_operation_advanced_configuration.tutorial003 import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/": {
|
||||
"post": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Item"}
|
||||
}
|
||||
},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"summary": "Create an item",
|
||||
"description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item\n",
|
||||
"operationId": "create_item_items__post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Item"}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Item": {
|
||||
"title": "Item",
|
||||
"required": ["name", "price"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"price": {"title": "Price", "type": "number"},
|
||||
"description": {"title": "Description", "type": "string"},
|
||||
"tax": {"title": "Tax", "type": "number"},
|
||||
"tags": {
|
||||
"title": "Tags",
|
||||
"uniqueItems": True,
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"default": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
"title": "ValidationError",
|
||||
"required": ["loc", "msg", "type"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loc": {
|
||||
"title": "Location",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"msg": {"title": "Message", "type": "string"},
|
||||
"type": {"title": "Error Type", "type": "string"},
|
||||
},
|
||||
},
|
||||
"HTTPValidationError": {
|
||||
"title": "HTTPValidationError",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {
|
||||
"title": "Detail",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_query_params_str_validations():
|
||||
response = client.post("/items/", json={"name": "Foo", "price": 42})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"name": "Foo",
|
||||
"price": 42,
|
||||
"description": None,
|
||||
"tax": None,
|
||||
"tags": [],
|
||||
}
|
||||
@@ -106,8 +106,9 @@ def test_openapi():
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"ctx": {"enum_values": ["alexnet", "resnet", "lenet"]},
|
||||
"loc": ["path", "model_name"],
|
||||
"msg": "value is not a valid enumeration member",
|
||||
"msg": "value is not a valid enumeration member; permitted: 'alexnet', 'resnet', 'lenet'",
|
||||
"type": "type_error.enum",
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user