Compare commits

...

33 Commits

Author SHA1 Message Date
Sebastián Ramírez
a9673f145a 🔖 Release version 0.46.0 2020-01-08 23:28:03 +01:00
Sebastián Ramírez
aa28784f6b 📝 Update release notes 2020-01-08 23:26:15 +01:00
Sebastián Ramírez
7b3319ddab ✏️ Tweak typos and configs (#837) 2020-01-08 23:25:29 +01:00
Sebastián Ramírez
6a20078259 📝 Update release notes 2020-01-08 23:03:08 +01:00
wxq0309
d0ab909544 📝 Add link to Chinese FastAPI posts (#810) 2020-01-08 23:01:58 +01:00
Sebastián Ramírez
13da029dca 📝 Update release notes 2020-01-08 22:51:55 +01:00
Jesse P. Johnson
91fe90e8e6 Implement OAuth2 authorization_code integration (#797) 2020-01-08 22:47:19 +01:00
Sebastián Ramírez
a0c8f93231 📝 Update release notes 2020-01-08 22:35:33 +01:00
Christoph Deil
cad6a6e0c1 📝 Highlight all new lines in docs example upgrade (#795) 2020-01-08 22:34:14 +01:00
Sebastián Ramírez
fd5ba77b83 📝 Update release notes 2020-01-08 22:23:54 +01:00
James Kaplan
cb1410426e 🐛 Fix callback handling in sub-routers (#792) 2020-01-08 22:22:14 +01:00
Sebastián Ramírez
7b31e52766 📝 Update release notes 2020-01-08 22:02:32 +01:00
Ken Kinder
4151616681 ✏️ Fix typos (#784) 2020-01-08 22:01:22 +01:00
Sebastián Ramírez
462e24e864 📝 Update release notes 2020-01-08 22:00:44 +01:00
Xucong ZHAN
9f9ed7a6bd 📝 Add four JP articles to external links (#783) 2020-01-08 21:58:33 +01:00
Sebastián Ramírez
b6ea9ea2ca 📝 Update release notes 2020-01-08 21:53:14 +01:00
Roald Storm
b85b2e3942 Add support for subtypes of main types in jsonable_encoder 2020-01-08 21:50:21 +01:00
Sebastián Ramírez
08fc2a41ca 📝 Update release notes 2020-01-08 19:16:57 +01:00
Dustyposa
8d3dcbcd1b fix type UrlStr -> HttpUrl (#832) 2020-01-08 00:08:43 -08:00
Justin DuJardin
861ed37c97 📝 update twitter compose tweet links (#813)
The links to post @tiangolo were not working
2019-12-30 15:22:11 -07:00
Sebastián Ramírez
7a445402d4 📝 Update release notes 2019-12-13 11:32:17 +01:00
Sebastián Ramírez
04c8502cc7 📝 Add docs for correctly using Peewee (#789) 2019-12-13 11:29:18 +01:00
Sebastián Ramírez
c7c69586ae 🔖 Release version 0.45.0 2019-12-11 18:05:19 +01:00
Sebastián Ramírez
4e09feda9e 📝 Update release notes 2019-12-11 18:02:53 +01:00
Ben Dayan
73260971b5 Add support for OpenAPI Callbacks (#722) 2019-12-11 17:58:00 +01:00
Sebastián Ramírez
b36bfff56e 📝 Update release notes 2019-12-09 20:04:04 +01:00
Sebastián Ramírez
83d04df8a6 🔊 Refactor logging (#781) 2019-12-09 20:02:44 +01:00
Sebastián Ramírez
7bc78c5fd3 📝 Update release notes 2019-12-09 19:15:37 +01:00
prostomarkeloff
ae8fa3aacd 📝 Add article about FastAPI to external links (#766) 2019-12-09 19:13:28 +01:00
Sebastián Ramírez
08bc120771 📝 Update release notes 2019-12-09 19:01:40 +01:00
Sebastián Ramírez
a39efb029f 💬 Rephrase handling-errors to remove gender while keeping readability (#780) 2019-12-09 18:59:29 +01:00
Sebastián Ramírez
58ca98285f 📝 Update release notes 2019-12-09 18:43:09 +01:00
prostomarkeloff
3f5f81bbdc 📝 Change 'Schema' to 'Field' in docs (#746) 2019-12-09 14:48:54 +01:00
58 changed files with 2152 additions and 141 deletions

View File

@@ -25,8 +25,8 @@ after_script:
- bash <(curl -s https://codecov.io/bash)
deploy:
provider: script
script: bash scripts/deploy.sh
on:
tags: true
python: "3.6"
provider: script
script: bash scripts/deploy.sh
on:
tags: true
python: "3.6"

View File

@@ -5,8 +5,8 @@
<em>FastAPI framework, high performance, easy to learn, fast to code, ready for production</em>
</p>
<p align="center">
<a href="https://travis-ci.org/tiangolo/fastapi" target="_blank">
<img src="https://travis-ci.org/tiangolo/fastapi.svg?branch=master" alt="Build Status">
<a href="https://travis-ci.com/tiangolo/fastapi" target="_blank">
<img src="https://travis-ci.com/tiangolo/fastapi.svg?branch=master" alt="Build Status">
</a>
<a href="https://codecov.io/gh/tiangolo/fastapi" target="_blank">
<img src="https://codecov.io/gh/tiangolo/fastapi/branch/master/graph/badge.svg" alt="Coverage">
@@ -206,8 +206,7 @@ Now modify the file `main.py` to receive a body from a `PUT` request.
Declare the body using standard Python types, thanks to Pydantic.
```Python hl_lines="2 7 8 9 10 24"
```Python hl_lines="2 7 8 9 10 23 24 25"
from fastapi import FastAPI
from pydantic import BaseModel
@@ -407,7 +406,7 @@ Used by FastAPI / Starlette:
* <a href="http://www.uvicorn.org" target="_blank"><code>uvicorn</code></a> - for the server that loads and serves your application.
You can install all of these with `pip3 install fastapi[all]`.
You can install all of these with `pip install fastapi[all]`.
## License

View File

@@ -51,10 +51,20 @@ Here's an incomplete list of some of them.
* <a href="https://qiita.com/hikarut/items/b178af2e2440c67c6ac4" target="_blank">フロントエンド開発者向けのDockerによるPython開発環境構築</a> by <a href="https://qiita.com/hikarut" target="_blank">Hikaru Takahashi</a>.
* <a href="https://rightcode.co.jp/blog/information-technology/fastapi-tutorial-todo-apps-environment" target="_blank">【第1回】FastAPIチュートリアル: ToDoアプリを作ってみよう【環境構築編】</a> by <a href="https://rightcode.co.jp/author/jun" target="_blank">ライトコードメディア編集部</a>
* <a href="https://rightcode.co.jp/blog/information-technology/fastapi-tutorial-todo-apps-model-building" target="_blank">【第2回】FastAPIチュートリアル: ToDoアプリを作ってみよう【モデル構築編】</a> by <a href="https://rightcode.co.jp/author/jun" target="_blank">ライトコードメディア編集部</a>
* <a href="https://rightcode.co.jp/blog/information-technology/fastapi-tutorial-todo-apps-authentication-user-registration" target="_blank">【第3回】FastAPIチュートリアル: toDoアプリを作ってみよう【認証・ユーザ登録編】</a> by <a href="https://rightcode.co.jp/author/jun" target="_blank">ライトコードメディア編集部</a>
* <a href="https://rightcode.co.jp/blog/information-technology/fastapi-tutorial-todo-apps-admin-page-improvement" target="_blank">【第4回】FastAPIチュートリアル: toDoアプリを作ってみよう【管理者ページ改良編】</a> by <a href="https://rightcode.co.jp/author/jun" target="_blank">ライトコードメディア編集部</a>
### Chinese
* <a href="https://cloud.tencent.com/developer/article/1431448" target="_blank">使用FastAPI框架快速构建高性能的api服务</a> by <a href="https://cloud.tencent.com/developer/user/5471722" target="_blank">逍遥散人</a>.
* <a href="https://wxq0309.github.io/" target="_blank">FastAPI框架中文文档</a> by <a href="https://wxq0309.github.io/" target="_blank">何大仙</a>.
### Vietnamese
* <a href="https://fullstackstation.com/fastapi-trien-khai-bang-docker/" target="_blank">FASTAPI: TRIỂN KHAI BẰNG DOCKER</a> by <a href="https://fullstackstation.com/author/figonking/" target="_blank">Nguyễn Nhân</a>.
@@ -63,6 +73,8 @@ Here's an incomplete list of some of them.
* <a href="https://habr.com/ru/post/454440/" target="_blank">Мелкая питонячая радость #2: Starlette - Солидная примочка FastAPI</a> by <a href="https://habr.com/ru/users/57uff3r/" target="_blank">Andrey Korchak</a>.
* <a href="https://habr.com/ru/post/478620/" target="_blank">Почему Вы должны попробовать FastAPI?</a> by <a href="https://github.com/prostomarkeloff" target="_blank">prostomarkeloff</a>.
## Podcasts
* <a href="https://pythonbytes.fm/episodes/show/123/time-to-right-the-py-wrongs?time_in_sec=855" target="_blank">FastAPI on PythonBytes</a> by <a href="https://pythonbytes.fm/" target="_blank">Python Bytes FM</a>.

View File

@@ -56,7 +56,7 @@ You can:
## Tweet about **FastAPI**
<a href="http://twitter.com/home/?status=I'm loving FastAPI because... https://github.com/tiangolo/fastapi cc @tiangolo" target="_blank">Tweet about **FastAPI**</a> and let me and others why you like it.
<a href="https://twitter.com/compose/tweet?text=I'm loving FastAPI because... https://github.com/tiangolo/fastapi cc @tiangolo" target="_blank">Tweet about **FastAPI**</a> and let me and others know why you like it.
## Let me know how are you using **FastAPI**
@@ -64,7 +64,7 @@ I love to hear about how **FastAPI** is being used, what have you liked in it, i
You can let me know:
* <a href="http://twitter.com/home/?status=Hey @tiangolo, I'm using FastAPI at..." target="_blank">On **Twitter**</a>.
* <a href="https://twitter.com/compose/tweet?text=Hey @tiangolo, I'm using FastAPI at..." target="_blank">On **Twitter**</a>.
* <a href="https://www.linkedin.com/in/tiangolo/" target="_blank">On **Linkedin**</a>.
* <a href="https://medium.com/@tiangolo" target="_blank">On **Medium**</a>.

View File

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

View File

@@ -5,8 +5,8 @@
<em>FastAPI framework, high performance, easy to learn, fast to code, ready for production</em>
</p>
<p align="center">
<a href="https://travis-ci.org/tiangolo/fastapi" target="_blank">
<img src="https://travis-ci.org/tiangolo/fastapi.svg?branch=master" alt="Build Status">
<a href="https://travis-ci.com/tiangolo/fastapi" target="_blank">
<img src="https://travis-ci.com/tiangolo/fastapi.svg?branch=master" alt="Build Status">
</a>
<a href="https://codecov.io/gh/tiangolo/fastapi" target="_blank">
<img src="https://codecov.io/gh/tiangolo/fastapi/branch/master/graph/badge.svg" alt="Coverage">
@@ -206,8 +206,7 @@ Now modify the file `main.py` to receive a body from a `PUT` request.
Declare the body using standard Python types, thanks to Pydantic.
```Python hl_lines="2 7 8 9 10 24"
```Python hl_lines="2 7 8 9 10 23 24 25"
from fastapi import FastAPI
from pydantic import BaseModel
@@ -407,7 +406,7 @@ Used by FastAPI / Starlette:
* <a href="http://www.uvicorn.org" target="_blank"><code>uvicorn</code></a> - for the server that loads and serves your application.
You can install all of these with `pip3 install fastapi[all]`.
You can install all of these with `pip install fastapi[all]`.
## License

View File

@@ -1,5 +1,31 @@
## Latest changes
## 0.46.0
* Fix typos and tweak configs. PR [#837](https://github.com/tiangolo/fastapi/pull/837).
* Add link to Chinese article in [External Links](https://fastapi.tiangolo.com/external-links/). PR [810](https://github.com/tiangolo/fastapi/pull/810) by [@wxq0309](https://github.com/wxq0309).
* Implement `OAuth2AuthorizationCodeBearer` class. PR [#797](https://github.com/tiangolo/fastapi/pull/797) by [@kuwv](https://github.com/kuwv).
* Update example upgrade in docs main page. PR [#795](https://github.com/tiangolo/fastapi/pull/795) by [@cdeil](https://github.com/cdeil).
* Fix callback handling for sub-routers. PR [#792](https://github.com/tiangolo/fastapi/pull/792) by [@jekirl](https://github.com/jekirl).
* Fix typos. PR [#784](https://github.com/tiangolo/fastapi/pull/784) by [@kkinder](https://github.com/kkinder).
* Add 4 Japanese articles to [External Links](https://fastapi.tiangolo.com/external-links/). PR [#783](https://github.com/tiangolo/fastapi/pull/783) by [@HymanZHAN](https://github.com/HymanZHAN).
* Add support for subtypes of main types in `jsonable_encoder`, e.g. asyncpg's UUIDs. PR [#756](https://github.com/tiangolo/fastapi/pull/756) by [@RmStorm](https://github.com/RmStorm).
* Fix usage of Pydantic's `HttpUrl` in docs. PR [#832](https://github.com/tiangolo/fastapi/pull/832) by [@Dustyposa](https://github.com/Dustyposa).
* Fix Twitter links in docs. PR [#813](https://github.com/tiangolo/fastapi/pull/813) by [@justindujardin](https://github.com/justindujardin).
* Add docs for correctly [using FastAPI with Peewee ORM](https://fastapi.tiangolo.com/tutorial/sql-databases-peewee/). Including how to overwrite parts of Peewee to correctly handle async threads. PR [#789](https://github.com/tiangolo/fastapi/pull/789).
## 0.45.0
* Add support for OpenAPI Callbacks:
* New docs: [OpenAPI Callbacks](https://fastapi.tiangolo.com/tutorial/openapi-callbacks/).
* Refactor generation of `operationId`s to be valid Python names (also valid variables in most languages).
* Add `default_response_class` parameter to `APIRouter`.
* Original PR [#722](https://github.com/tiangolo/fastapi/pull/722) by [@booooh](https://github.com/booooh).
* Refactor logging to use the same logger everywhere, update log strings and levels. PR [#781](https://github.com/tiangolo/fastapi/pull/781).
* Add article to [External Links](https://fastapi.tiangolo.com/external-links/): [Почему Вы должны попробовать FastAPI?](https://habr.com/ru/post/478620/). PR [#766](https://github.com/tiangolo/fastapi/pull/766) by [@prostomarkeloff](https://github.com/prostomarkeloff).
* Remove gender bias in docs for handling errors. PR [#780](https://github.com/tiangolo/fastapi/pull/780). Original idea in PR [#761](https://github.com/tiangolo/fastapi/pull/761) by [@classywhetten](https://github.com/classywhetten).
* Rename docs and references to `body-schema` to `body-fields` to keep in line with Pydantic. PR [#746](https://github.com/tiangolo/fastapi/pull/746) by [@prostomarkeloff](https://github.com/prostomarkeloff).
## 0.44.1
* Add GitHub social preview images to git. PR [#752](https://github.com/tiangolo/fastapi/pull/752).

View File

@@ -1,13 +1,13 @@
from typing import Set
from fastapi import FastAPI
from pydantic import BaseModel, UrlStr
from pydantic import BaseModel, HttpUrl
app = FastAPI()
class Image(BaseModel):
url: UrlStr
url: HttpUrl
name: str

View File

@@ -1,13 +1,13 @@
from typing import List, Set
from fastapi import FastAPI
from pydantic import BaseModel, UrlStr
from pydantic import BaseModel, HttpUrl
app = FastAPI()
class Image(BaseModel):
url: UrlStr
url: HttpUrl
name: str

View File

@@ -1,13 +1,13 @@
from typing import List, Set
from fastapi import FastAPI
from pydantic import BaseModel, UrlStr
from pydantic import BaseModel, HttpUrl
app = FastAPI()
class Image(BaseModel):
url: UrlStr
url: HttpUrl
name: str

View File

@@ -1,13 +1,13 @@
from typing import List
from fastapi import FastAPI
from pydantic import BaseModel, UrlStr
from pydantic import BaseModel, HttpUrl
app = FastAPI()
class Image(BaseModel):
url: UrlStr
url: HttpUrl
name: str

View File

@@ -0,0 +1,52 @@
from fastapi import APIRouter, FastAPI
from pydantic import BaseModel, HttpUrl
from starlette.responses import JSONResponse
app = FastAPI()
class Invoice(BaseModel):
id: str
title: str = None
customer: str
total: float
class InvoiceEvent(BaseModel):
description: str
paid: bool
class InvoiceEventReceived(BaseModel):
ok: bool
invoices_callback_router = APIRouter(default_response_class=JSONResponse)
@invoices_callback_router.post(
"{$callback_url}/invoices/{$request.body.id}", response_model=InvoiceEventReceived,
)
def invoice_notification(body: InvoiceEvent):
pass
@app.post("/invoices/", callbacks=invoices_callback_router.routes)
def create_invoice(invoice: Invoice, callback_url: HttpUrl = None):
"""
Create an invoice.
This will (let's imagine) let the API user (some external developer) create an
invoice.
And this path operation will:
* Send the invoice to the client.
* Collect the money from the client.
* Send a notification back to the API user (the external developer), as a callback.
* At this point is that the API will somehow send a POST request to the
external API with the notification of the invoice event
(e.g. "payment successful").
"""
# Send the invoice, collect the money, send the notification (the callback)
return {"msg": "Invoice received"}

View File

View File

@@ -0,0 +1,30 @@
from . import models, schemas
def get_user(user_id: int):
return models.User.filter(models.User.id == user_id).first()
def get_user_by_email(email: str):
return models.User.filter(models.User.email == email).first()
def get_users(skip: int = 0, limit: int = 100):
return list(models.User.select().offset(skip).limit(limit))
def create_user(user: schemas.UserCreate):
fake_hashed_password = user.password + "notreallyhashed"
db_user = models.User(email=user.email, hashed_password=fake_hashed_password)
db_user.save()
return db_user
def get_items(skip: int = 0, limit: int = 100):
return list(models.Item.select().offset(skip).limit(limit))
def create_user_item(item: schemas.ItemCreate, user_id: int):
db_item = models.Item(**item.dict(), owner_id=user_id)
db_item.save()
return db_item

View File

@@ -0,0 +1,26 @@
from contextvars import ContextVar
import peewee
DATABASE_NAME = "test.db"
class PeeweeConnectionState(peewee._ConnectionState):
def __init__(self, **kwargs):
super().__setattr__("_state", {})
self._state["closed"] = ContextVar("closed", default=True)
self._state["conn"] = ContextVar("conn", default=None)
self._state["ctx"] = ContextVar("ctx", default=[])
self._state["transactions"] = ContextVar("transactions", default=[])
super().__init__(**kwargs)
def __setattr__(self, name, value):
self._state[name].set(value)
def __getattr__(self, name):
return self._state[name].get()
db = peewee.SqliteDatabase(DATABASE_NAME, check_same_thread=False)
db._state = PeeweeConnectionState()

View File

@@ -0,0 +1,70 @@
import time
from typing import List
from fastapi import Depends, FastAPI, HTTPException
from . import crud, database, models, schemas
database.db.connect()
database.db.create_tables([models.User, models.Item])
database.db.close()
app = FastAPI()
# Dependency
def get_db():
try:
database.db.connect()
yield
finally:
if not database.db.is_closed():
database.db.close()
@app.post("/users/", response_model=schemas.User, dependencies=[Depends(get_db)])
def create_user(user: schemas.UserCreate):
db_user = crud.get_user_by_email(email=user.email)
if db_user:
raise HTTPException(status_code=400, detail="Email already registered")
return crud.create_user(user=user)
@app.get("/users/", response_model=List[schemas.User], dependencies=[Depends(get_db)])
def read_users(skip: int = 0, limit: int = 100):
users = crud.get_users(skip=skip, limit=limit)
return users
@app.get(
"/users/{user_id}", response_model=schemas.User, dependencies=[Depends(get_db)]
)
def read_user(user_id: int):
db_user = crud.get_user(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,
dependencies=[Depends(get_db)],
)
def create_item_for_user(user_id: int, item: schemas.ItemCreate):
return crud.create_user_item(item=item, user_id=user_id)
@app.get("/items/", response_model=List[schemas.Item], dependencies=[Depends(get_db)])
def read_items(skip: int = 0, limit: int = 100):
items = crud.get_items(skip=skip, limit=limit)
return items
@app.get(
"/slowusers/", response_model=List[schemas.User], dependencies=[Depends(get_db)]
)
def read_slow_users(skip: int = 0, limit: int = 100):
time.sleep(15) # Fake long processing request
users = crud.get_users(skip=skip, limit=limit)
return users

View File

@@ -0,0 +1,21 @@
import peewee
from .database import db
class User(peewee.Model):
email = peewee.CharField(unique=True, index=True)
hashed_password = peewee.CharField()
is_active = peewee.BooleanField(default=True)
class Meta:
database = db
class Item(peewee.Model):
title = peewee.CharField(index=True)
description = peewee.CharField(index=True)
owner = peewee.ForeignKeyField(User, backref="items")
class Meta:
database = db

View File

@@ -0,0 +1,49 @@
from typing import Any, List
import peewee
from pydantic import BaseModel
from pydantic.utils import GetterDict
class PeeweeGetterDict(GetterDict):
def get(self, key: Any, default: Any = None):
res = getattr(self._obj, key, default)
if isinstance(res, peewee.ModelSelect):
return list(res)
return res
class ItemBase(BaseModel):
title: str
description: str = None
class ItemCreate(ItemBase):
pass
class Item(ItemBase):
id: int
owner_id: int
class Config:
orm_mode = True
getter_dict = PeeweeGetterDict
class UserBase(BaseModel):
email: str
class UserCreate(UserBase):
password: str
class User(UserBase):
id: int
is_active: bool
items: List[Item] = []
class Config:
orm_mode = True
getter_dict = PeeweeGetterDict

View File

@@ -5,7 +5,7 @@ The same way you can declare additional validation and metadata in path operatio
First, you have to import it:
```Python hl_lines="2"
{!./src/body_schema/tutorial001.py!}
{!./src/body_fields/tutorial001.py!}
```
!!! warning
@@ -17,7 +17,7 @@ First, you have to import it:
You can then use `Field` with model attributes:
```Python hl_lines="9 10"
{!./src/body_schema/tutorial001.py!}
{!./src/body_fields/tutorial001.py!}
```
`Field` works the same way as `Query`, `Path` and `Body`, it has all the same parameters, etc.
@@ -34,7 +34,7 @@ You can then use `Field` with model attributes:
!!! tip
Notice how each model's attribute with a type, default value and `Field` has the same structure as a path operation function's parameter, with `Field` instead of `Path`, `Query` and `Body`.
## Schema extras
## JSON Schema extras
In `Field`, `Path`, `Query`, `Body` and others you'll see later, you can declare extra parameters apart from those described before.
@@ -48,12 +48,12 @@ If you know JSON Schema and want to add extra information apart from what we hav
For example, you can use that functionality to pass a <a href="http://json-schema.org/latest/json-schema-validation.html#rfc.section.8.5" target="_blank">JSON Schema example</a> field to a body request JSON Schema:
```Python hl_lines="20 21 22 23 24 25"
{!./src/body_schema/tutorial002.py!}
{!./src/body_fields/tutorial002.py!}
```
And it would look in the `/docs` like this:
<img src="/img/tutorial/body-schema/image01.png">
<img src="/img/tutorial/body-fields/image01.png">
## Recap

View File

@@ -118,7 +118,7 @@ Apart from normal singular types like `str`, `int`, `float`, etc. You can use mo
To see all the options you have, checkout the docs for <a href="https://pydantic-docs.helpmanual.io/#exotic-types" target="_blank">Pydantic's exotic types</a>. You will see some examples in the next chapter.
For example, as in the `Image` model we have a `url` field, we can declare it to be instead of a `str`, a Pydantic's `UrlStr`:
For example, as in the `Image` model we have a `url` field, we can declare it to be instead of a `str`, a Pydantic's `HttpUrl`:
```Python hl_lines="4 10"
{!./src/body_nested_models/tutorial005.py!}

View File

@@ -4,9 +4,9 @@ This client could be a browser with a frontend, the code from someone else, an I
You could need to tell that client that:
* He doesn't have enough privileges for that operation.
* He doesn't have access to that resource.
* The item he was trying to access doesn't exist.
* The client doesn't have enough privileges for that operation.
* The client doesn't have access to that resource.
* The item the client was trying to access doesn't exist.
* etc.
In these cases, you would normally return an **HTTP status code** in the range of **400** (from 400 to 499).

View File

@@ -0,0 +1,186 @@
You could create an API with a *path operation* that could trigger a request to an *external API* created by someone else (probably the same developer that would be *using* your API).
The process that happens when your API app calls the *external API* is named a "callback". Because the software that the external developer wrote sends a request to your API and then your API *calls back*, sending a request to an *external API* (that was probably created by the same developer).
In this case, you could want to document how that external API *should* look like. What *path operation* it should have, what body it should expect, what response it should return, etc.
## An app with callbacks
Let's see all this with an example.
Imagine you develop an app that allows creating invoices.
These invoices will have an `id`, `title` (optional), `customer`, and `total`.
The user of your API (an external developer) will create an invoice in your API with a POST request.
Then your API will (let's imagine):
* Send the invoice to some customer of the external developer.
* Collect the money.
* Send a notification back to the API user (the external developer).
* This will be done by sending a POST request (from *your API*) to some *external API* provided by that external developer (this is the "callback").
## The normal **FastAPI** app
Let's first see how the normal API app would look like before adding the callback.
It will have a *path operation* that will receive an `Invoice` body, and a query parameter `callback_url` that will contain the URL for the callback.
This part is pretty normal, most of the code is probably already familiar to you:
```Python hl_lines="8 9 10 11 12 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53"
{!./src/openapi_callbacks/tutorial001.py!}
```
!!! tip
The `callback_url` query parameter uses a Pydantic <a href="https://pydantic-docs.helpmanual.io/usage/types/#urls" target="_blank">URL</a> type.
The only new thing is the `callbacks=messages_callback_router.routes` as an argument to the *path operation decorator*. We'll see what that is next.
## Documenting the callback
The actual callback code will depend heavily on your own API app.
And it will probably vary a lot from one app to the next.
It could be just one or two lines of code, like:
```Python
callback_url = "https://example.com/api/v1/invoices/events/"
requests.post(callback_url, json={"description": "Invoice paid", "paid": True})
```
But possibly the most important part of the callback is making sure that your API user (the external developer) implements the *external API* correctly, according to the data that *your API* is going to send in the request body of the callback, etc.
So, what we will do next is add the code to document how that *external API* should look like to receive the callback from *your API*.
That documentation will show up in the Swagger UI at `/docs` in your API, and it will let external developers know how to build the *external API*.
This example doesn't implement the callback itself (that could be just a line of code), only the documentation part.
!!! tip
The actual callback is just an HTTP request.
When implementing the callback yourself, you could use something like <a href="https://www.encode.io/httpx/" target="_blank">HTTPX</a> or <a href="https://requests.readthedocs.io/" target="_blank">Requests</a>.
## Write the callback documentation code
This code won't be executed in your app, we only need it to *document* how that *external API* should look like.
But, you already know how to easily create automatic documentation for an API with **FastAPI**.
So we are going to use that same knowledge to document how the *external API* should look like... by creating the *path operation(s)* that the external API should implement (the ones your API will call).
!!! tip
When writing the code to document a callback, it might be useful to imagine that you are that *external developer*. And that you are currently implementing the *external API*, not *your API*.
Temporarily adopting this point of view (of the *external developer*) can help you feel like it's more obvious where to put the parameters, the Pydantic model for the body, for the response, etc. for that *external API*.
### Create a callback `APIRouter`
First create a new `APIRouter` that will contain one or more callbacks.
This router will never be added to an actual `FastAPI` app (i.e. it will never be passed to `app.include_router(...)`).
Because of that, you need to declare what will be the `default_response_class`, and set it to `JSONResponse`.
!!! Note "Technical Details"
The `response_class` is normally set by the `FastAPI` app during the call to `app.include_router(some_router)`.
But as we are never calling `app.include_router(some_router)`, we need to set the `default_response_class` during creation of the `APIRouter`.
```Python hl_lines="3 24"
{!./src/openapi_callbacks/tutorial001.py!}
```
### Create the callback *path operation*
To create the callback *path operation* use the same `APIRouter` you created above.
It should look just like a normal FastAPI *path operation*:
* It should probably have a declaration of the body it should receive, e.g. `body: InvoiceEvent`.
* And it could also have a declaration of the response it should return, e.g. `response_model=InvoiceEventReceived`.
```Python hl_lines="15 16 17 20 21 27 28 29 30 31"
{!./src/openapi_callbacks/tutorial001.py!}
```
There are 2 main differences from a normal *path operation*:
* It doesn't need to have any actual code, because your app will never call this code. It's only used to document the *external API*. So, the function could just have `pass`.
* The *path* can contain an <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#key-expression" target="_blank">OpenAPI 3 expression</a> (see more below) where it can use variables with parameters and parts of the original request sent to *your API*.
### The callback path expression
The callback *path* can have an <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#key-expression" target="_blank">OpenAPI 3 expression</a> that can contain parts of the original request sent to *your API*.
In this case, it's the `str`:
```Python
"{$callback_url}/invoices/{$request.body.id}"
```
So, if your API user (the external developer) sends a request to *your API* to:
```
https://yourapi.com/invoices/?callback_url=https://www.external.org/events
```
with a JSON body of:
```JSON
{
"id": "2expen51ve",
"customer": "Mr. Richie Rich",
"total": "9999"
}
```
Then *your API* will process the invoice, and at some point later, send a callback request to the `callback_url` (the *external API*):
```
https://www.external.org/events/invoices/2expen51ve
```
with a JSON body containing something like:
```JSON
{
"description": "Payment celebration",
"paid": true
}
```
and it would expect a response from that *external API* with a JSON body like:
```JSON
{
"ok": true
}
```
!!! tip
Notice how the callback URL used contains the URL received as a query parameter in `callback_url` (`https://www.external.org/events`) and also the invoice `id` from inside of the JSON body (`2expen51ve`).
### Add the callback router
At this point you have the *callback path operation(s)* needed (the one(s) that the *external developer* should implement in the *external API*) in the callback router you created above.
Now use the parameter `callbacks` in *your API's path operation decorator* to pass the attribute `.routes` (that's actually just a `list` of routes/*path operations*) from that callback router:
```Python hl_lines="34"
{!./src/openapi_callbacks/tutorial001.py!}
```
!!! tip
Notice that you are not passing the router itself (`invoices_callback_router`) to `callback=`, but the attribute `.routes`, as in `invoices_callback_router.routes`.
### Check the docs
Now you can start your app with Uvicorn and go to <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a>.
You will see your docs including a "Callback" section for your *path operation* that shows how the *external API* should look like:
<img src="/img/tutorial/openapi-callbacks/image01.png">

View File

@@ -0,0 +1,408 @@
!!! warning
If you are just starting, the <a href="https://fastapi.tiangolo.com/tutorial/sql-databases/" target="_blank">SQLAlchemy tutorial</a> should be enough.
Feel free to skip this.
If you are starting a project from scratch, you are probably better off with <a href="https://fastapi.tiangolo.com/tutorial/sql-databases/" target="_blank">SQLAlchemy ORM</a>, or any other async ORM.
If you already have a code base that uses <a href="http://docs.peewee-orm.com/en/latest/" target="_blank">Peewee ORM</a>, you can check here how to use it with **FastAPI**.
!!! warning "Python 3.7+ required"
You will need Python 3.7 or above to safely use Peewee with FastAPI.
## Peewee for async
Peewee was not designed for async frameworks, or with them in mind.
Peewee has some heavy assumptions about its defaults and about how it should be used.
If you are developing an application with an older non-async framework, and can work with all its defaults, **it can be a great tool**.
But if you need to change some of the defaults, support more than one predefined database, work with an async framework (like FastAPI), etc, you will need to add quite some complex extra code to override those defaults.
Nevertheless, it's possible to do it, and here you'll see exactly what code you have to add to be able to use Peewee with FastAPI.
!!! note "Technical Details"
You can read more about Peewee's stand about async in Python <a href="http://docs.peewee-orm.com/en/latest/peewee/database.html#async-with-gevent" target="_blank">in the docs</a>, <a href="https://github.com/coleifer/peewee/issues/263#issuecomment-517347032" target="_blank">an issue</a>, <a href="https://github.com/coleifer/peewee/pull/2072#issuecomment-563215132" target="_blank">a PR</a>.
## The same app
We are going to create the same application as in the <a href="https://fastapi.tiangolo.com/tutorial/sql-databases/" target="_blank">SQLAlchemy tutorial</a>.
Most of the code is actually the same.
So, we are going to focus only on the differences.
## File structure
Let's say you have a directory named `my_super_project` that contains a sub-directory called `sql_app` with a structure like this:
```
.
└── sql_app
├── __init__.py
├── crud.py
├── database.py
├── main.py
└── schemas.py
```
This is almost the same structure as we had for the SQLAlchemy tutorial.
Now let's see what each file/module does.
## Create the Peewee parts
Let's refer to the file `sql_app/database.py`.
### The standard Peewee code
Let's first check all the normal Peewee code, create a Peewee database:
```Python hl_lines="3 5 24"
{!./src/sql_databases_peewee/sql_app/database.py!}
```
!!! tip
Have in mind that if you wanted to use a different database, like PostgreSQL, you couldn't just change the string. You would need to use a different Peewee database class.
#### Note
The argument:
```Python
check_same_thread=False
```
is equivalent to the one in the SQLAlchemy tutorial:
```Python
connect_args={"check_same_thread": False}
```
...it is needed only for `SQLite`.
!!! info "Technical Details"
Exactly the same technical details as in the <a href="https://fastapi.tiangolo.com/tutorial/sql-databases/#note" target="_blank">SQLAlchemy tutorial</a> apply.
### Make Peewee async-compatible `PeeweeConnectionState`
The main issue with Peewee and FastAPI is that Peewee relies heavily on <a href="https://docs.python.org/3/library/threading.html#thread-local-data" target="_blank">Python's `threading.local`</a>, and it doesn't have a direct way to override it or let you handle connections/sessions directly (as is done in the SQLAlchemy tutorial).
And `threading.local` is not compatible with the new async features of modern Python.
!!! note "Technical Details"
`threading.local` is used to have a "magic" variable that has a different value for each thread.
This was useful in older frameworks designed to have one single thread per request, no more, no less.
Using this, each request would have its own database connection/session, which is the actual final goal.
But FastAPI, using the new async features, could handle more than one request on the same thread. And at the same time, for a single request, it could run multiple things in different threads (in a threadpool), depending on if you use `async def` or normal `def`. This is what gives all the performance improvements to FastAPI.
But Python 3.7 and above provide a more advanced alternative to `threading.local`, that can also be used in the places where `threading.local` would be used, but is compatible with the new async features.
We are going to use that. It's called <a href="https://docs.python.org/3/library/contextvars.html" target="_blank">`contextvars`</a>.
We are going to override the internal parts of Peewee that use `threading.local` and replace them with `contextvars`, with the corresponding updates.
This might seem a bit complex (and it actually is), you don't really need to completely understand how it works to use it.
We will create a `PeeweeConnectionState`:
```Python hl_lines="8 9 10 11 12 13 14 15 16 17 18 19 20 21"
{!./src/sql_databases_peewee/sql_app/database.py!}
```
This class inherits from a special internal class used by Peewee.
It has all the logic to make Peewee use `contextvars` instead of `threading.local`.
`contextvars` works a bit differently than `threading.local`. But the rest of Peewee's internal code assumes that this class works with `threading.local`.
So, we need to do some extra tricks to make it work as if it was just using `threading.local`. The `__init__`, `__setattr__`, and `__getattr__` implement all the required tricks for this to be used by Peewee without knowing that it is now compatible with FastAPI.
!!! tip
This will just make Peewee behave correctly when used with FastAPI. Not randomly opening or closing connections that are being used, creating errors, etc.
But it doesn't give Peewee async super-powers. You should still use normal `def` functions and not `async def`.
### Use the custom `PeeweeConnectionState` class
Now, overwrite the `._state` internal attribute in the Peewee database `db` object using the new `PeeweeConnectionState`:
```Python hl_lines="26"
{!./src/sql_databases_peewee/sql_app/database.py!}
```
!!! tip
Make sure you overwrite `db._state` *after* creating `db`.
!!! tip
You would do the same for any other Peewee database, including `PostgresqlDatabase`, `MySQLDatabase`, etc.
## Create the database models
Let's now see the file `sql_app/models.py`.
### Create Peewee models for our data
Now create the Peewee models (classes) for `User` and `Item`.
This is the same you would do if you followed the Peewee tutorial and updated the models to have the same data as in the SQLAlchemy tutorial.
!!! tip
Peewee also uses the term "**model**" to refer to these classes and instances that interact with the database.
But Pydantic also uses the term "**model**" to refer to something different, the data validation, conversion, and documentation classes and instances.
Import `db` from `database` (the file `database.py` from above) and use it here.
```Python hl_lines="3 6 7 8 9 10 11 12 15 16 17 18 19 20 21"
{!./src/sql_databases_peewee/sql_app/models.py!}
```
!!! tip
Peewee creates several magic attributes.
It will automatically add an `id` attribute as an integer to be the primary key.
It will chose the name of the tables based on the class names.
For the `Item`, it will create an attribute `owner_id` with the integer ID of the `User`. But we don't declare it anywhere.
## Create the Pydantic models
Now let's check the file `sql_app/schemas.py`.
!!! tip
To avoid confusion between the Peewee *models* and the Pydantic *models*, we will have the file `models.py` with the Peewee models, and the file `schemas.py` with the Pydantic models.
These Pydantic models define more or less a "schema" (a valid data shape).
So this will help us avoiding confusion while using both.
### Create the Pydantic *models* / schemas
Create all the same Pydantic models as in the SQLAlchemy tutorial:
```Python hl_lines="16 17 18 21 22 25 26 27 28 29 30 34 35 38 39 42 43 44 45 46 47 48"
{!./src/sql_databases_peewee/sql_app/schemas.py!}
```
!!! tip
Here we are creating the models with an `id`.
We didn't explicitly specify an `id` attribute in the Peewee models, but Peewee adds one automatically.
We are also adding the magic `owner_id` attribute to `Item`.
### Create a `PeeweeGetterDict` for the Pydantic *models* / schemas
When you access a relationship in a Peewee object, like in `some_user.items`, Peewee doesn't provide a `list` of `Item`.
It provides a special custom object of class `ModelSelect`.
It's possible to create a `list` of its items with `list(some_user.items)`.
But the object itself is not a `list`. And it's also not an actual Python <a href="https://docs.python.org/3/glossary.html#term-generator" target="_blank">generator</a>. Because of this, Pydantic doesn't know by default how to convert it to a `list` of Pydantic *models* / schemas.
But recent versions of Pydantic allow providing a custom class that inherits from `pydantic.utils.GetterDict`, to provide the functionality used when using the `orm_mode = True` to retrieve the values for ORM model attributes.
We are going to create a custom `PeeweeGetterDict` class and use it in all the same Pydantic *models* / schemas that use `orm_mode`:
```Python hl_lines="3 8 9 10 11 12 13 31 49"
{!./src/sql_databases_peewee/sql_app/schemas.py!}
```
Here we are checking if the attribute that is being accessed (e.g. `.items` in `some_user.items`) is an instance of `peewee.ModelSelect`.
And if that's the case, just return a `list` with it.
And then we use it in the Pydantic *models* / schemas that use `orm_mode = True`, with the configuration variable `getter_dict = PeeweeGetterDict`.
!!! tip
We only need to create one `PeeweeGetterDict` class, and we can use it in all the Pydantic *models* / schemas.
## CRUD utils
Now let's see the file `sql_app/crud.py`.
### Create all the CRUD utils
Create all the same CRUD utils as in the SQLAlchemy tutorial, all the code is very similar:
```Python hl_lines="1 4 5 8 9 12 13 16 17 18 19 20 23 24 27 28 29 30"
{!./src/sql_databases_peewee/sql_app/crud.py!}
```
There are some differences with the code for the SQLAlchemy tutorial.
We don't pass a `db` attribute around. Instead we use the models directly. This is because the `db` object is a global object, that includes all the connection logic. That's why we had to do all the `contextvars` updates above.
Aso, when returning several objects, like in `get_users`, we directly call `list`, like in:
```Python
list(models.User.select())
```
This is for the same reason that we had to create a custom `PeeweeGetterDict`. But by returning something that is already a `list` instead of the `peewee.ModelSelect` the `response_model` in the path operation with `List[models.User]` (that we'll see later) will work correctly.
## Main **FastAPI** app
And now in the file `sql_app/main.py` let's integrate and use all the other parts we created before.
### Create the database tables
In a very simplistic way create the database tables:
```Python hl_lines="8 9 10"
{!./src/sql_databases_peewee/sql_app/main.py!}
```
### Create a dependency
Create a dependency that will connect the database right at the beginning of a request and disconnect it at the end:
```Python hl_lines="15 16 17 18 19 20 21 22"
{!./src/sql_databases_peewee/sql_app/main.py!}
```
Here we have an empty `yield` because we are actually not using the database object directly.
It is connecting to the database and storing the connection data in an internal variable that is independent for each request (using the `contextvars` tricks from above).
And then, in each *path operation function* that needs to access the database we add it as a dependency.
But we are not using the value given by this dependency (it actually doesn't give any value, as it has an empty `yield`). So, we don't add it to the *path operation function* but to the *path operation decorator* in the `dependencies` parameter:
```Python hl_lines="25 33 40 52 58 65"
{!./src/sql_databases_peewee/sql_app/main.py!}
```
### Create your **FastAPI** *path operations*
Now, finally, here's the standard **FastAPI** *path operations* code.
```Python hl_lines="25 26 27 28 29 30 33 34 35 36 39 40 41 42 43 44 45 46 49 50 51 52 53 54 55 58 59 60 61 64 65 66 67 68 69 70"
{!./src/sql_databases_peewee/sql_app/main.py!}
```
### About `def` vs `async def`
The same as with SQLAlchemy, we are not doing something like:
```Python
user = await models.User.select().first()
```
...but instead we are using:
```Python
user = models.User.select().first()
```
So, again, we should declare the *path operation functions* and the dependency without `async def`, just with a normal `def`, as:
```Python hl_lines="2"
# Something goes here
def read_users(skip: int = 0, limit: int = 100):
# Something goes here
```
## Testing Peewee with async
This example includes an extra *path operation* that simulates a long processing request with `time.sleep(15)`.
It will have the database connection open at the beginning and will just wait 15 seconds before replying back.
This will easily let you test that your app with Peewee and FastAPI is behaving correctly with all the stuff about threads.
If you want to check how Peewee would break your app if used without modification, go the the `sql_app/database.py` file and comment the line:
```Python
# db._state = PeeweeConnectionState()
```
Then run your app with Uvicorn:
```bash
uvicorn sql_app.main:app --reload
```
Open your browser at <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a> and create a couple of users.
Then open 10 tabs at <a href="http://127.0.0.1:8000/docs#/default/read_slow_users_slowusers__get" target="_blank">http://127.0.0.1:8000/docs#/default/read_slow_users_slowusers__get</a> at the same time.
Go to the *path operation* "Get `/slowusers/`" in all of the tabs. Use the "Try it out" button and execute the request in each tab, one right after the other.
The tabs will wait for a bit and then some of them will show `Internal Server Error`.
### What happens
The first tab will make your app create a connection to the database and wait for 15 seconds before replying back and closing the connection.
Then one of the other tabs will try to open a database connection, but as one of those requests for the other tabs will probably be handled in the same thread as the first one, it will have the same database connection that is already open, and Peewee will throw an error and you will see it in the terminal, and the response will have an `Internal Server Error`.
This will probably happen for more than one of those tabs.
If you had multiple clients talking to your app exactly at the same time, this is what could happen.
And as your app starts to handle more and more clients at the same time, the waiting time in a single requests needs to be shorter and shorter to trigger the error.
### Fix Peewee with FastAPI
Now go back to the file `sql_app/database.py`, and uncomment the line:
```Python
db._state = PeeweeConnectionState()
```
Terminate your running app and start it again.
Repeat the same process with the 10 tabs. This time all of them will wait and you will get all the results without errors.
...You fixed it!
## 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/__init__.py`: is an empty file.
* `sql_app/database.py`:
```Python hl_lines=""
{!./src/sql_databases_peewee/sql_app/database.py!}
```
* `sql_app/models.py`:
```Python hl_lines=""
{!./src/sql_databases_peewee/sql_app/models.py!}
```
* `sql_app/schemas.py`:
```Python hl_lines=""
{!./src/sql_databases_peewee/sql_app/schemas.py!}
```
* `sql_app/crud.py`:
```Python hl_lines=""
{!./src/sql_databases_peewee/sql_app/crud.py!}
```
* `sql_app/main.py`:
```Python hl_lines=""
{!./src/sql_databases_peewee/sql_app/main.py!}
```
## Technical Details
If you want to go deeper into the technical details related to Peewee with FastAPI, you can <a href="https://github.com/coleifer/peewee/pull/2072" target="_blank">read more about it here</a>.

View File

@@ -55,20 +55,24 @@ Common ORMs are for example: Django-ORM (part of the Django framework), SQLAlche
Here we will see how to work with **SQLAlchemy ORM**.
The same way, you could use Peewee or any other.
In a similar way you could use any other ORM.
!!! tip
There's an equivalent article using Peewee here in the docs.
## File structure
For these examples, let's say you have a directory named `my_super_project` that contains a sub-directory called `sql_app` with a structure like this:
```
├── sql_app
│   ├── __init__.py
│   ├── crud.py
│   ├── database.py
│   ├── main.py
│   ├── models.py
│   ├── schemas.py
.
└── sql_app
├── __init__.py
├── crud.py
├── database.py
├── main.py
├── models.py
└── schemas.py
```
The file `__init__.py` is just an empty file, but it tells Python that `sql_app` with all its modules (Python files) is a package.
@@ -131,12 +135,17 @@ connect_args={"check_same_thread": False}
!!! info "Technical Details"
That argument `check_same_thread` is there mainly to be able to run the tests that cover this example.
By default SQLite will only allow one thread to communicate with it, assuming that each thread would handle an independent request.
This is to prevent accidentally sharing the same connection for different things (for different requests).
But in FastAPI, using normal functions (`def`) more than one thread could interact with the database for the same request, so we need to make SQLite know that it should allow that with `connect_args={"check_same_thread": False}`.
Also, we will make sure each request gets its own database connection session in a dependency, so there's no need for that default mechanism.
### Create a `SessionLocal` class
Each instance of the `SessionLocal` class will be a database session. The class itself is not a database session yet.
Each instance of the `SessionLocal` class will be a database session. The class itself is not a database session yet.
But once we create an instance of the `SessionLocal` class, this instance will be the actual database session.
@@ -413,9 +422,9 @@ And now in the file `sql_app/main.py` let's integrate and use all the other part
### Create the database tables
In a very simplistic way, create the database tables:
In a very simplistic way create the database tables:
```Python hl_lines="11"
```Python hl_lines="9"
{!./src/sql_databases/sql_app/main.py!}
```

View File

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

View File

@@ -303,6 +303,7 @@ class FastAPI(Starlette):
include_in_schema: bool = True,
response_class: Type[Response] = None,
name: str = None,
callbacks: List[routing.APIRoute] = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
@@ -327,6 +328,7 @@ class FastAPI(Starlette):
include_in_schema=include_in_schema,
response_class=response_class or self.default_response_class,
name=name,
callbacks=callbacks,
)
def put(
@@ -351,6 +353,7 @@ class FastAPI(Starlette):
include_in_schema: bool = True,
response_class: Type[Response] = None,
name: str = None,
callbacks: List[routing.APIRoute] = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
@@ -375,6 +378,7 @@ class FastAPI(Starlette):
include_in_schema=include_in_schema,
response_class=response_class or self.default_response_class,
name=name,
callbacks=callbacks,
)
def post(
@@ -399,6 +403,7 @@ class FastAPI(Starlette):
include_in_schema: bool = True,
response_class: Type[Response] = None,
name: str = None,
callbacks: List[routing.APIRoute] = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
@@ -423,6 +428,7 @@ class FastAPI(Starlette):
include_in_schema=include_in_schema,
response_class=response_class or self.default_response_class,
name=name,
callbacks=callbacks,
)
def delete(
@@ -447,6 +453,7 @@ class FastAPI(Starlette):
include_in_schema: bool = True,
response_class: Type[Response] = None,
name: str = None,
callbacks: List[routing.APIRoute] = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
@@ -471,6 +478,7 @@ class FastAPI(Starlette):
include_in_schema=include_in_schema,
response_class=response_class or self.default_response_class,
name=name,
callbacks=callbacks,
)
def options(
@@ -495,6 +503,7 @@ class FastAPI(Starlette):
include_in_schema: bool = True,
response_class: Type[Response] = None,
name: str = None,
callbacks: List[routing.APIRoute] = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
@@ -519,6 +528,7 @@ class FastAPI(Starlette):
include_in_schema=include_in_schema,
response_class=response_class or self.default_response_class,
name=name,
callbacks=callbacks,
)
def head(
@@ -543,6 +553,7 @@ class FastAPI(Starlette):
include_in_schema: bool = True,
response_class: Type[Response] = None,
name: str = None,
callbacks: List[routing.APIRoute] = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
@@ -567,6 +578,7 @@ class FastAPI(Starlette):
include_in_schema=include_in_schema,
response_class=response_class or self.default_response_class,
name=name,
callbacks=callbacks,
)
def patch(
@@ -591,6 +603,7 @@ class FastAPI(Starlette):
include_in_schema: bool = True,
response_class: Type[Response] = None,
name: str = None,
callbacks: List[routing.APIRoute] = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
@@ -615,6 +628,7 @@ class FastAPI(Starlette):
include_in_schema=include_in_schema,
response_class=response_class or self.default_response_class,
name=name,
callbacks=callbacks,
)
def trace(
@@ -639,6 +653,7 @@ class FastAPI(Starlette):
include_in_schema: bool = True,
response_class: Type[Response] = None,
name: str = None,
callbacks: List[routing.APIRoute] = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
@@ -663,4 +678,5 @@ class FastAPI(Starlette):
include_in_schema=include_in_schema,
response_class=response_class or self.default_response_class,
name=name,
callbacks=callbacks,
)

View File

@@ -1,8 +1,9 @@
from enum import Enum
from types import GeneratorType
from typing import Any, Dict, List, Set, Union
from typing import Any, Callable, Dict, List, Set, Tuple, Union
from fastapi.utils import PYDANTIC_1, logger
from fastapi.logger import logger
from fastapi.utils import PYDANTIC_1
from pydantic import BaseModel
from pydantic.json import ENCODERS_BY_TYPE
@@ -10,6 +11,21 @@ SetIntStr = Set[Union[int, str]]
DictIntStrAny = Dict[Union[int, str], Any]
def generate_encoders_by_class_tuples(
type_encoder_map: Dict[Any, Callable]
) -> Dict[Callable, Tuple]:
encoders_by_classes: Dict[Callable, List] = {}
for type_, encoder in type_encoder_map.items():
encoders_by_classes.setdefault(encoder, []).append(type_)
encoders_by_class_tuples: Dict[Callable, Tuple] = {}
for encoder, classes in encoders_by_classes.items():
encoders_by_class_tuples[encoder] = tuple(classes)
return encoders_by_class_tuples
encoders_by_class_tuples = generate_encoders_by_class_tuples(ENCODERS_BY_TYPE)
def jsonable_encoder(
obj: Any,
include: Union[SetIntStr, DictIntStrAny] = None,
@@ -23,9 +39,9 @@ def jsonable_encoder(
) -> Any:
if skip_defaults is not None:
logger.warning( # pragma: nocover
"skip_defaults in jsonable_encoder has been deprecated in \
favor of exclude_unset to keep in line with Pydantic v1, support for it \
will be removed soon."
"skip_defaults in jsonable_encoder has been deprecated in favor of "
"exclude_unset to keep in line with Pydantic v1, support for it will be "
"removed soon."
)
if include is not None and not isinstance(include, set):
include = set(include)
@@ -105,24 +121,31 @@ def jsonable_encoder(
)
)
return encoded_list
if custom_encoder:
if type(obj) in custom_encoder:
return custom_encoder[type(obj)](obj)
else:
for encoder_type, encoder in custom_encoder.items():
if isinstance(obj, encoder_type):
return encoder(obj)
if type(obj) in ENCODERS_BY_TYPE:
return ENCODERS_BY_TYPE[type(obj)](obj)
for encoder, classes_tuple in encoders_by_class_tuples.items():
if isinstance(obj, classes_tuple):
return encoder(obj)
errors: List[Exception] = []
try:
if custom_encoder and type(obj) in custom_encoder:
encoder = custom_encoder[type(obj)]
else:
encoder = ENCODERS_BY_TYPE[type(obj)]
return encoder(obj)
except KeyError as e:
data = dict(obj)
except Exception as e:
errors.append(e)
try:
data = dict(obj)
data = vars(obj)
except Exception as e:
errors.append(e)
try:
data = vars(obj)
except Exception as e:
errors.append(e)
raise ValueError(errors)
raise ValueError(errors)
return jsonable_encoder(
data,
by_alias=by_alias,

3
fastapi/logger.py Normal file
View File

@@ -0,0 +1,3 @@
import logging
logger = logging.getLogger("fastapi")

View File

@@ -1,7 +1,7 @@
from enum import Enum
from typing import Any, Dict, List, Optional, Union
from fastapi.utils import logger
from fastapi.logger import logger
from pydantic import BaseModel
try:
@@ -21,9 +21,9 @@ try:
# TODO: remove when removing support for Pydantic < 1.0.0
from pydantic.types import EmailStr # type: ignore
except ImportError: # pragma: no cover
logger.warning(
logger.info(
"email-validator not installed, email fields will be treated as str.\n"
+ "To install, run: pip install email-validator"
"To install, run: pip install email-validator"
)
class EmailStr(str): # type: ignore

View File

@@ -187,6 +187,14 @@ def get_openapi_path(
)
if request_body_oai:
operation["requestBody"] = request_body_oai
if route.callbacks:
callbacks = {}
for callback in route.callbacks:
cb_path, cb_security_schemes, cb_definitions, = get_openapi_path(
route=callback, model_name_map=model_name_map
)
callbacks[callback.name] = {callback.path: cb_path}
operation["callbacks"] = callbacks
if route.responses:
for (additional_status_code, response) in route.responses.items():
assert isinstance(

View File

@@ -1,6 +1,5 @@
import asyncio
import inspect
import logging
from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Type, Union
from fastapi import params
@@ -13,6 +12,7 @@ from fastapi.dependencies.utils import (
)
from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
from fastapi.logger import logger
from fastapi.openapi.constants import STATUS_CODES_WITH_NO_BODY
from fastapi.utils import (
PYDANTIC_1,
@@ -108,7 +108,7 @@ def get_request_handler(
if body_bytes:
body = await request.json()
except Exception as e:
logging.error(f"Error getting request body: {e}")
logger.error(f"Error getting request body: {e}")
raise HTTPException(
status_code=400, detail="There was an error parsing the body"
) from e
@@ -218,6 +218,7 @@ class APIRoute(routing.Route):
include_in_schema: bool = True,
response_class: Optional[Type[Response]] = None,
dependency_overrides_provider: Any = None,
callbacks: Optional[List["APIRoute"]] = None,
) -> None:
self.path = path
self.endpoint = endpoint
@@ -338,6 +339,7 @@ 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.callbacks = callbacks
self.app = request_response(self.get_route_handler())
def get_route_handler(self) -> Callable:
@@ -363,12 +365,14 @@ class APIRouter(routing.Router):
default: ASGIApp = None,
dependency_overrides_provider: Any = None,
route_class: Type[APIRoute] = APIRoute,
default_response_class: Type[Response] = None,
) -> None:
super().__init__(
routes=routes, redirect_slashes=redirect_slashes, default=default
)
self.dependency_overrides_provider = dependency_overrides_provider
self.route_class = route_class
self.default_response_class = default_response_class
def add_api_route(
self,
@@ -395,6 +399,7 @@ class APIRouter(routing.Router):
response_class: Type[Response] = None,
name: str = None,
route_class_override: Optional[Type[APIRoute]] = None,
callbacks: List[APIRoute] = None,
) -> None:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
@@ -420,9 +425,10 @@ class APIRouter(routing.Router):
response_model_exclude_unset or 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,
dependency_overrides_provider=self.dependency_overrides_provider,
callbacks=callbacks,
)
self.routes.append(route)
@@ -449,6 +455,7 @@ class APIRouter(routing.Router):
include_in_schema: bool = True,
response_class: Type[Response] = None,
name: str = None,
callbacks: List[APIRoute] = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
@@ -475,8 +482,9 @@ class APIRouter(routing.Router):
response_model_exclude_unset or 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,
callbacks=callbacks,
)
return func
@@ -546,6 +554,7 @@ class APIRouter(routing.Router):
response_class=route.response_class or default_response_class,
name=route.name,
route_class_override=type(route),
callbacks=route.callbacks,
)
elif isinstance(route, routing.Route):
self.add_route(
@@ -586,6 +595,7 @@ class APIRouter(routing.Router):
include_in_schema: bool = True,
response_class: Type[Response] = None,
name: str = None,
callbacks: List[APIRoute] = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
@@ -609,8 +619,9 @@ class APIRouter(routing.Router):
response_model_exclude_unset or 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,
callbacks=callbacks,
)
def put(
@@ -635,6 +646,7 @@ class APIRouter(routing.Router):
include_in_schema: bool = True,
response_class: Type[Response] = None,
name: str = None,
callbacks: List[APIRoute] = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
@@ -658,8 +670,9 @@ class APIRouter(routing.Router):
response_model_exclude_unset or 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,
callbacks=callbacks,
)
def post(
@@ -684,6 +697,7 @@ class APIRouter(routing.Router):
include_in_schema: bool = True,
response_class: Type[Response] = None,
name: str = None,
callbacks: List[APIRoute] = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
@@ -707,8 +721,9 @@ class APIRouter(routing.Router):
response_model_exclude_unset or 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,
callbacks=callbacks,
)
def delete(
@@ -733,6 +748,7 @@ class APIRouter(routing.Router):
include_in_schema: bool = True,
response_class: Type[Response] = None,
name: str = None,
callbacks: List[APIRoute] = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
@@ -756,8 +772,9 @@ class APIRouter(routing.Router):
response_model_exclude_unset or 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,
callbacks=callbacks,
)
def options(
@@ -782,6 +799,7 @@ class APIRouter(routing.Router):
include_in_schema: bool = True,
response_class: Type[Response] = None,
name: str = None,
callbacks: List[APIRoute] = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
@@ -805,8 +823,9 @@ class APIRouter(routing.Router):
response_model_exclude_unset or 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,
callbacks=callbacks,
)
def head(
@@ -831,6 +850,7 @@ class APIRouter(routing.Router):
include_in_schema: bool = True,
response_class: Type[Response] = None,
name: str = None,
callbacks: List[APIRoute] = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
@@ -854,8 +874,9 @@ class APIRouter(routing.Router):
response_model_exclude_unset or 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,
callbacks=callbacks,
)
def patch(
@@ -880,6 +901,7 @@ class APIRouter(routing.Router):
include_in_schema: bool = True,
response_class: Type[Response] = None,
name: str = None,
callbacks: List[APIRoute] = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
@@ -903,8 +925,9 @@ class APIRouter(routing.Router):
response_model_exclude_unset or 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,
callbacks=callbacks,
)
def trace(
@@ -929,6 +952,7 @@ class APIRouter(routing.Router):
include_in_schema: bool = True,
response_class: Type[Response] = None,
name: str = None,
callbacks: List[APIRoute] = None,
) -> Callable:
if response_model_skip_defaults is not None:
warning_response_model_skip_defaults_deprecated() # pragma: nocover
@@ -952,6 +976,7 @@ class APIRouter(routing.Router):
response_model_exclude_unset or 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,
callbacks=callbacks,
)

View File

@@ -8,6 +8,7 @@ from .http import (
)
from .oauth2 import (
OAuth2,
OAuth2AuthorizationCodeBearer,
OAuth2PasswordBearer,
OAuth2PasswordRequestForm,
SecurityScopes,

View File

@@ -163,6 +163,43 @@ class OAuth2PasswordBearer(OAuth2):
return param
class OAuth2AuthorizationCodeBearer(OAuth2):
def __init__(
self,
authorizationUrl: str,
tokenUrl: str,
refreshUrl: str = None,
scheme_name: str = None,
scopes: dict = None,
auto_error: bool = True,
):
if not scopes:
scopes = {}
flows = OAuthFlowsModel(
authorizationCode={
"authorizationUrl": authorizationUrl,
"tokenUrl": tokenUrl,
"refreshUrl": refreshUrl,
"scopes": scopes,
}
)
super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)
async def __call__(self, request: Request) -> Optional[str]:
authorization: str = request.headers.get("Authorization")
scheme, param = get_authorization_scheme_param(authorization)
if not authorization or scheme.lower() != "bearer":
if self.auto_error:
raise HTTPException(
status_code=HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
else:
return None # pragma: nocover
return param
class SecurityScopes:
def __init__(self, scopes: List[str] = None):
self.scopes = scopes or []

View File

@@ -1,17 +1,15 @@
import logging
import re
from dataclasses import is_dataclass
from typing import Any, Dict, List, Sequence, Set, Type, cast
from fastapi import routing
from fastapi.logger import logger
from fastapi.openapi.constants import REF_PREFIX
from pydantic import BaseConfig, BaseModel, create_model
from pydantic.schema import get_flat_models_from_fields, model_process_schema
from pydantic.utils import lenient_issubclass
from starlette.routing import BaseRoute
logger = logging.getLogger("fastapi")
try:
from pydantic.fields import FieldInfo, ModelField
@@ -22,8 +20,8 @@ except ImportError: # pragma: nocover
from pydantic import Schema as FieldInfo # type: ignore
logger.warning(
"Pydantic versions < 1.0.0 are deprecated in FastAPI and support will be \
removed soon"
"Pydantic versions < 1.0.0 are deprecated in FastAPI and support will be "
"removed soon."
)
PYDANTIC_1 = False
@@ -39,15 +37,16 @@ def get_field_info(field: ModelField) -> FieldInfo:
# TODO: remove when removing support for Pydantic < 1.0.0
def warning_response_model_skip_defaults_deprecated() -> None:
logger.warning( # pragma: nocover
"response_model_skip_defaults has been deprecated in favor \
of response_model_exclude_unset to keep in line with Pydantic v1, \
support for it will be removed soon."
"response_model_skip_defaults has been deprecated in favor of "
"response_model_exclude_unset to keep in line with Pydantic v1, support for "
"it will be removed soon."
)
def get_flat_models_from_routes(routes: Sequence[BaseRoute]) -> Set[Type[BaseModel]]:
body_fields_from_routes: List[ModelField] = []
responses_from_routes: List[ModelField] = []
callback_flat_models: Set[Type[BaseModel]] = set()
for route in routes:
if getattr(route, "include_in_schema", None) and isinstance(
route, routing.APIRoute
@@ -61,7 +60,9 @@ def get_flat_models_from_routes(routes: Sequence[BaseRoute]) -> Set[Type[BaseMod
responses_from_routes.append(route.response_field)
if route.response_fields:
responses_from_routes.extend(route.response_fields.values())
flat_models = get_flat_models_from_fields(
if route.callbacks:
callback_flat_models |= get_flat_models_from_routes(route.callbacks)
flat_models = callback_flat_models | get_flat_models_from_fields(
body_fields_from_routes + responses_from_routes, known_models=set()
)
return flat_models
@@ -155,6 +156,6 @@ def create_cloned_field(field: ModelField) -> ModelField:
def generate_operation_id_for_path(*, name: str, path: str, method: str) -> str:
operation_id = name + path
operation_id = operation_id.replace("{", "_").replace("}", "_").replace("/", "_")
operation_id = re.sub("[^0-9a-zA-Z_]", "_", operation_id)
operation_id = operation_id + "_" + method.lower()
return operation_id

View File

@@ -5,8 +5,8 @@ site_url: https://fastapi.tiangolo.com/
theme:
name: 'material'
palette:
primary: 'teal'
accent: 'amber'
primary: 'teal'
accent: 'amber'
logo: 'img/icon-white.svg'
favicon: 'img/favicon.png'
@@ -30,7 +30,7 @@ nav:
- Query Parameters and String Validations: 'tutorial/query-params-str-validations.md'
- Path Parameters and Numeric Validations: 'tutorial/path-params-numeric-validations.md'
- Body - Multiple Parameters: 'tutorial/body-multiple-params.md'
- Body - Schema: 'tutorial/body-schema.md'
- Body - Fields: 'tutorial/body-fields.md'
- Body - Nested Models: 'tutorial/body-nested-models.md'
- Extra data types: 'tutorial/extra-data-types.md'
- Cookie Parameters: 'tutorial/cookie-params.md'
@@ -72,6 +72,7 @@ nav:
- CORS (Cross-Origin Resource Sharing): 'tutorial/cors.md'
- Using the Request Directly: 'tutorial/using-request-directly.md'
- SQL (Relational) Databases: 'tutorial/sql-databases.md'
- SQL (Relational) Databases with Peewee: 'tutorial/sql-databases-peewee.md'
- Async SQL (Relational) Databases: 'tutorial/async-sql-databases.md'
- NoSQL (Distributed / Big Data) Databases: 'tutorial/nosql-databases.md'
- Bigger Applications - Multiple Files: 'tutorial/bigger-applications.md'
@@ -88,6 +89,7 @@ nav:
- Testing Dependencies with Overrides: 'tutorial/testing-dependencies.md'
- Debugging: 'tutorial/debugging.md'
- Extending OpenAPI: 'tutorial/extending-openapi.md'
- OpenAPI Callbacks: 'tutorial/openapi-callbacks.md'
- Concurrency and async / await: 'async.md'
- Deployment: 'deployment.md'
- Project Generation - Template: 'project-generation.md'
@@ -100,10 +102,27 @@ nav:
- Release Notes: release-notes.md
markdown_extensions:
- markdown.extensions.codehilite:
guess_lang: false
- markdown_include.include:
base_path: docs
- admonition
- codehilite
- extra
- toc:
permalink: true
- markdown.extensions.codehilite:
guess_lang: false
- markdown_include.include:
base_path: docs
- admonition
- codehilite
- extra
extra:
social:
- type: 'github'
link: 'https://github.com/tiangolo/typer'
- type: 'twitter'
link: 'https://twitter.com/tiangolo'
- type: 'linkedin'
link: 'https://www.linkedin.com/in/tiangolo'
- type: 'rss'
link: 'https://dev.to/tiangolo'
- type: 'medium'
link: 'https://medium.com/@tiangolo'
- type: 'globe'
link: 'https://tiangolo.com'

View File

@@ -8,17 +8,17 @@ author = "Sebastián Ramírez"
author-email = "tiangolo@gmail.com"
home-page = "https://github.com/tiangolo/fastapi"
classifiers = [
'Intended Audience :: Information Technology',
'Intended Audience :: System Administrators',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3',
'Programming Language :: Python',
'Topic :: Internet',
'Topic :: Software Development :: Libraries :: Application Frameworks',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Software Development :: Libraries',
'Topic :: Software Development',
'Typing :: Typed',
"Intended Audience :: Information Technology",
"Intended Audience :: System Administrators",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python",
"Topic :: Internet",
"Topic :: Software Development :: Libraries :: Application Frameworks",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Software Development :: Libraries",
"Topic :: Software Development",
"Typing :: Typed",
"Development Status :: 4 - Beta",
"Environment :: Web Environment",
"Framework :: AsyncIO",
@@ -51,6 +51,7 @@ test = [
"requests",
"email_validator",
"sqlalchemy",
"peewee",
"databases[sqlite]",
"orjson",
"async_exit_stack",

View File

@@ -11,3 +11,5 @@ fi
export PYTHONPATH=./docs/src
pytest --cov=fastapi --cov=tests --cov=docs/src --cov-report=term-missing ${@}
bash ./scripts/lint.sh
# Check README.md is up to date
diff --brief docs/index.md README.md

View File

@@ -244,7 +244,7 @@ openapi_schema = {
},
},
"summary": "Get Path Param Required Id",
"operationId": "get_path_param_required_id_path_param-required__item_id__get",
"operationId": "get_path_param_required_id_path_param_required__item_id__get",
"parameters": [
{
"required": True,
@@ -274,7 +274,7 @@ openapi_schema = {
},
},
"summary": "Get Path Param Min Length",
"operationId": "get_path_param_min_length_path_param-minlength__item_id__get",
"operationId": "get_path_param_min_length_path_param_minlength__item_id__get",
"parameters": [
{
"required": True,
@@ -308,7 +308,7 @@ openapi_schema = {
},
},
"summary": "Get Path Param Max Length",
"operationId": "get_path_param_max_length_path_param-maxlength__item_id__get",
"operationId": "get_path_param_max_length_path_param_maxlength__item_id__get",
"parameters": [
{
"required": True,
@@ -342,7 +342,7 @@ openapi_schema = {
},
},
"summary": "Get Path Param Min Max Length",
"operationId": "get_path_param_min_max_length_path_param-min_maxlength__item_id__get",
"operationId": "get_path_param_min_max_length_path_param_min_maxlength__item_id__get",
"parameters": [
{
"required": True,
@@ -377,7 +377,7 @@ openapi_schema = {
},
},
"summary": "Get Path Param Gt",
"operationId": "get_path_param_gt_path_param-gt__item_id__get",
"operationId": "get_path_param_gt_path_param_gt__item_id__get",
"parameters": [
{
"required": True,
@@ -411,7 +411,7 @@ openapi_schema = {
},
},
"summary": "Get Path Param Gt0",
"operationId": "get_path_param_gt0_path_param-gt0__item_id__get",
"operationId": "get_path_param_gt0_path_param_gt0__item_id__get",
"parameters": [
{
"required": True,
@@ -445,7 +445,7 @@ openapi_schema = {
},
},
"summary": "Get Path Param Ge",
"operationId": "get_path_param_ge_path_param-ge__item_id__get",
"operationId": "get_path_param_ge_path_param_ge__item_id__get",
"parameters": [
{
"required": True,
@@ -479,7 +479,7 @@ openapi_schema = {
},
},
"summary": "Get Path Param Lt",
"operationId": "get_path_param_lt_path_param-lt__item_id__get",
"operationId": "get_path_param_lt_path_param_lt__item_id__get",
"parameters": [
{
"required": True,
@@ -513,7 +513,7 @@ openapi_schema = {
},
},
"summary": "Get Path Param Lt0",
"operationId": "get_path_param_lt0_path_param-lt0__item_id__get",
"operationId": "get_path_param_lt0_path_param_lt0__item_id__get",
"parameters": [
{
"required": True,
@@ -547,7 +547,7 @@ openapi_schema = {
},
},
"summary": "Get Path Param Le",
"operationId": "get_path_param_le_path_param-le__item_id__get",
"operationId": "get_path_param_le_path_param_le__item_id__get",
"parameters": [
{
"required": True,
@@ -581,7 +581,7 @@ openapi_schema = {
},
},
"summary": "Get Path Param Lt Gt",
"operationId": "get_path_param_lt_gt_path_param-lt-gt__item_id__get",
"operationId": "get_path_param_lt_gt_path_param_lt_gt__item_id__get",
"parameters": [
{
"required": True,
@@ -616,7 +616,7 @@ openapi_schema = {
},
},
"summary": "Get Path Param Le Ge",
"operationId": "get_path_param_le_ge_path_param-le-ge__item_id__get",
"operationId": "get_path_param_le_ge_path_param_le_ge__item_id__get",
"parameters": [
{
"required": True,
@@ -651,7 +651,7 @@ openapi_schema = {
},
},
"summary": "Get Path Param Lt Int",
"operationId": "get_path_param_lt_int_path_param-lt-int__item_id__get",
"operationId": "get_path_param_lt_int_path_param_lt_int__item_id__get",
"parameters": [
{
"required": True,
@@ -685,7 +685,7 @@ openapi_schema = {
},
},
"summary": "Get Path Param Gt Int",
"operationId": "get_path_param_gt_int_path_param-gt-int__item_id__get",
"operationId": "get_path_param_gt_int_path_param_gt_int__item_id__get",
"parameters": [
{
"required": True,
@@ -719,7 +719,7 @@ openapi_schema = {
},
},
"summary": "Get Path Param Le Int",
"operationId": "get_path_param_le_int_path_param-le-int__item_id__get",
"operationId": "get_path_param_le_int_path_param_le_int__item_id__get",
"parameters": [
{
"required": True,
@@ -753,7 +753,7 @@ openapi_schema = {
},
},
"summary": "Get Path Param Ge Int",
"operationId": "get_path_param_ge_int_path_param-ge-int__item_id__get",
"operationId": "get_path_param_ge_int_path_param_ge_int__item_id__get",
"parameters": [
{
"required": True,
@@ -787,7 +787,7 @@ openapi_schema = {
},
},
"summary": "Get Path Param Lt Gt Int",
"operationId": "get_path_param_lt_gt_int_path_param-lt-gt-int__item_id__get",
"operationId": "get_path_param_lt_gt_int_path_param_lt_gt_int__item_id__get",
"parameters": [
{
"required": True,
@@ -822,7 +822,7 @@ openapi_schema = {
},
},
"summary": "Get Path Param Le Ge Int",
"operationId": "get_path_param_le_ge_int_path_param-le-ge-int__item_id__get",
"operationId": "get_path_param_le_ge_int_path_param_le_ge_int__item_id__get",
"parameters": [
{
"required": True,
@@ -1037,7 +1037,7 @@ openapi_schema = {
},
},
"summary": "Get Query Param Required",
"operationId": "get_query_param_required_query_param-required_get",
"operationId": "get_query_param_required_query_param_required_get",
"parameters": [
{
"required": True,
@@ -1067,7 +1067,7 @@ openapi_schema = {
},
},
"summary": "Get Query Param Required Type",
"operationId": "get_query_param_required_type_query_param-required_int_get",
"operationId": "get_query_param_required_type_query_param_required_int_get",
"parameters": [
{
"required": True,

View File

@@ -259,7 +259,7 @@ openapi_schema = {
},
},
"summary": "Get Not Decorated",
"operationId": "get_not_decorated_items-not-decorated__item_id__get",
"operationId": "get_not_decorated_items_not_decorated__item_id__get",
"parameters": [
{
"required": True,

View File

@@ -0,0 +1,73 @@
import uuid
import pytest
from fastapi import FastAPI
from pydantic import BaseModel
from starlette.testclient import TestClient
app = FastAPI()
class MyUuid:
def __init__(self, uuid_string: str):
self.uuid = uuid_string
def __str__(self):
return self.uuid
@property
def __class__(self):
return uuid.UUID
@property
def __dict__(self):
"""Spoof a missing __dict__ by raising TypeError, this is how
asyncpg.pgroto.pgproto.UUID behaves"""
raise TypeError("vars() argument must have __dict__ attribute")
@app.get("/fast_uuid")
def return_fast_uuid():
# I don't want to import asyncpg for this test so I made my own UUID
# Import asyncpg and uncomment the two lines below for the actual bug
# from asyncpg.pgproto import pgproto
# asyncpg_uuid = pgproto.UUID("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51")
assert isinstance(asyncpg_uuid, uuid.UUID)
assert type(asyncpg_uuid) != uuid.UUID
with pytest.raises(TypeError):
vars(asyncpg_uuid)
return {"fast_uuid": asyncpg_uuid}
class SomeCustomClass(BaseModel):
class Config:
arbitrary_types_allowed = True
json_encoders = {uuid.UUID: str}
a_uuid: MyUuid
@app.get("/get_custom_class")
def return_some_user():
# Test that the fix also works for custom pydantic classes
return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01"))
client = TestClient(app)
def test_dt():
with client:
response_simple = client.get("/fast_uuid")
response_pydantic = client.get("/get_custom_class")
assert response_simple.json() == {
"fast_uuid": "a10ff360-3b1e-4984-a26f-d3ab460bdb51"
}
assert response_pydantic.json() == {
"a_uuid": "b8799909-f914-42de-91bc-95c819218d01"
}

View File

@@ -0,0 +1,77 @@
from typing import Optional
from fastapi import FastAPI, Security
from fastapi.security import OAuth2AuthorizationCodeBearer
from starlette.testclient import TestClient
app = FastAPI()
oauth2_scheme = OAuth2AuthorizationCodeBearer(
authorizationUrl="/authorize", tokenUrl="/token", auto_error=True
)
@app.get("/items/")
async def read_items(token: Optional[str] = Security(oauth2_scheme)):
return {"token": token}
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/items/": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
"summary": "Read Items",
"operationId": "read_items_items__get",
"security": [{"OAuth2AuthorizationCodeBearer": []}],
}
}
},
"components": {
"securitySchemes": {
"OAuth2AuthorizationCodeBearer": {
"type": "oauth2",
"flows": {
"authorizationCode": {
"authorizationUrl": "/authorize",
"tokenUrl": "/token",
"scopes": {},
}
},
}
}
},
}
def test_openapi_schema():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema
def test_no_token():
response = client.get("/items")
assert response.status_code == 401
assert response.json() == {"detail": "Not authenticated"}
def test_incorrect_token():
response = client.get("/items", headers={"Authorization": "Non-existent testtoken"})
assert response.status_code == 401
assert response.json() == {"detail": "Not authenticated"}
def test_token():
response = client.get("/items", headers={"Authorization": "Bearer testtoken"})
assert response.status_code == 200
assert response.json() == {"token": "testtoken"}

View File

@@ -80,7 +80,7 @@ openapi_schema = {
},
},
"summary": "Create Item",
"operationId": "create_item_starlette-items__item_id__get",
"operationId": "create_item_starlette_items__item_id__get",
"parameters": [
{
"required": True,

230
tests/test_sub_callbacks.py Normal file
View File

@@ -0,0 +1,230 @@
from fastapi import APIRouter, FastAPI
from pydantic import BaseModel, HttpUrl
from starlette.responses import JSONResponse
from starlette.testclient import TestClient
app = FastAPI()
class Invoice(BaseModel):
id: str
title: str = None
customer: str
total: float
class InvoiceEvent(BaseModel):
description: str
paid: bool
class InvoiceEventReceived(BaseModel):
ok: bool
invoices_callback_router = APIRouter(default_response_class=JSONResponse)
@invoices_callback_router.post(
"{$callback_url}/invoices/{$request.body.id}", response_model=InvoiceEventReceived,
)
def invoice_notification(body: InvoiceEvent):
pass
subrouter = APIRouter()
@subrouter.post("/invoices/", callbacks=invoices_callback_router.routes)
def create_invoice(invoice: Invoice, callback_url: HttpUrl = None):
"""
Create an invoice.
This will (let's imagine) let the API user (some external developer) create an
invoice.
And this path operation will:
* Send the invoice to the client.
* Collect the money from the client.
* Send a notification back to the API user (the external developer), as a callback.
* At this point is that the API will somehow send a POST request to the
external API with the notification of the invoice event
(e.g. "payment successful").
"""
# Send the invoice, collect the money, send the notification (the callback)
return {"msg": "Invoice received"}
app.include_router(subrouter)
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/invoices/": {
"post": {
"summary": "Create Invoice",
"description": 'Create an invoice.\n\nThis will (let\'s imagine) let the API user (some external developer) create an\ninvoice.\n\nAnd this path operation will:\n\n* Send the invoice to the client.\n* Collect the money from the client.\n* Send a notification back to the API user (the external developer), as a callback.\n * At this point is that the API will somehow send a POST request to the\n external API with the notification of the invoice event\n (e.g. "payment successful").',
"operationId": "create_invoice_invoices__post",
"parameters": [
{
"required": False,
"schema": {
"title": "Callback Url",
"maxLength": 2083,
"minLength": 1,
"type": "string",
"format": "uri",
},
"name": "callback_url",
"in": "query",
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Invoice"}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"callbacks": {
"invoice_notification": {
"{$callback_url}/invoices/{$request.body.id}": {
"post": {
"summary": "Invoice Notification",
"operationId": "invoice_notification__callback_url__invoices___request_body_id__post",
"requestBody": {
"required": True,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvoiceEvent"
}
}
},
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvoiceEventReceived"
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
}
}
},
}
}
},
"components": {
"schemas": {
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
"Invoice": {
"title": "Invoice",
"required": ["id", "customer", "total"],
"type": "object",
"properties": {
"id": {"title": "Id", "type": "string"},
"title": {"title": "Title", "type": "string"},
"customer": {"title": "Customer", "type": "string"},
"total": {"title": "Total", "type": "number"},
},
},
"InvoiceEvent": {
"title": "InvoiceEvent",
"required": ["description", "paid"],
"type": "object",
"properties": {
"description": {"title": "Description", "type": "string"},
"paid": {"title": "Paid", "type": "boolean"},
},
},
"InvoiceEventReceived": {
"title": "InvoiceEventReceived",
"required": ["ok"],
"type": "object",
"properties": {"ok": {"title": "Ok", "type": "boolean"}},
},
"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"},
},
},
}
},
}
def test_openapi():
with client:
response = client.get("/openapi.json")
assert response.json() == openapi_schema
def test_get():
response = client.post(
"/invoices/", json={"id": "fooinvoice", "customer": "John", "total": 5.3}
)
assert response.status_code == 200
assert response.json() == {"msg": "Invoice received"}
def test_dummy_callback():
# Just for coverage
invoice_notification({})

View File

View File

@@ -1,7 +1,7 @@
import pytest
from starlette.testclient import TestClient
from body_schema.tutorial001 import app
from body_fields.tutorial001 import app
# TODO: remove when removing support for Pydantic < 1.0.0
try:

View File

@@ -27,7 +27,7 @@ openapi_schema = {
},
},
"summary": "Create Index Weights",
"operationId": "create_index_weights_index-weights__post",
"operationId": "create_index_weights_index_weights__post",
"requestBody": {
"content": {
"application/json": {

View File

@@ -16,7 +16,7 @@ openapi_schema = {
"content": {
"application/json": {
"schema": {
"title": "Response Read Keyword Weights Keyword-Weights Get",
"title": "Response Read Keyword Weights Keyword Weights Get",
"type": "object",
"additionalProperties": {"type": "number"},
}
@@ -25,7 +25,7 @@ openapi_schema = {
}
},
"summary": "Read Keyword Weights",
"operationId": "read_keyword_weights_keyword-weights__get",
"operationId": "read_keyword_weights_keyword_weights__get",
}
}
},

View File

@@ -27,7 +27,7 @@ openapi_schema = {
},
},
"summary": "Read Item Header",
"operationId": "read_item_header_items-header__item_id__get",
"operationId": "read_item_header_items_header__item_id__get",
"parameters": [
{
"required": True,

View File

View File

@@ -0,0 +1,174 @@
from starlette.testclient import TestClient
from openapi_callbacks.tutorial001 import app, invoice_notification
client = TestClient(app)
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/invoices/": {
"post": {
"summary": "Create Invoice",
"description": 'Create an invoice.\n\nThis will (let\'s imagine) let the API user (some external developer) create an\ninvoice.\n\nAnd this path operation will:\n\n* Send the invoice to the client.\n* Collect the money from the client.\n* Send a notification back to the API user (the external developer), as a callback.\n * At this point is that the API will somehow send a POST request to the\n external API with the notification of the invoice event\n (e.g. "payment successful").',
"operationId": "create_invoice_invoices__post",
"parameters": [
{
"required": False,
"schema": {
"title": "Callback Url",
"maxLength": 2083,
"minLength": 1,
"type": "string",
"format": "uri",
},
"name": "callback_url",
"in": "query",
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Invoice"}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
"callbacks": {
"invoice_notification": {
"{$callback_url}/invoices/{$request.body.id}": {
"post": {
"summary": "Invoice Notification",
"operationId": "invoice_notification__callback_url__invoices___request_body_id__post",
"requestBody": {
"required": True,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvoiceEvent"
}
}
},
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvoiceEventReceived"
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
}
}
},
}
}
},
"components": {
"schemas": {
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
"Invoice": {
"title": "Invoice",
"required": ["id", "customer", "total"],
"type": "object",
"properties": {
"id": {"title": "Id", "type": "string"},
"title": {"title": "Title", "type": "string"},
"customer": {"title": "Customer", "type": "string"},
"total": {"title": "Total", "type": "number"},
},
},
"InvoiceEvent": {
"title": "InvoiceEvent",
"required": ["description", "paid"],
"type": "object",
"properties": {
"description": {"title": "Description", "type": "string"},
"paid": {"title": "Paid", "type": "boolean"},
},
},
"InvoiceEventReceived": {
"title": "InvoiceEventReceived",
"required": ["ok"],
"type": "object",
"properties": {"ok": {"title": "Ok", "type": "boolean"}},
},
"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"},
},
},
}
},
}
def test_openapi():
with client:
response = client.get("/openapi.json")
assert response.json() == openapi_schema
def test_get():
response = client.post(
"/invoices/", json={"id": "fooinvoice", "customer": "John", "total": 5.3}
)
assert response.status_code == 200
assert response.json() == {"msg": "Invoice received"}
def test_dummy_callback():
# Just for coverage
invoice_notification({})

View File

View File

@@ -0,0 +1,430 @@
import time
from pathlib import Path
from unittest.mock import MagicMock
import pytest
from starlette.testclient import TestClient
from ...utils import skip_py36
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/users/": {
"get": {
"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",
},
],
"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"
}
}
},
},
},
},
"post": {
"summary": "Create User",
"operationId": "create_user_users__post",
"requestBody": {
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/UserCreate"}
}
},
"required": True,
},
"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"
}
}
},
},
},
},
},
"/users/{user_id}": {
"get": {
"summary": "Read User",
"operationId": "read_user_users__user_id__get",
"parameters": [
{
"required": True,
"schema": {"title": "User Id", "type": "integer"},
"name": "user_id",
"in": "path",
}
],
"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"
}
}
},
},
},
}
},
"/users/{user_id}/items/": {
"post": {
"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,
},
"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"
}
}
},
},
},
}
},
"/items/": {
"get": {
"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",
},
],
"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"
}
}
},
},
},
}
},
"/slowusers/": {
"get": {
"summary": "Read Slow Users",
"operationId": "read_slow_users_slowusers__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",
},
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"title": "Response Read Slow Users Slowusers Get",
"type": "array",
"items": {"$ref": "#/components/schemas/User"},
}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
},
},
"components": {
"schemas": {
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
"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"},
},
},
"ItemCreate": {
"title": "ItemCreate",
"required": ["title"],
"type": "object",
"properties": {
"title": {"title": "Title", "type": "string"},
"description": {"title": "Description", "type": "string"},
},
},
"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"},
},
},
}
},
}
@pytest.fixture(scope="module")
def client():
# Import while creating the client to create the DB after starting the test session
from sql_databases_peewee.sql_app.main import app
test_db = Path("./test.db")
with TestClient(app) as c:
yield c
test_db.unlink()
@skip_py36
def test_openapi_schema(client):
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema
@skip_py36
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
@skip_py36
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
@skip_py36
def test_inexistent_user(client):
response = client.get("/users/999")
assert response.status_code == 404
@skip_py36
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]
time.sleep = MagicMock()
@skip_py36
def test_get_slowusers(client):
response = client.get("/slowusers/")
assert response.status_code == 200
data = response.json()
assert "email" in data[0]
assert "id" in data[0]
@skip_py36
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"]
@skip_py36
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

View File

@@ -1,17 +1,16 @@
import sys
from typing import Optional, Union
import pytest
from fastapi import FastAPI
from pydantic import BaseModel
from starlette.testclient import TestClient
from .utils import skip_py36
# In Python 3.6:
# u = Union[ExtendedItem, Item] == __main__.Item
# But in Python 3.7:
# u = Union[ExtendedItem, Item] == typing.Union[__main__.ExtendedItem, __main__.Item]
skip_py36 = pytest.mark.skipif(sys.version_info < (3, 7), reason="skip python3.6")
app = FastAPI()

5
tests/utils.py Normal file
View File

@@ -0,0 +1,5 @@
import sys
import pytest
skip_py36 = pytest.mark.skipif(sys.version_info < (3, 7), reason="skip python3.6")