官方文件主要側重點是循序漸進地學習FastAPI, 不利於有其他框架使用經驗的人快速查閱
故本文與官方文件不一樣, 並補充了一些官方文件沒有的內容
安裝
包括安裝uvicorn
$pip install fastapi[all]
分開安裝
$pip install fastapi
$pip install uvicorn[standard]
uvicorn使用
uvicorn
是一個非常快速的 ASGI 伺服器。
官方文件在這裡: uvicorn
命令列啟動
# mian.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def index():
return {"index": "root"}
$uvicorn --reload main:app
程式碼中啟動
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def index():
return {"index": "root1"}
if __name__ == '__main__':
import uvicorn
uvicorn.run("main:app", host="127.0.0.1", port=8888, reload=True)
配置
配置名稱 命令列/引數 | 型別 | 說明 | 備註 |
---|---|---|---|
必選引數 /app |
str |
ASGI應用(app 是程式碼中的引數, 命令列啟動不需要宣告) [必須] |
格式: <module>:<attribute> , 如: main.py中的app ==> main:app |
--host /host |
str |
繫結的IP | 預設127.0.0.1 , 本地網路可用: -host 0.0.0.0 |
--port /port |
int |
繫結的埠 | 預設8000 |
--uds /uds |
str |
繫結到Unix domain socket |
沒用過 |
--fd /fd |
int |
將檔案描述符繫結到套接字 | 沒用過 |
--loop /loop |
str |
設定事件迴圈實現方式 | 可選值: auto asyncio uvloop , 注: uvloop 有更高效能, 但不相容Windows 和PyPy, 預設值為auto |
--http /http |
str |
設定 HTTP 協議實現方式 | 可選值: auto h11 httptools , 注: httptools 有更高效能, 但不相容PyPy, 且Windows需要進行編譯, 預設值為auto |
--ws /ws |
str |
設定 websocket 協議實現方式 | 可選值: auto none websockets wsproto , 注: none 拒絕所有ws請求, 預設為auto |
--ws-max-size /ws_max_size |
int |
設定websocket的最大訊息大小(單位: 位元組) | 需要與ws配置配合使用, 預設: 16 * 1024 * 1024 = 16777216 即16MB |
--ws-ping-interval /ws_ping_interval |
float |
設定websocket ping間隔(單位: 秒) | 需要與ws配置配合使用, 預設: 20秒 |
--ws-ping-timeout /ws_ping_timeout |
float |
設定websocket ping超時(單位: 秒) | 需要與ws配置配合使用, 預設: 20秒 |
--lifespan /lifespan |
str |
設定ASGI的Lifespan協議實現方式 | 可選值: auto on off , 預設值為auto |
--env-file /env_file |
str |
環境配置檔案路徑 | |
--log-config /log_config |
日誌配置檔案路徑, 格式: json/yaml (命令列) 字典(引數時) | 日誌配置 | 預設: uvicorn.config.LOGGING_CONFIG |
--log-level /log_level |
str |
日誌級別 | 可選項: critical error warning info debug trace , 預設值: info |
--no-access-log /access_log |
命令列只有--no-xxx bool (引數時) | 是否僅禁用訪問日誌,而不更改日誌級別 | 預設:True |
--use-colors /--no-use-colors/use_colors |
沒有值(命令列) bool (引數時) |
是否使用顏色渲染日誌 | 配置log-config CLI會忽略該配置 |
--interface /interface |
str |
選擇 ASGI3、 ASGI2或 WSGI 作為應用程式介面 | 可選項: auto asgi3 asgi2 wsgi , 預設: auto , 注: wsgi不支援WebSocket |
debug |
bool |
是否除錯 | 無命令列使用, 預設為: False |
--reload /reload |
bool (作為引數時) |
是否開啟熱載入 | 命令啟動不需要值, 預設False |
--reload-dir /reload_dirs |
path (命令列) [path1, path2](引數時) |
需要監聽熱載入的路徑或路徑列表 | 預設整個工作目錄 |
--reload-delay /reload_delay |
int |
熱載入延遲秒數 | 預設即刻載入 |
--reload-include /reload_includes |
glob-pattern(命令列) [<glob-pattern1, <glob-pattern2](引數時) |
需要監聽熱載入的路徑或路徑列表(支援glob模式) | 預設為*.py |
--reload-exclude /reload_exclude |
glob-pattern(命令列) [<glob-pattern1, <glob-pattern2](引數時) |
排除不需要監聽的檔案或目錄(支援glob模式) | 預設為 .* .py[cod] .sw.* ~* |
--workers /workers |
int |
工作程式數 | 預設$WEB_CONCURRENCY 環境變數或1 |
--root-path /root_path |
str |
為ASGI設定root_path | 沒用過 |
--proxy-headers /--no-proxy-headers /proxy_headers |
沒有值(命令列) bool (引數時) |
開啟/關閉 X-Forwarded-Proto, X-Forwarded-For, X-Forwarded-Port 來填充遠端地址資訊 | 預設值: True |
--forwarded-allow-ips /forwarded_allow_ips |
[str, ..] |
可信任IP地址 | 值為ip列表, 預設$FORWARDED_ALLOW_IPS 環境變數或127.0.0.1 , * 代表總信任 |
--limit-concurrency /limit_concurrency |
int |
在發出 HTTP 503響應之前, 允許的併發連線或任務的最大數量 | |
--limit-max-requests /limit_max_requests |
int |
終止程式之前的最大服務請求數 | 與程式管理器一起執行時非常有用, 可以防止記憶體洩漏影響長時間執行的程式 |
--backlog /backlog |
int |
backlog中的最大連線數量 | 預設值: 2048 |
--timeout-keep-alive /timeout_keep_alive |
int |
關閉Keep-Alive的最大超時數 | 預設值: 5 |
--ssl-keyfile /ssl_keyfile |
str |
SSL金鑰檔案路徑 | |
--ssl-keyfile-password /ssl_keyfile_password |
str |
SSL KEY 密碼 | |
--ssl-certfile /ssl_certfile |
srt |
SSL證照檔案路徑 | |
--ssl-version /ssl_version |
int |
SSL版本 | 預設為: ssl.PROTOCOL_TLS_SERVER |
--ssl-cert-reqs /ssl_cert_reqs |
int |
是否需要客戶端證照 | 預設為: ssl.CERT_NONE |
--ssl-ca-certs /ssl_ca_certs |
str |
CA 證照檔案 | |
--ssl-ciphers /ssl_ciphers |
str |
Ciphers | 預設值: TLSv1 |
--factory /factory |
沒有值 (命令列) bool (引數時) |
是否將應用視為應用工廠 | 預設值: False |
注: 使用
uvicorn --help
可以檢視完整配置
$uvicorn --help
Usage: uvicorn [OPTIONS] APP
...
路由
單個檔案
和其他輕型web框架一樣: 使用@xx.請求方式, 指定路徑
一般的使用: app = FastAPI()
@app.get()
@app.post()
@app.put()
@app.delete()
@app.options()
@app.head()
@app.patch()
@app.trace()
# 1 匯入fast api
from fastapi import FastAPI
# 2 建立例項
app = FastAPI()
# 3 繫結路由
"""
常見的REST url通常:
POST:建立資料。
GET:讀取資料。
PUT:更新資料。
DELETE:刪除資料。
"""
@app.get("/")
async def root():
return {"message": "hello world"}
引數見下文的
app.get等的引數
多個檔案
假如, 檔案結構這樣:
+--- app
| +--- main.py
| +--- routers
| | +--- movie.py
| | +--- music.py
| | +--- __init__.py
main.py
: 網站主頁, 負責啟動fastmovie.py
: 處理/movie/xxx
的URLmusic.py
: 處理/music/xxx
的URL
具體程式碼
使用兩種方式定義
# movie.py
from fastapi import APIRouter
router = APIRouter()
@router.get("/")
async def movie():
return {"message": "movie"}
# music.py
from fastapi import APIRouter
# 字首不能以 / 作為結尾
router = APIRouter(prefix="/music")
@router.get("/")
async def music():
return {"message": "music"}
# main.py
from fastapi import FastAPI
from routers import music, movie
app = FastAPI()
# 方式一,直接匯入
app.include_router(music.router)
# 方式二, 新增額外引數, 為已存在router修飾
app.include_router(prefix="/movie", router=movie.router)
@app.get("/")
async def root():
return {"message": "hello world"}
if __name__ == "__main__":
import uvicorn
config = {
"app": "main:app",
"host": "127.0.0.1",
"port": 8000,
"reload": True
}
uvicorn.run(**config)
訪問
http://127.0.0.1:8000/music/
和http://127.0.0.1:8000/movie/
可以找到對應的頁面
include_router
的引數見下文的app.include_router的引數
APIRouter
的引數見: APIRouter的引數
設定子應用
將一個app
掛載到另一個app
上
from fastapi import Depends, FastAPI
app = FastAPI()
sub_app = FastAPI()
# /home/
@app.get("/home/")
async def home():
return {"index": "home"}
# /api/users/
@sub_app.get("/users/")
async def users():
return {"index": "users"}
# 將 /api 掛在到 /
app.mount("/api", sub_app)
if __name__ == '__main__':
import uvicorn
uvicorn.run("fast_test:app", host="127.0.0.1", port=8888, reload=True)
一些引數
這部分內容包括FastAPI
APIRouter
app
app.include_router
的引數
FastAPI的引數
FastAPI繼承Starlette, 一些引數與Starlette的引數相同
引數 | 型別 | 說明 |
---|---|---|
debug |
bool |
是否在瀏覽器中, (如Django一樣) 顯示錯誤資訊Traceback |
title |
str |
文件的Title, 見: 文件資訊 |
description |
str |
文件的描述資訊, 見: 文件資訊 |
version |
str |
文件的應用版本, 見: 文件資訊 |
openapi_url |
str |
文件的json資料的URL, 預設/openapi.json , 見: 文件資訊 |
servers |
List[Dict[str, Union[str, Any]]] |
文件的服務列表, 見: 文件資訊 |
terms_of_service |
str |
文件的服務條款URL, 見: 文件資訊 |
contact |
Dict[str, Union[str, Any]] |
文件的定義聯絡資訊, 見: 文件資訊 |
license_info |
Dict[str, Union[str, Any]] |
文件的許可資訊, 見: 文件資訊 |
openapi_tags |
List[Dict[str, Any]] |
文件的標籤後設資料, 見: 標籤與標籤後設資料 |
deprecated |
bool |
True 時, 在文件中標記已過時的API, 見: 標記已過時api |
include_in_schema |
bool |
False 時, 將API從文件中排除, 見: 從文件中排除api |
responses |
Dict[Union[int, str], Dict[str, Any]] |
文件的響應資料, 見: api的返回值 |
dependencies |
Sequence[Depends] |
全域性依賴, 見: 全域性依賴 |
default_response_class |
Type[Response] |
預設響應類, 預設JSONResponse |
middleware |
Sequence[Middleware] |
中介軟體列表 |
docs_url |
str |
Swagger UI 文件路徑, 預設/docs , 為None 時禁用 |
redoc_url |
str |
ReDoc 文件路徑, 預設/redoc , 為None 時禁用 |
on_startup |
Sequence[Callable[[], Any]] |
應用啟動時的回撥函式 |
on_shutdown |
Sequence[Callable[[], Any]] |
應用關閉時的回撥函式 |
exception_handlers |
Dict[Union[int, Type[Exception]], Callable[[Request, Any], Coroutine[Any, Any, Response]],] |
異常處理器, 見: 自定義異常處理器 |
swagger_ui_oauth2_redirect_url |
str |
沒用過, 見文件 : OAuth2 redirect page, 預設/docs/oauth2-redirect |
swagger_ui_init_oauth |
Dict[str, Any] |
沒試過, 見文件: swagger_ui_init_oauth |
routes |
[List[BaseRoute]] |
路由列表, 見: Starlette Applications |
root_path |
str |
見: root_path |
root_path_in_servers |
bool |
見: Disable automatic server |
callbacks |
List[BaseRoute] |
見: callback |
APIRouter的引數
引數 | 型別 | 說明 |
---|---|---|
prefix |
str |
路由字首 |
tags |
[List[str] |
文件的Tag, 見: 標籤與標籤後設資料 |
responses |
Dict[Union[int, str], Dict[str, Any]] |
文件的響應資料, 見: api的返回值 |
deprecated |
bool |
True 時, 在文件中標記已過時的API, 見: 標記已過時api |
include_in_schema |
bool |
False 時, 將API從文件中排除, 見: 從文件中排除api |
dependencies |
Sequence[params.Depends] |
指定全域性依賴, 見: 全域性依賴 |
default_response_class |
Type[Response] |
預設響應類, 預設JSONResponse |
on_startup |
Sequence[Callable[[], Any]] |
應用啟動時的回撥函式 |
on_shutdown |
Sequence[Callable[[], Any]] |
應用關閉時的回撥函式 |
callbacks |
List[BaseRoute] |
見: callback |
routes |
[List[BaseRoute]] |
路由列表, 見: Starlette Applications |
redirect_slashes |
bool |
暫時不知道 |
default |
ASGIApp |
暫時不知道 |
dependency_overrides_provider |
Any |
暫時不知道 |
route_class |
Type[APIRoute] |
暫時不知道 |
app.get等的引數
說實話app.get
等的引數著實有點多, 而且很多都有生產doc有關, 具體如何使用可以點選表格中的連結.
引數 | 型別 | 說明 |
---|---|---|
path |
str |
請求路徑 |
response_model |
Type[Any] |
響應模型, 見: 快速模型 |
status_code |
int |
狀態碼, 見: status_code |
tags |
[List[str] |
文件的Tag, 見: 標籤與標籤後設資料 |
summary |
str |
文件的 路徑的概要, 見: API的概要及描述 |
description |
str |
文件的 路徑的描述資訊, 見: API的概要及描述 |
response_description |
str |
文件的 成功響應的描述資訊, 見: api的返回值 |
responses |
Dict[Union[int, str], Dict[str, Any]] |
文件的響應資料, 見: api的返回值 |
deprecated |
bool |
True 時, 在文件中標記已過時的API, 見: 標記已過時api |
include_in_schema |
bool |
False 時, 將API從文件中排除, 見: 從文件中排除api |
dependencies |
Sequence[params.Depends] |
指定路徑依賴, 見: 路徑依賴 |
response_class |
Type[Response] |
預設響應類, 預設JSONResponse |
response_model_include |
Union[SetIntStr, DictIntStrAny] |
響應模型中只返回某些欄位, 見: 只返回某些欄位 |
response_model_exclude |
Union[SetIntStr, DictIntStrAny] |
響應模型中的引數, 見: 為輸出模型作限定 |
response_model_by_alias |
bool |
暫時不知道 |
response_model_exclude_unset |
bool |
響應模型中不返回預設值, 見: 只返回某些欄位 |
response_model_exclude_defaults |
bool |
響應模型中不返回與預設值相同的值, 見: 不返回與預設值相同的值 |
response_model_exclude_none |
bool |
響應模型中不返回為None 的值 , 不返回為None 的值 |
operation_id |
str |
設定OpenAPI的operationId, 見: OpenAPI 的 operationId |
name |
str |
暫時不知道 |
callbacks |
List[BaseRoute] |
見: callback |
openapi_extra |
[Dict[str, Any] |
文件引數 |
app.include_router的引數
引數 | 型別 | 說明 |
---|---|---|
prefix |
str |
路由字首 |
tags |
[List[str] |
文件的Tag, 見: 標籤與標籤後設資料 |
responses |
Dict[Union[int, str], Dict[str, Any]] |
文件的響應資料, 見: api的返回值 |
deprecated |
bool |
True 時, 在文件中標記已過時的API, 見: 標記已過時api |
include_in_schema |
bool |
False 時, 將API從文件中排除, 見: 從文件中排除api |
default_response_class |
Type[Response] |
預設響應類, 預設JSONResponse |
dependencies |
Sequence[params.Depends] |
指定全域性依賴, 見: 全域性依賴 |
callbacks |
List[BaseRoute] |
見: callback |
Reqeust
解析請求引數的順序: 路徑引數 > 查詢引數 > 請求體引數
路徑引數
即, 一般的路由
不會把引數轉換為對應的資料型別
from fastapi import FastAPI
app = FastAPI()
# 路徑引數
@app.get("/test/{item_id}")
async def retrieve(item_id):
return {"item_id": item_id}
有型別的路徑引數
為引數指定引數型別即可
一些常用的型別見: typing
@app.get("/test/{item_id}")
async def retrieve(item_id: int):
# item_id 會自動轉換為int
return {"item_id": item_id}
引數對應的型別不對應的話, 報錯
給路徑引數設定預設值
使用列舉型別, 定義預設值
from fastapi import FastAPI
from typing import Optional
from enum import Enum
# ...
class ItemId(str, Enum):
a = "aa"
b = "bb"
c = "cc"
@app.get("/test2/{item_id}")
async def test2(item_id: ItemId):
# item_id只能是aa/bb/cc
# 裡面可以if判斷,處理不同的邏輯
return {"item_id": item_id}
引數對應的值, 不為預設值的話, 報錯
為路徑引數作描述或限制
使用
fastapi.Path
接收, 可以為路徑引數宣告相同型別的校驗和後設資料
from typing import Optional
from fastapi import FastAPI, Path, Query
app = FastAPI()
@app.get("/items/{item_id}")
async def read_items(
item_id: int = Path(..., title="The ID of the item to get"),
q: Optional[str] = Query(None, alias="item-query"),
):
results = {"item_id": item_id}
if q:
results.update({"q": q})
return results
注:
Path
是Param
的子類, 具有通用的方法, 具體引數見: Param
路徑轉換器
# 以下為路徑轉換器
@app.get("/test3/{file_path:path}")
async def file_retrieve(file_path):
return {"file_path": file_path}
這個例子, 會將形如:
/test3//root/
, 那麼, file_path:path為/root/
, 注意是兩個//
.
查詢引數
宣告不屬於路徑引數的其他函式引數時,它們將被自動解釋為"查詢字串"引數
預設引數
和路徑引數, 不一樣
查詢引數是可以有預設值的
# 沒有預設值:必選引數
# 有預設值: Optional, 非必選引數
# 可以是布林型別, 可將1/True/true/on/yes轉換為python的bool值
@app.get("/test")
async def test_list(page: int, limit: Optional[int] = None):
return {"page": page, "limit": limit}
設定引數預設值
from fastapi import FastAPI
from typing import Optional
from enum import Enum
# ...
# 引數預設值
class ModelName(str, Enum):
alexnet = "alexnet"
resnet = "resnet"
lenet = "lenet"
@app.get("/models")
async def get_model(model_name: ModelName):
if model_name == ModelName.alexnet:
return {"model_name": model_name, "message": "Deep Learning FTW!"}
if model_name.value == "lenet":
return {"model_name": model_name, "message": "LeCNN all the images"}
return {"model_name": model_name, "message": "Have some residuals"}
為查詢引數作描述或限制
fastapi.Query
可以為查詢引數進行校驗
@app.get("/items")
async def test3(item_id: List[int] = Query(..., title="id錯誤", description="id 必須大於10", alias="item-id", ge=10)):
# 路徑形如: http://127.0.0.1:8000/items?item-id=11&item-id=12
return {"item_id": item_id}
注:
Query
是Param
的子類, 具有通用的方法, 更多引數見: Param
請求體引數
請求體是客戶端傳送給 API 的資料
pydantic
庫是python中用於資料介面定義檢查與設定管理的庫。
FastAPI
會將pydantic
的型別在請求體中匹配
關於Pydantic的詳細操作, 見: Pydantic使用
BaseModel 一般使用
定義
pydantic.BaseModel
的子類, 作為接收請求體的型別
和typing
使用一樣, 使用=
指定預設值, 為可選引數, 不知道預設值則為必須引數
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
# 1. 定義pydantic.BaseModel 子類
class Item(BaseModel):
# 2. 定義資料型別
name: str
age: int
description: Optional[str] = None
# 3. 混合使用
# ** 請使用 postman等工具除錯
# ** Item
@app.post("/test/item/{item_id}")
async def item_retrieve(item_id, item: Item, page: int = 1, limit: Optional[int] = None):
print(item_id)
print(page)
print(limit)
return item.dict()
使用:
curl -X 'POST' \
'http://127.0.0.1:8000/test/item/1?page=1' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"name": "string",
"age": 10,
"description": "string"
}'
fastAPI
會將請求體中的資料賦值給Item
(我們定義的baseModel
子類)
關於BaseModel的方法, 可以看這裡Model屬性
一般的使用方法有item.name
或item.dict()
Field 額外約束
即
pydantic.BaseModel
與pydantic.Field
相結合
pydantic.Field
可以為BaseModel
的欄位新增額外的約束條件
Field
引數:
default
預設值, 注意:...
為必須值alias
別名, 即請求體的key
const
是否只能是預設值title
標題名稱, 預設為欄位名稱的title()
方法description
詳細, 用於文件使用gt/ge/lt/le/regex
大於/大於等於/小於/小於等於/正規表示式驗證
class Item(BaseModel):
# 2. 定義資料型別
name: str
age: int = Field(..., ge=10, description="age must ge 10", title="age title") # !!! 使用Field
description: Optional[str] = None
單個請求體引數
pydantic.BaseModel
可以匹配多條資料, 而fastapi.Body
只能匹配一條資料
當pydantic.BaseModel
與fastapi.Body
結合時, 傳入的資料需要裹上一個{}
@app.post("/test2/{item_id}")
async def test2_retrieve(item_id, item: Item, username: str = Body(..., regex=r"^lcz"), page: int = 1):
return {"username": username}
"""
傳送: http://127.0.0.1:8000/test2/1
{
"item": {
"name": "string",
"age":11,
"description": "string"
},
"username": "lczmx"
}
"""
注:
Body
是FieldInfo
的子類, 具有通用的方法, 更多引數見: Body
多個請求體模型-並列
多個
pydantic.BaseModel
引數, 請求體資料同樣在外面裹上一個{}
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
class User(BaseModel):
username: str
full_name: Optional[str] = None
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item, user: User):
results = {"item_id": item_id, "item": item, "user": user}
return results
資料:
{
"item": {
"name": "Foo",
"description": "The pretender",
"price": 42.0,
"tax": 3.2
},
"user": {
"username": "dave",
"full_name": "Dave Grohl"
}
}
多個請求體模型-巢狀
一個BaseModel
的欄位為另一個BaseModel
時, 傳入的資料同樣是巢狀的.
from typing import Optional, Set
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Image(BaseModel):
url: str
name: str
class Item(BaseModel):
name: str
# 巢狀另一個模型
image: Optional[Image] = None
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
results = {"item_id": item_id, "item": item}
return results
資料:
{
"name": "Foo",
"image": {
"url": "http://example.com/baz.jpg",
"name": "The Foo live"
}
}
列表請求體資料
只需要將引數指定為List[BaseModel]
即可:
from typing import List
from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl
app = FastAPI()
class Image(BaseModel):
url: HttpUrl
name: str
@app.post("/images/multiple/")
async def create_multiple_images(images: List[Image]):
return images
資料:
[
{
"url": "http://xxx.com/1.jpg",
"name": "1.jpg"
}
]
更多內建欄位型別
所有的欄位型別見官方文件: Field Types
上面欄位主要是這幾個:
- 標準的: Standard Library Types
pydantic
定義的: Pydantic Types- 等...
例子:
from datetime import datetime, time, timedelta
from typing import Optional
from uuid import UUID
from fastapi import Body, FastAPI
app = FastAPI()
@app.put("/items/{item_id}")
async def read_items(
item_id: UUID,
start_datetime: Optional[datetime] = Body(None),
end_datetime: Optional[datetime] = Body(None),
repeat_at: Optional[time] = Body(None),
process_after: Optional[timedelta] = Body(None),
):
start_process = start_datetime + process_after
duration = end_datetime - start_process
return {
"item_id": item_id,
"start_datetime": start_datetime,
"end_datetime": end_datetime,
"repeat_at": repeat_at,
"process_after": process_after,
"start_process": start_process,
"duration": duration,
}
也可以在BaseModel
子類中定義
更多驗證方式
pydantic擁有更加細的自定義驗證器定義方法, 詳情點選這裡
Form表單
需要安裝
python-multipart
:
$pip install python-multipart
讀取application/x-www-form-urlencoded
application/x-www-form-urlencoded
的資料形如:say=Hi&to=Mom
即, 我們一般的input
表單資料
from fastapi import FastAPI, Form
app = FastAPI()
@app.post("/login/")
async def login(username: str = Form(...), password: str = Form(...)):
return {"username": username}
傳送資料:
POST http://localhost:8000/login/
Content-Type: application/x-www-form-urlencoded
username=lczmx&password=123456
返回資料:
{
"username": "lczmx"
}
注:
Form
是Body
的子類, 具有通用的方法, 更多引數見: Body
讀取multipart/form-data
即上傳檔案
使用pycharm HTTP Client傳送資料:
POST /test.html HTTP/1.1
Host: example.org
Content-Type: multipart/form-data;boundary="boundary"
--boundary
Content-Disposition: form-data; name="field1"
value1
--boundary
Content-Disposition: form-data; name="field2"; filename="example.txt"
value2
有以下兩種接收方式:
使用bytes接收
在接收檔案時, 必須使用fastapi.File
, 否則, FastAPI 會把該引數當作查詢引數或請求體(JSON)引數。
注意: 檔案是二進位制資料, 故使用bytes型別. input標籤的name屬性作為變數名
例子:
from typing import List
from fastapi import FastAPI, File
app = FastAPI()
# 接收單個檔案直接用bytes, 多個檔案使用List
@app.post("/files/")
async def create_file(first: bytes = File(...), second: List[bytes] = File(...)):
return {
"firstFileSize": len(first),
"secondFilesContent": [f.decode("utf-8") for f in second]
}
if __name__ == '__main__':
import uvicorn
uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)
使用pycharm HTTP Client傳送資料:
POST http://localhost:8000/files/
Content-Type: multipart/form-data; boundary=boundary
--boundary
Content-Disposition: form-data; name="first"; filename="r.txt"
// 上傳r.txt, 需要本地有r.txt
< ./r.txt
--boundary
Content-Disposition: form-data; name="second"; filename="input-second.txt"
// 內容直接為Text Content1
Text Content1
--boundary
Content-Disposition: form-data; name="second"; filename="input-second.txt"
// 內容直接為Text Content2
Text Content2
響應資料:
{
"firstFileSize": 30,
"secondFilesContent": [
"Text Content1",
"Text Content2"
]
}
注:
File
是Form
的子類, 具有通用的方法, 更多引數見: Body
使用UploadFile接收
由於使用bytes
不能處理檔案的資訊, 為此在某些情況下使用UploadFile
更加方便
from typing import List
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
# 接收單個檔案直接用bytes, 多個檔案使用List
@app.post("/files/")
async def create_file(first: UploadFile = File(...), second: List[UploadFile] = File(...)):
return {
"firstFileName": first.filename,
"secondFilesContent": [f.file.read() for f in second]
}
if __name__ == '__main__':
import uvicorn
uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)
使用上面的請求資料, 響應資料為:
{
"firstFileName": "r.txt",
"secondFilesContent": [
"Text Content1",
"Text Content2"
]
}
UploadFile
與 bytes
相比有更多優勢:
- 使用UploadFile類進行檔案上傳時,
會使用到一種特殊機制“離線檔案”(Spooled File):即是當檔案在記憶體讀取超過一定限制後,多出來的部分會寫入磁碟。 - UploadFile適合用於大檔案傳輸, 如: 影像、視訊、二進位制檔案等大型檔案,好處是不會佔用所有記憶體;
- 自帶 file-like async 介面
- 暴露的Python SpooledTemporaryFile物件, 可直接傳遞給其他預期「file-like」物件的庫。
UploadFile
的屬性
屬性 | 說明 |
---|---|
filename |
上傳檔名字串 |
content_type |
內容型別, 全部型別見: MIME 型別 |
file |
是一個file-like 物件 |
UploadFile
的方法
方法 | 說明 |
---|---|
write(data) |
把 data (型別為str /bytes ) 寫入檔案 |
read(size) |
讀取指定size(型別為int )大小的位元組或字元 |
seek(offset) |
移動至檔案offset (型別為int ) 位元組處的位置 |
close() |
關閉檔案。 |
使用UploadFile讀取檔案資料:
# 1. async方法
contents = await myfile.read()
# 2. 普通方法
contents = myfile.file.read()
Response
response_class
引數可以指定響應類, 直接return
資料即可, 如 HTML
一般的response
from fastapi import FastAPI, Response
app = FastAPI()
@app.get("/index")
async def index():
"""
響應的引數
content 響應體內容
status_code 狀態碼, 預設200
headers 響應頭
media_type 響應型別
background 後臺任務
"""
f = open("statics/index.html", encoding="utf8")
response = Response(content=f.read(), media_type="text/html", status_code=200, headers={"x-server": "Test Server"})
f.close()
return response
if __name__ == "__main__":
import uvicorn
uvicorn.run(app="test:app", host="127.0.0.1", port=8000, reload=True)
響應模型
FastAPI可以根據根據請求資料快速返回對應的資料
如:
// Request:
// POST /book
{
"name": "book1",
"price": 99
}
// Response:
{
"name": "book1",
"price": 99
}
一般使用 輸入同輸出
通過
response_model
引數指定
但是, 不通過response_model
引數直接返回亦可以, 但不能自動生成返回值的doc
程式碼:
from typing import List, Optional
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
tags: List[str] = []
# 這種情況可以省略response_model
# 但是, 省略的話, 不能再doc中顯示
@app.post("/items/", response_model=Item)
async def create_item(item: Item):
return item
if __name__ == '__main__':
import uvicorn
uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)
請求:
使用pycharm HTTP Client傳送資料:
POST http://localhost:8000/items
Content-Type: application/json
{
"name": "name1",
"price": 1000,
"description": "this is description"
}
響應:
{
"name": "name1",
"description": "this is description",
"price": 1000.0,
"tax": null,
"tags": []
}
FastAPI會將
resturn
的資料自動轉換為Item
中的資料
所以需要名稱對應, 缺失欄位的話會報錯!!
注意: 這種使用方法會將全部請求資料作為返回資料, 在某些場合並不適合!
輸入模型與輸出模型分開
from typing import Optional
from pydantic import BaseModel
from fastapi import FastAPI
class UserIn(BaseModel):
"""
使用者輸入資料
"""
username: str
password: str
age: int
description: Optional[str] = None
class UserOut(BaseModel):
"""
使用者輸出資料
"""
# 剔除password
username: str
age: int
description: Optional[str] = None
app = FastAPI()
@app.post("/user", response_model=UserOut)
def register(data: UserIn):
return data
if __name__ == '__main__':
import uvicorn
uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)
可以看到: 接收資料模型為
UserIn
,return data
使用輸出資料模型 (UserOut
) 接收
為輸出模型作限定
我們可以通過指定引數, 為輸出模型的欄位作修改
也就是說, 我們在某些場合下可以 在只使用一個模型的情況下 過濾敏感資料
-
不返回預設值
response_model_exclude_unset
FastAPI預設會將預設值返回
from typing import Optional from pydantic import BaseModel from fastapi import FastAPI class UserIn(BaseModel): """ 使用者輸入資料 """ username: str password: str age: int description: Optional[str] = None class UserOut(BaseModel): """ 使用者輸出資料 """ # 剔除password username: str age: int description: Optional[str] = None app = FastAPI() @app.post("/user", response_model=UserOut, response_model_exclude_unset=True) def register(data: UserIn): return data if __name__ == '__main__': import uvicorn uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)
如傳送資料為:
{ "username": "lczmx", "password": "123456", "age": 18 }
返回資料為:
{ "username": "lczmx", "age": 18 }
原理: FastAPI會將輸出模型的
.dict()
方法的exclude_unset
引數指定, 見: pydanticExporting models -
不返回與預設值相同的值
response_model_exclude_defaults
from typing import Optional from pydantic import BaseModel from fastapi import FastAPI class UserIn(BaseModel): """ 使用者輸入資料 """ username: str password: str age: int description: Optional[str] = None class UserOut(BaseModel): """ 使用者輸出資料 """ # 剔除password username: str age: int description: Optional[str] = "abc" app = FastAPI() @app.post("/user", response_model=UserOut, response_model_exclude_defaults=True) def register(data: UserIn): return data if __name__ == '__main__': import uvicorn uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)
如傳送資料為:
{ "username": "lczmx", "password": "123456", "age": 18, "description": "abc" }
返回資料為:
{ "username": "lczmx", "age": 18 }
原理: FastAPI會將輸出模型的
.dict()
方法的exclude_defaults
引數指定, 見: pydanticExporting models -
不返回為
None
的值response_model_exclude_none
from typing import Optional from pydantic import BaseModel from fastapi import FastAPI class UserIn(BaseModel): """ 使用者輸入資料 """ username: str password: str age: int description: Optional[str] = None class UserOut(BaseModel): """ 使用者輸出資料 """ # 剔除password username: str age: int description: Optional[str] = "abc" app = FastAPI() @app.post("/user", response_model=UserOut, response_model_exclude_none=True) def register(data: UserIn): return data if __name__ == '__main__': import uvicorn uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)
如傳送資料為:
{ "username": "lczmx", "password": "123456", "age": 18, "description": null }
返回資料為:
{ "username": "lczmx", "age": 18 }
原理: FastAPI會將輸出模型的
.dict()
方法的exclude_none
引數指定, 見: pydanticExporting models -
只返回某些欄位
response_model_include
from typing import Optional from pydantic import BaseModel from fastapi import FastAPI class UserIn(BaseModel): """ 使用者輸入資料 """ username: str password: str age: int description: Optional[str] = None app = FastAPI() @app.post("/user", response_model=UserIn, response_model_include={"password"}) def register(data: UserIn): return data if __name__ == '__main__': import uvicorn uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)
例子中: 只返回
password
欄位
原理: FastAPI會將輸出模型的.dict()
方法的include
引數指定, 見: pydanticExporting models -
不返回某些欄位
response_model_exclude
from typing import Optional from pydantic import BaseModel from fastapi import FastAPI class UserIn(BaseModel): """ 使用者輸入資料 """ username: str password: str age: int description: Optional[str] = None app = FastAPI() @app.post("/user", response_model=UserIn, response_model_exclude={"password"}) def register(data: UserIn): return data if __name__ == '__main__': import uvicorn uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)
例子中: 不返回
password
欄位
原理: FastAPI會將輸出模型的.dict()
方法的exclude
引數指定, 見: pydanticExporting models
通過繼承減少程式碼
以註冊為例子
from typing import Optional
from hashlib import md5
import logging
from logging import config
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
# 祕鑰
SECRET = r"""=+Au+Z]Ho%W@fG6j7gb\`_@=tUG`|6*!yze:=fi(v&125hirNc$('=AH3FC"wj)E"""
# logging配置
config.dictConfig({
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"running": {
"()": "uvicorn.logging.DefaultFormatter",
"fmt": "%(levelprefix)s %(message)s",
"use_colors": None,
},
},
"handlers": {
"running": {
"formatter": "running",
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout",
},
},
"loggers": {
"running": {"handlers": ["running"], "level": "INFO"},
},
})
logger = logging.getLogger("running")
log_level = logging.INFO # 預設logging級別
class UserBase(BaseModel):
"""
用做資料模板
"""
username: str
email: EmailStr
full_name: Optional[str] = None
class UserIn(UserBase):
"""
輸入模型
"""
password: str
class UserOut(UserBase):
"""
輸出模型
同 UserBase
"""
pass
class UserInDB(UserBase):
"""
寫入資料庫的模型
"""
hashed_password: str
def fake_password_hasher(raw_password: str) -> str:
"""
為明文密碼作hash
:param raw_password: 明文密碼
:return: 加密密文
"""
m = md5()
m.update(SECRET.encode())
m.update(raw_password.encode())
return m.hexdigest()
def create_user(user_in: UserIn):
"""
建立使用者並儲存到資料庫[假裝]
"""
hashed_password = fake_password_hasher(user_in.password)
user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password)
logger.info("save to db")
if log_level <= logging.DEBUG:
logger.setLevel(logging.DEBUG)
logger.debug(f"hashed password is {hashed_password}")
logger.setLevel(logging.INFO)
return user_in_db
@app.post("/user", response_model=UserOut)
async def register(user_in: UserIn):
user_saved = create_user(user_in)
return user_saved
if __name__ == '__main__':
import uvicorn
uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True, log_level=log_level)
請求資料:
POST http://localhost:8000/user
Content-Type: application/json
{
"username": "lczmx",
"email": "lczmx@foxmail.com",
"full_name": "xxx",
"password": "123456"
}
響應資料:
{
"username": "lczmx",
"email": "lczmx@foxmail.com",
"full_name": "xxx"
}
使用Union List Dict與模型結合
- Union
你可以將一個響應宣告為兩種型別的 Union,這意味著該響應將是兩種型別中的任何一種。from typing import Union from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class BaseItem(BaseModel): description: str type: str class CarItem(BaseItem): type = "car" class PlaneItem(BaseItem): type = "plane" size: int items = { "item1": {"description": "All my friends drive a low rider", "type": "car"}, "item2": { "description": "Music is my aeroplane, it's my aeroplane", "type": "plane", "size": 5, }, } @app.get("/items/{item_id}", response_model=Union[PlaneItem, CarItem]) async def read_item(item_id: str): return items[item_id]
- List
宣告由物件列表構成的響應from typing import List from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str description: str items = [ {"name": "Foo", "description": "There comes my hero"}, {"name": "Red", "description": "It's my aeroplane"}, ] @app.get("/items/", response_model=List[Item]) async def read_items(): return items
- Dict
你還可以使用一個任意的普通 dict 宣告響應,僅宣告鍵和值的型別,而不使用 Pydantic 模型。from typing import Dict from fastapi import FastAPI app = FastAPI() @app.get("/keyword-weights/", response_model=Dict[str, float]) async def read_keyword_weights(): return {"foo": 2.3, "bar": 3.4}
status_code
FastAPI支援修改status code
status_code
可以直接用數字表示, 但FastAPI提供了一些內建狀態碼變數:
位於fastpi.status
, 需要根據需求確定具體要用哪個狀態碼
HTTP狀態碼可以點選這裡檢視, WebSocket狀態碼可以點選這裡檢視
修改成功響應的狀態碼
from typing import Optional
from fastapi import FastAPI, status
from pydantic import BaseModel
app = FastAPI()
class BookModel(BaseModel):
name: str
price: int
info: Optional[str] = None
@app.post("/books", status_code=status.HTTP_201_CREATED, response_model=BookModel)
def create_book(data: BookModel):
return data
if __name__ == '__main__':
import uvicorn
uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)
使用pycharm HTTP Client傳送資料:
POST http://localhost:8000/books/
Content-Type: application/json
{
"name": "b1",
"price": 100,
"info": "book b1 information"
}
響應的資料:
POST http://localhost:8000/books/
HTTP/1.1 201 Created
date: Sat, 06 Nov 2021 13:28:06 GMT
server: uvicorn
content-length: 54
content-type: application/json
{
"name": "b1",
"price": 100,
"info": "book b1 information"
}
在執行過程中修改狀態碼
比如: 使用PUT
請求, 若資料已經存在, 返回已經存在資料 狀態碼為200
, 否則建立, 返回資料 狀態碼為201
from fastapi import FastAPI, Response, status
app = FastAPI()
tasks = {"foo": "Listen to the Bar Fighters"}
@app.put("/get-or-create-task/{task_id}", status_code=200)
def get_or_create_task(task_id: str, response: Response):
if task_id not in tasks:
tasks[task_id] = "This didn't exist before"
response.status_code = status.HTTP_201_CREATED
return tasks[task_id]
if __name__ == '__main__':
import uvicorn
uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)
即, 通過
response.status_code
指定
JSON
FastAPI預設返回json
格式的資料, 即response_class
的預設值為: JSONResponse
將其他資料結構轉化為json, 見這裡: 資料轉換
HTML
通過response_class
引數處理響應的類, HTMLResponse
即返回html的類
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
app = FastAPI()
@app.get("/", response_class=HTMLResponse)
async def home():
return """<html>
<head>
<title>title</title>
</head>
<body>
<h1>測試HTML</h1>
</body>
</html>
"""
if __name__ == '__main__':
import uvicorn
uvicorn.run("fast_test:app", host="127.0.0.1", port=8888, reload=True)
除此外, 你還可以使用模板引擎, 如: jinja2
, 使用方式如下
- 安裝
jinja2
$pip install jinja2
fastapi-jinja2.py
from fastapi import FastAPI, Request from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates app = FastAPI() # 設定template目錄 templates = Jinja2Templates(directory="templates") # 設定response_class @app.get("/", response_class=HTMLResponse) async def root(request: Request): data = { "id": 1, "name": "lczmx", "message": "hello world", "tags": ["tag1", "tag2", "tag3", "tag4"] } # !!! 必須帶上request return templates.TemplateResponse("index.html", {"request": request, "data": data}) if __name__ == '__main__': import uvicorn uvicorn.run(app="fastapi-jinja2:app", host="0.0.0.0", port=8000, reload=True)
templates/index.html
假如需要靜態檔案, 可以這樣寫:<!DOCTYPE html> <html lang="en"> <head> <title>Title</title> </head> <body> <p>id: {{ data.id}}</p> <p>name: {{ data.name}}</p> <p>message: {{ data.message}}</p> {% for tag in data.tags %} <li>{{ tag }}</li> {% endfor %} </body> </html>
<link href="{{ url_for('static', path='/styles.css') }}" rel="stylesheet">
關於
jinja2
的一般語法, 見: 模板引擎
靜態檔案
需要設定靜態檔案的路徑
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
app = FastAPI()
# 訪問/static/xxx 時 會找 伺服器的statics/xxx
app.mount("/static", StaticFiles(directory="statics"), name="statics")
if __name__ == '__main__':
import uvicorn
uvicorn.run("fast_test:app", host="127.0.0.1", port=8888, reload=True)
內部呼叫的是
starlette.staticfiles
重定向
預設
307
狀態碼 (臨時重定向)
from fastapi import FastAPI, Response
from fastapi.responses import RedirectResponse
app = FastAPI()
@app.get("/")
async def index_redirect():
"""
url 要跳轉的url
status_code 狀態碼 預設307
headers 響應頭
background 後臺任務
"""
return RedirectResponse("/index")
迭代返回流式傳輸響應主體
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
app = FastAPI()
async def fake_video_streamer():
"""假裝讀取視訊檔案, 並yield"""
for i in range(10):
yield b"some fake video bytes"
@app.get("/")
async def main():
return StreamingResponse(fake_video_streamer())
if __name__ == "__main__":
import uvicorn
uvicorn.run(app="test:app", host="127.0.0.1", port=8000, reload=True)
非同步傳輸檔案
from fastapi import FastAPI
from fastapi.responses import FileResponse
# 檔案路徑
some_file_path = "large-video-file.mp4"
app = FastAPI()
@app.get("/")
async def main():
return FileResponse(some_file_path)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app="test:app", host="127.0.0.1", port=8000, reload=True)
異常處理
主動觸發異常
觸發的是使用者的異常, 即以
4
開頭的狀態碼
例子:
from fastapi import FastAPI, Path, HTTPException, status
app = FastAPI()
book_data = {
1: {
"name": "book1",
"price": 88
},
2: {
"name": "book2",
"price": 89
},
3: {
"name": "book3",
"price": 99
}
}
@app.get("/books/{book_id}")
def book_retrieve(book_id: int):
book_item = book_data.get(book_id)
if not book_item:
# 不存在的book id
# 主動丟擲HTTPException
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
# 定製detail資訊和響應頭
detail="不存在book id",
headers={"X-Error": "book not exists error"})
return book_item
if __name__ == '__main__':
import uvicorn
uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)
使用pycharm HTTP Client傳送資料:
### 請求1
GET http://localhost:8000/books/1
### 請求2
GET http://localhost:8000/books/4
響應資料
GET http://localhost:8000/books/1
HTTP/1.1 200 OK
date: Sat, 06 Nov 2021 16:04:45 GMT
server: uvicorn
content-length: 27
content-type: application/json
{
"name": "book1",
"price": 88
}
GET http://localhost:8000/books/4
HTTP/1.1 404 Not Found
date: Sat, 06 Nov 2021 16:02:52 GMT
server: uvicorn
x-error: book not exists error
content-length: 29
content-type: application/json
{
"detail": "不存在book id"
}
自定義異常處理器
步驟:
- 定義異常類
- 新增異常處理器
from fastapi import FastAPI, Path, status, Request
from fastapi.responses import JSONResponse
app = FastAPI()
book_data = {
1: {
"name": "book1",
"price": 88
},
2: {
"name": "book2",
"price": 89
},
3: {
"name": "book3",
"price": 99
}
}
# 自定義異常類
class NotFoundException(Exception):
def __init__(self, name):
self.name = name
# 自定義異常處理器 即處理函式
@app.exception_handler(NotFoundException)
def not_found_handler(request: Request, exc: NotFoundException):
content = {
"status": False,
"message": f"{exc.name} not exists"
}
return JSONResponse(status_code=status.HTTP_404_NOT_FOUND,
content=content,
headers={"X-Error": "not exists error"})
@app.get("/books/{book_id}")
def book_retrieve(book_id: int):
book_item = book_data.get(book_id)
if not book_item:
# 主動丟擲異常
raise NotFoundException("book id")
return book_item
if __name__ == '__main__':
import uvicorn
uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)
使用pycharm HTTP Client傳送資料:
GET http://localhost:8000/books/4
響應資料:
GET http://localhost:8000/books/4
HTTP/1.1 404 Not Found
date: Sat, 06 Nov 2021 16:31:40 GMT
server: uvicorn
x-error: not exists error
content-length: 47
content-type: application/json
{
"status": false,
"message": "book id not exists"
}
只要觸發了
exception_handler
中繫結的異常, 就會呼叫對應的處理函式
修改內建異常處理器
FastAPI 自帶了一些預設異常處理器, 在執行過程中碰到異常時, FastAPI就會根據這些異常處理器處理異常並返回資料
內建異常類, 位於 fastapi.exceptions
類名稱 | 說明 |
---|---|
HTTPException |
包含了和 API 有關資料的常規 Python 異常 |
RequestValidationError |
繼承pydantic ValidationError , 使用 Pydantic 模型, 資料有錯誤時觸發 |
關於 ValidationError
與 RequestValidationError
的關係, 見官網的介紹: RequestValidationError vs ValidationError
內建異常處理器, 位於fastapi.exception_handlers
異常處理器名稱 | 說明 |
---|---|
http_exception_handler |
返回JSONResponse({"detail": ..}, status_code=..., headers=...) |
request_validation_exception_handler |
直接丟擲Exception , 故狀態碼為500 |
from fastapi import FastAPI
from fastapi.exceptions import HTTPException
from fastapi.responses import JSONResponse
app = FastAPI()
# 只需要將內建異常類, 新增到異常處理器字典即可
@app.exception_handler(HTTPException)
async def http_exception_handler(request, exc):
content = {
"status": False,
"detail": str(exc.detail)
}
return JSONResponse(content, status_code=exc.status_code)
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id == 3:
raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
return {"item_id": item_id}
if __name__ == '__main__':
import uvicorn
uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)
可以與原異常處理器配合使用,
return await http_exception_handler(request, exc)
這樣使用即可
關於
ValidationError
的屬性, 見: pydantic官網
資料轉換
FastAPI提供了將其他資料型別轉化為JSON相容的資料型別的函式: fastapi.encoders.jsonable_encoder
根據原始碼, jsonable_encoder
提供了以下型別的資料的轉換:
pydantic.BaseModel
dataclasses
enum.Enum
pathlib.PurePath
str, int, float, type(None)
dict
list, set, frozenset, types.GeneratorType, tuple
一般使用
from typing import List, Optional
from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
price: Optional[float] = None
tax: float = 10.5
tags: List[str] = []
@app.get("/item")
async def read_item():
data = {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []}
data_dict = jsonable_encoder(Item(**data))
print(type(data_dict)) # <class 'dict'>
return data_dict
if __name__ == '__main__':
import uvicorn
uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)
其他引數
jsonable_encoder
有很多引數, 部分引數和get/post/put/delete
等方法的引數類似, 見: 為輸出模型作限定
-
include
只返回某些欄位 -
exclude
不返回某些欄位 -
by_alias
欄位別名是否應該用作返回字典中的鍵 -
exclude_unset
不返回預設值 -
exclude_defaults
不返回與預設值相同的值 -
exclude_none
不返回為None
的值 -
custom_encoder
指定自定義的編碼器
先看看呼叫custom_encoder
的原始碼: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)
也就是說
custom_encoder
應該是dict
, key為型別, value為具體的處理函式
例子:from typing import Optional from fastapi.encoders import jsonable_encoder from pydantic import BaseModel class BookItem(BaseModel): name: Optional[str] = None price: Optional[float] = None class AuthorClass: def __init__(self, name: str, age: int): self.name = name self.age = age def __str__(self): return f"{self.name} ({self.age})" def __repr__(self): return self.__str__() # 自定義的編碼器 # 將類屬性轉換為字典 custom_encoder = { AuthorClass: lambda obj: {"name": obj.name, "age": obj.age} } book_data = BookItem(**{"name": "book1", "price": 50.2}).dict() author_instance = AuthorClass(name="lczmx", age=18) # 更新資料 book_data.update({"author": author_instance}) print(book_data) # {'name': 'book1', 'price': 50.2, 'author': lczmx (18)} data_dict = jsonable_encoder(book_data, custom_encoder=custom_encoder) print(data_dict) # {'name': 'book1', 'price': 50.2, 'author': {'name': 'lczmx', 'age': 18}}
你亦可以在
BaseModel
中指定json_encoders
作為編碼器, 若想知道如何使用見: json_encoders -
sqlalchemy_safe
暫不知道該引數有什麼用 (待補充)
ORM
下面舉一個完整的專案, 說明如何在FastAPI中使用ORM
使用的是SQLAlchemy這個框架
專案結構
+--- test_app
| +--- __init__.py
| +--- crud.py
| +--- database.py
| +--- main.py
| +--- models.py
| +--- schemas.py
+--- run.py
專案依賴:
fastapi==0.63.0
pydantic==1.7.3
requests==2.25.1
SQLAlchemy==1.3.22
程式碼
-
run.py
程式的入口import uvicorn from fastapi import FastAPI from test_app import application app = FastAPI( title='Fast ORM 測試', description='FastAPI 使用SQlAlchemy框架', version='1.0.0', docs_url='/docs', redoc_url='/redocs', ) app.include_router(application, prefix='/test_app', tags=['FastAPI ORM']) if __name__ == '__main__': uvicorn.run('run:app', host='0.0.0.0', port=8000, reload=True, debug=True, workers=1)
-
test_app/__init__.py
用作run.py
匯入from .main import application
-
test_app/database.py
用於建立連線和生成建立表的公共基類from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker SQLALCHEMY_DATABASE_URL = 'sqlite:///./coronavirus.sqlite3' # MySQL或PostgreSQL的連線方法: # SQLALCHEMY_DATABASE_URL = "postgresql://username:password@host:port/database_name" engine = create_engine( # echo=True表示引擎將用repr()函式記錄所有語句及其引數列表到日誌 # 由於SQLAlchemy是多執行緒,指定check_same_thread=False來讓建立的物件任意執行緒都可使用。這個引數只在用SQLite資料庫時設定 SQLALCHEMY_DATABASE_URL, encoding='utf-8', echo=True, connect_args={'check_same_thread': False} ) # 在SQLAlchemy中,CRUD都是通過會話(session)進行的,所以我們必須要先建立會話,每一個SessionLocal例項就是一個資料庫session # flush()是指傳送資料庫語句到資料庫,但資料庫不一定執行寫入磁碟;commit()是指提交事務,將變更儲存到資料庫檔案 SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, expire_on_commit=True) # 建立基本對映類 Base = declarative_base(bind=engine, name='Base')
-
test_app/crud.py
用於增刪改查""" 資料增刪改查介面 """ from sqlalchemy.orm import Session from test_app import models, schemas def get_city(db: Session, city_id: int): return db.query(models.City).filter(models.City.id == city_id).first() def get_city_by_name(db: Session, name: str): return db.query(models.City).filter(models.City.province == name).first() def get_cities(db: Session, skip: int = 0, limit: int = 10): return db.query(models.City).offset(skip).limit(limit).all() def create_city(db: Session, city: schemas.CreateCity): db_city = models.City(**city.dict()) db.add(db_city) db.commit() db.refresh(db_city) return db_city def get_data(db: Session, city: str = None, skip: int = 0, limit: int = 10): if city: return db.query(models.Data).filter( models.Data.city.has(province=city)) # 外來鍵關聯查詢,這裡不是像Django ORM那樣Data.city.province return db.query(models.Data).offset(skip).limit(limit).all() def create_city_data(db: Session, data: schemas.CreateData, city_id: int): db_data = models.Data(**data.dict(), city_id=city_id) db.add(db_data) db.commit() db.refresh(db_data) return db_data
-
test_app/schemas.py
定義 傳入或返回的資料from datetime import date as date_ from datetime import datetime from pydantic import BaseModel class CreateData(BaseModel): date: date_ confirmed: int = 0 deaths: int = 0 recovered: int = 0 class CreateCity(BaseModel): province: str country: str country_code: str country_population: int class ReadData(CreateData): id: int city_id: int updated_at: datetime created_at: datetime class Config: orm_mode = True class ReadCity(CreateCity): id: int updated_at: datetime created_at: datetime class Config: orm_mode = True
-
test_app/main.py
定義網站的邏輯程式碼from typing import List import requests from pydantic import HttpUrl from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks from sqlalchemy.orm import Session from test_app import crud, schemas from test_app.database import engine, Base, SessionLocal from test_app.models import City, Data application = APIRouter() # 建立表 Base.metadata.create_all(bind=engine) def get_db(): db = SessionLocal() try: yield db finally: db.close() def bg_task(url: HttpUrl, db: Session): """建立資料 根據返回資料解析成 需要的格式 """ city_data = requests.get(url=f"{url}?source=jhu&country_code=CN&timelines=false") if 200 == city_data.status_code: db.query(City).delete() # 同步資料前先清空原有的資料 for location in city_data.json()["locations"]: city = { "province": location["province"], "country": location["country"], "country_code": "CN", "country_population": location["country_population"] } crud.create_city(db=db, city=schemas.CreateCity(**city)) coronavirus_data = requests.get(url=f"{url}?source=jhu&country_code=CN&timelines=true") if 200 == coronavirus_data.status_code: db.query(Data).delete() for city in coronavirus_data.json()["locations"]: db_city = crud.get_city_by_name(db=db, name=city["province"]) for date, confirmed in city["timelines"]["confirmed"]["timeline"].items(): data = { "date": date.split("T")[0], # 把'2020-12-31T00:00:00Z' 變成 ‘2020-12-31’ "confirmed": confirmed, "deaths": city["timelines"]["deaths"]["timeline"][date], "recovered": 0 # 每個城市每天有多少人痊癒,這種資料沒有 } # 這個city_id是city表中的主鍵ID,不是coronavirus_data資料裡的ID crud.create_city_data(db=db, data=schemas.CreateData(**data), city_id=db_city.id) @application.get("/gen_data/jhu", description="在後臺生成資料") def gen_data(background_tasks: BackgroundTasks, db: Session = Depends(get_db)): """在後灘自動生成資料""" background_tasks.add_task(bg_task, "https://coronavirus-tracker-api.herokuapp.com/v2/locations", db) return {"message": "正在後臺同步資料..."} @application.post("/create_city", response_model=schemas.ReadCity, description="建立一個城市資料") def create_city(city: schemas.CreateCity, db: Session = Depends(get_db)): db_city = crud.get_city_by_name(db, name=city.province) if db_city: raise HTTPException(status_code=400, detail="City already registered") return crud.create_city(db=db, city=city) @application.get("/get_city/{city}", response_model=schemas.ReadCity, description="獲取一個城市的資料") def get_city(city: str, db: Session = Depends(get_db)): db_city = crud.get_city_by_name(db, name=city) if db_city is None: raise HTTPException(status_code=404, detail="City not found") return db_city @application.get("/get_cities", response_model=List[schemas.ReadCity], description="獲取全部城市的資料") def get_cities(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): cities = crud.get_cities(db, skip=skip, limit=limit) return cities @application.post("/create_data", response_model=schemas.ReadData, description="建立一個城市的資料") def create_data_for_city(city: str, data: schemas.CreateData, db: Session = Depends(get_db)): db_city = crud.get_city_by_name(db, name=city) data = crud.create_city_data(db=db, data=data, city_id=db_city.id) return data @application.get("/get_data", description="獲取一個城市的資料") def get_data(city: str = None, skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): data = crud.get_data(db, city=city, skip=skip, limit=limit) return data
認證
即確認, 你到底是不是你?
OAUTH2.0
OAuth是一個驗證授權(Authorization)的開放標準, 詳情見: 理解OAuth 2.0
OAuth2的授權原理圖:
OAuth2.0的授權模式有三種:
- 授權碼模式
Authoriztion Code Grant
- 隱授權碼模式
Implicit Grant
- 密碼授權模式
Resource Owner Password Credentials Grant
- 客戶端憑證授權模式
client Credentials Grant
這裡的例子用的是第三種模式: 密碼授權模式
使用密碼授權模式需要兩個類:
-
fastapi.security.OAuth2PasswordBearer
OAuth2PasswordBearer
是接收URL
作為引數的一個類, 這並 不會 建立相應的URL
路徑操作,只是指明客戶端用來請求Token
的URL
地址
客戶端會向該URL傳送username和password引數,然後得到一個Token值
作為依賴注入時, 表明該URL
需要進行驗證: 當請求到來的時候,FastAPI會檢查請求的Authorization
頭資訊,
若: 無Authorization
頭資訊,或者頭資訊的內容不是Bearer token
, 它會丟擲異常:raise HTTPException( status_code=HTTP_401_UNAUTHORIZED, detail="Not authenticated", headers={"WWW-Authenticate": "Bearer"}, )
檢驗成功返回
token
注: 沒有這檢驗token的合法性, 只是檢驗有無請求頭, 所以需要我們手寫檢驗token
的邏輯!! -
fastapi.security.OAuth2PasswordRequestForm
OAuth2PasswordRequestForm
可用於接收登入資料, 資料型別為Form
, 即application/x-www-form-urlencoded
OAuth2PasswordRequestForm
的欄位有:- grant_type 授權模式,
passwrod
- username 登陸的使用者名稱
- password 登陸的密碼
- scope 用來限制客戶端的訪問範圍,如果為空(預設)的話,那麼客戶端擁有全部的訪問範圍
格式形如:items:read items:write users:read profile openid
- client_id 客戶端金鑰
- client_secret 客戶端ID
- grant_type 授權模式,
例子:
from typing import Optional
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
app = FastAPI()
# 告知客戶端 請求Token的URL地址是 /token
oauth2_schema = OAuth2PasswordBearer(tokenUrl="/token")
# 模擬資料庫的資料
fake_users_db = {
"john snow": {
"username": "john snow",
"full_name": "John Snow",
"email": "johnsnow@example.com",
"hashed_password": "fakehashedsecret",
"disabled": False,
},
"alice": {
"username": "alice",
"full_name": "Alice Wonderson",
"email": "alice@example.com",
"hashed_password": "fakehashedsecret2",
"disabled": True,
},
}
# hash 密碼
def fake_hash_password(password: str):
return "fakehashed" + password
class User(BaseModel):
username: str
email: Optional[str] = None
full_name: Optional[str] = None
disabled: Optional[bool] = None
class UserInDB(User):
hashed_password: str
# 登入
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
user_dict = fake_users_db.get(form_data.username)
if not user_dict:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Incorrect username or password")
user = UserInDB(**user_dict)
hashed_password = fake_hash_password(form_data.password)
# 檢驗密碼
if not hashed_password == user.hashed_password:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Incorrect username or password")
return {"access_token": user.username, "token_type": "bearer"}
# 獲取使用者
def get_user(db, username: str):
if username in db:
user_dict = db[username]
return UserInDB(**user_dict)
# 檢驗token的合法性
def fake_decode_token(token: str):
user = get_user(fake_users_db, token)
return user
# 檢驗是否 已經驗證了
async def get_current_user(token: str = Depends(oauth2_schema)):
# 這裡的token是使用者名稱
user = fake_decode_token(token)
if not user:
# UNAUTHORIZED 的 固定寫法
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
# OAuth2的規範,如果認證失敗,請求頭中返回“WWW-Authenticate”
headers={"WWW-Authenticate": "Bearer"},
)
return user
async def get_current_active_user(current_user: User = Depends(get_current_user)):
if current_user.disabled:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user")
return current_user
# 獲得 active的使用者
@app.get("/users/me")
async def read_users_me(current_user: User = Depends(get_current_active_user)):
return current_user
if __name__ == "__main__":
import uvicorn
uvicorn.run("fastapi-test:app", port=8000, reload=True)
主要注意/users/me
和/token
路由, 以及fake_decode_token
函式, 上面程式碼看起來比較複雜, 只是由於使用了依賴注入 一層套一層而已.
JWT
JWT介紹
jwt是我們常用的認證方式, jwt由三部分組成: 頭部 (header)
載荷 (payload)
簽證 (signature)
-
頭部
header
jwt的頭部承載兩部分資訊: 宣告型別和宣告加密的演算法, 形如:{ 'typ': 'JWT', 'alg': 'HS256' }
然後將頭部進行base64加密, 變為:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
-
載荷
payload
載荷就是存放有效資訊的地方, 即我們存放資料的地方, 由三部分組成:標準中註冊的宣告
公共的宣告
私有的宣告
標準中註冊的宣告, 即已經預定的標識
名稱 key 描述 iss jwt簽發者 sub jwt所面向的使用者 aud 接收jwt的一方 exp jwt的過期時間,這個過期時間必須要大於簽發時間 nbf 定義在什麼時間之前,該jwt都是不可用的 iat jwt的簽發時間 jti jwt的唯一身份標識,主要用來作為一次性token, 從而回避重放攻擊 公共的宣告
公共的宣告可以新增任何的資訊, 一般新增使用者的相關資訊或其他業務需要的必要資訊私有的宣告
私有宣告是提供者和消費者所共同定義的宣告不建議在JWT中存放敏感資訊, 因為base64是對稱解密的, 意味著該部分資訊可以歸類為明文資訊
假如
payload
資料為:{ "sub": "1234567890", "name": "John Doe", "admin": true }
對其進行base64加密, 得到:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
-
簽證
signature
即對資料的簽證, 由三部分組成:header (base64後的)
payload (base64後的)
secret
這個部分需要
base64
加密後的header
和base64
加密後的payload
連線組成的字串
然後通過header
中宣告的加密方式進行加鹽secret
組合加密,然後就構成了jwt
的第三部分
最終得到jwt: header.payload.signature
訪問時通過指定請求頭Authorization: Bearer token
訪問伺服器.
安裝依賴
安裝生成和校驗 JWT 令牌的庫:
$pip install python-jose[cryptography]
安裝生成hash
密碼的庫:
$pip install passlib[bcrypt]
passlib
一般使用
from passlib.context import CryptContext
# 加密演算法為: bcrypt, 沒有安裝的話需要 pip install bcrypt
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# 獲得hash後的密文
password = "123456"
# hash(self, secret, scheme=None, category=None):
hash_str = pwd_context.hash(password)
print(f"hash password {hash_str}")
# 檢驗密碼是否符合
# verify(self, secret, hash, scheme=None, category=None)
is_verify = pwd_context.verify(password, hash_str)
print(f"is verify? {is_verify}")
FastAPI使用JWT
步驟:
- 生成祕鑰
- 定義加密演算法和令牌過期時間
- 指定雜湊加密演算法和token url
- 呼叫
jwt.encode
生成jwt - 通過依賴注入獲取jwt令牌
你需要先安裝依賴, 如上文
生成安全祕鑰:
$openssl rand -hex 32
09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7
例子:
from datetime import datetime, timedelta
from typing import Optional
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel
app = FastAPI()
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256" # jwt加密演算法
ACCESS_TOKEN_EXPIRE_MINUTES = 30 # 訪問令牌過期分鐘
# 模擬當前使用者資料
fake_users_db = {
"alice": {
"username": "alice",
"full_name": "Alice Wonderson",
"email": "alice@example.com",
"hashed_password": "fakehashedsecret2",
"disabled": True,
},
"john snow": {
"username": "john snow",
"full_name": "John Snow",
"email": "johnsnow@example.com",
"hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
"disabled": False,
}
}
class User(BaseModel):
username: str
email: Optional[str] = None
full_name: Optional[str] = None
disabled: Optional[bool] = None
class UserInDB(User):
hashed_password: str
class Token(BaseModel):
"""返回給使用者的Token"""
access_token: str
token_type: str
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_schema = OAuth2PasswordBearer(tokenUrl="/jwt/token")
def verity_password(plain_password: str, hashed_password: str):
"""對密碼進行校驗"""
return pwd_context.verify(plain_password, hashed_password)
def jwt_get_user(db, username: str):
if username in db:
user_dict = db[username]
return UserInDB(**user_dict)
# 檢驗jwt是否合法
def jwt_authenticate_user(db, username: str, password: str):
# 獲取當前使用者
user = jwt_get_user(db=db, username=username)
if not user:
return False
# 檢驗密碼是否合法
if not verity_password(plain_password=password, hashed_password=user.hashed_password):
return False
return user
# 生成jwt token
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
# data => payload
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
# 標準中註冊的宣告 過期時間
to_encode.update({"exp": expire})
# jwt.encode 的引數
# claims 指定payload
# key 指定signature的加密祕鑰
# algorithm 指定signature的加密演算法
encoded_jwt = jwt.encode(claims=to_encode, key=SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
@app.post("/jwt/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
"""
登入 返回 jwt token
通過依賴注入 OAuth2PasswordRequestForm
獲得 username 和 password
"""
user = jwt_authenticate_user(db=fake_users_db, username=form_data.username, password=form_data.password)
if not user:
raise HTTPException(
status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
async def jwt_get_current_user(token: str = Depends(oauth2_schema)):
"""
獲取當前請求的jwt token
通過 OAuth2PasswordBearer 獲得
"""
credentials_exception = HTTPException(
status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
# 獲取 資料
# decode jwt token
# 得到payload, 即 create_access_token 中的 to_encode
payload = jwt.decode(token=token, key=SECRET_KEY, algorithms=[ALGORITHM])
username = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = jwt_get_user(db=fake_users_db, username=username)
if user is None:
raise credentials_exception
return user
# 獲取 active使用者
async def jwt_get_current_active_user(current_user: User = Depends(jwt_get_current_user)):
if current_user.disabled:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user")
return current_user
@app.get("/jwt/users/me")
async def jwt_read_users_me(current_user: User = Depends(jwt_get_current_active_user)):
return current_user
if __name__ == "__main__":
import uvicorn
uvicorn.run("fastapi-test:app", port=8000, reload=True)
這個例子的 username為john snow
, password為 secret
訪問時通過指定請求頭Authorization: Bearer token
訪問伺服器
session
即使用傳統的session-cookie方式進行認證, FastAPI用於前後端分離的專案居多, 所以不舉例子了
總的來說, 你需要Starlette
的SessionMiddleware
中介軟體, 然後通過request.session
獲取session
關於SessionMiddleware
, 見: SessionMiddleware
第三方SessionMiddleware
庫: starsessions
許可權
即確認, 你能不能訪問?
一般通過依賴注入完成簡單的許可權驗證
例子 (使用者名稱: alice
和john
, 密碼都為123456
):
from datetime import datetime, timedelta
from typing import Optional, List
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel
app = FastAPI()
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256" # jwt加密演算法
ACCESS_TOKEN_EXPIRE_MINUTES = 30 # 訪問令牌過期分鐘
# 模擬當前使用者資料
fake_users_db = {
"alice": {
"username": "alice",
"full_name": "Alice Wonderson",
"email": "alice@example.com",
"hashed_password": "$2b$12$tCUwz5MrDTgnugd3AKBBr..jZpFBRBIc321iBrbmEA3flPaxWmMwO",
"disabled": True,
"role": ["role1"]
},
"john": {
"username": "john",
"full_name": "John",
"email": "johnsnow@example.com",
"hashed_password": "$2b$12$Z5xEfIb1sD487A8IdT3.seUGaBAIVpZtwe5/MXhLu4dKzhaeiF.OC",
"disabled": True,
"role": ["role2"]
}
}
class User(BaseModel):
username: str
email: Optional[str] = None
full_name: Optional[str] = None
disabled: Optional[bool] = None
role: List[str]
class UserInDB(User):
hashed_password: str
class Token(BaseModel):
"""返回給使用者的Token"""
access_token: str
token_type: str
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_schema = OAuth2PasswordBearer(tokenUrl="/jwt/token")
def verity_password(plain_password: str, hashed_password: str):
"""對密碼進行校驗"""
return pwd_context.verify(plain_password, hashed_password)
def jwt_get_user(db, username: str):
if username in db:
user_dict = db[username]
return UserInDB(**user_dict)
# 檢驗使用者名稱和密碼是否合法
def jwt_authenticate_user(db, username: str, password: str):
# 獲取當前使用者
user = jwt_get_user(db=db, username=username)
hash_str = pwd_context.hash(password)
if not user:
return False
# 檢驗密碼是否合法
if not verity_password(plain_password=password, hashed_password=user.hashed_password):
return False
return user
# 生成jwt token
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(claims=to_encode, key=SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
@app.post("/jwt/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
"""
生成jwt token
"""
user = jwt_authenticate_user(db=fake_users_db, username=form_data.username, password=form_data.password)
if not user:
raise HTTPException(
status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
async def jwt_get_current_user(token: str = Depends(oauth2_schema)):
"""
獲取當前已經登陸的使用者資料
"""
credentials_exception = HTTPException(
status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token=token, key=SECRET_KEY, algorithms=[ALGORITHM])
username = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = jwt_get_user(db=fake_users_db, username=username)
if user is None:
raise credentials_exception
return user
async def verify_user(user: UserInDB = Depends(jwt_get_current_user)):
"""
驗證當前使用者是否可以訪問
"""
# 通過判斷角色來判斷是否有無訪問許可權
if "role1" not in user.role:
# 檢驗不可以訪問
raise HTTPException(
status_code=403, detail="Forbidden"
)
# 通過以來注入的方式
@app.get("/items", dependencies=[Depends(verify_user)])
async def get_items():
return {"data": "items"}
if __name__ == "__main__":
import uvicorn
uvicorn.run("fastapi-test:app", port=8000, reload=True)
以上例子中, 使用JWT
認證使用者, 登入alice
可以訪問/items
, 而john
無法訪問/items
Cookie
設定
呼叫
response.set_cookie
方法
不主動返回response時, 需要在引數中指定Response引數, 否則會解析成查詢引數
from fastapi import FastAPI, Response
from fastapi.responses import JSONResponse
app = FastAPI()
# !!!!!!!! 不返回response
@app.post("/cookie-and-object/")
def create_cookie(response: Response):
response.set_cookie(key="fakesession", value="fake-cookie-session-value")
return {"message": "Come to the dark side, we have cookies"}
# !!!!!!!! 返回response
@app.post("/cookie/")
def create_cookie():
content = {"message": "Come to the dark side, we have cookies"}
response = JSONResponse(content=content)
response.set_cookie(key="fakesession", value="fake-cookie-session-value")
return response
if __name__ == '__main__':
import uvicorn
uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)
set_cookie引數:
引數 | 說明 |
---|---|
key | str , cookie 的鍵 |
value | str , cookie 的值 |
max_age | int , cookie 的生命週期, 以秒為單位, 負數或0表示立即丟棄該 cookie |
expires | int , cookie 的過期時間, 以秒為單位 |
path | str , cookie在哪個路徑之下, 預設根路徑 |
domain | str , cookie有效的域 |
secure | bool , 如果使用SSL和HTTPS協議發出請求, cookie只會傳送到伺服器 |
httponly | boo , 無法通過JS的Document.cookie、XMLHttpRequest或請求API訪問cookie |
samesite | str , 為cookie指定相同站點策略, 有效值: lax (預設)、strict 和none |
獲取
Cookie指定要獲取的cookie
注: Cookie是Param的子類, 具有通用的方法, 更多引數見: Param
from typing import Optional
from fastapi import FastAPI, Cookie
app = FastAPI()
@app.get("/items/")
async def read_items(ads_id: Optional[str] = Cookie(None)):
return {"ads_id": ads_id}
if __name__ == '__main__':
import uvicorn
uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)
刪除
呼叫
response.delete_cookie
方法
from fastapi import FastAPI, Response
from fastapi.responses import JSONResponse
app = FastAPI()
# !!!!!!!! 不返回response
@app.post("/cookie-and-object/")
def create_cookie(response: Response):
response.delete_cookie(key="fakesession")
return {"message": "Come to the dark side, we have cookies"}
# !!!!!!!! 返回response
@app.post("/cookie/")
def create_cookie():
content = {"message": "Come to the dark side, we have cookies"}
response = JSONResponse(content=content)
response.delete_cookie(key="fakesession")
return response
if __name__ == '__main__':
import uvicorn
uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)
delete_cookie引數:
引數 | 說明 |
---|---|
key | str , cookie 的鍵 |
path | str , cookie在哪個路徑之下, 預設根路徑 |
domain | str , cookie有效的域 |
delete_cookie原始碼:
def delete_cookie(self, key: str, path: str = "/", domain: str = None) -> None:
self.set_cookie(key, expires=0, max_age=0, path=path, domain=domain)
Header
設定
from fastapi import FastAPI, Response
from fastapi.responses import JSONResponse
app = FastAPI()
# !!!!!!!! 不返回response
@app.get("/headers-and-object/")
def set_headers(response: Response):
response.headers["X-Cat-Dog"] = "alone in the world"
return {"message": "Hello World"}
# !!!!!!!! 返回response
@app.get("/headers/")
def set_headers():
content = {"message": "Hello World"}
headers = {"X-Cat-Dog": "alone in the world", "Content-Language": "en-US"}
return JSONResponse(content=content, headers=headers)
if __name__ == '__main__':
import uvicorn
uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)
獲取
通過Header指定要獲取的header
注: Header是Param的子類, 具有通用的方法, 更多引數見: Param
注意: HTTP Header的名稱使用
-
相連, 不符合python變數命名規則, 故FastAPI會將_
轉化為-
, 如user_agent
==>user-agent
一個Header多個值時, 可以使用List
接收, 如:x_token: Optional[List[str]] = Header(None)
from typing import Optional
from fastapi import FastAPI, Header
app = FastAPI()
@app.get("/items/")
async def read_items(user_agent: Optional[str] = Header(None)):
return {"User-Agent": user_agent}
if __name__ == '__main__':
import uvicorn
uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)
刪除
from fastapi import FastAPI, Response
from fastapi.responses import JSONResponse
app = FastAPI()
# !!!!!!!! 不返回response
@app.get("/headers-and-object/")
def delete_headers(response: Response):
del response.headers["X-Cat-Dog"]
return {"message": "Hello World"}
# !!!!!!!! 返回response
@app.get("/headers/")
def delete_headers():
content = {"message": "Hello World"}
response = JSONResponse(content=content)
del response.headers["X-Cat-Dog"]
return response
if __name__ == '__main__':
import uvicorn
uvicorn.run("test:app", host="127.0.0.1", port=8000, reload=True)
依賴注入
所謂依賴注入就是 我們在執行程式碼過程中要用到其他依賴 或 子函式 時, 可以在函式定義時宣告
理解起來有點抽象, 就算看了官方文件的例子也會讓人覺得費解: 明明不用依賴注入也可以做到, 為什麼額外定義一個"依賴"來使用呢?
按我的理解, 依賴注入有以下好處, 值得我們花費時間學習:
依賴注入主要的作用是解耦、 驗證和提高複用率
我們之前使用FastAPI時的主要步驟就是: 1. 定義一堆引數 2. 將引數在函式中接收 3. 在函式中使用
但是, 假如我們需要替換函式中的處理邏輯呢? 那不是整個函式的一部分要重寫, 假如是一個函式還好, 但很多個函式都要修改的話就比較麻煩了.
而且, 假如我們需要為某個連結新增某些許可權時, 也不能每次都在函式處理吧.
也就是說: 有了依賴注入,原本接受各種引數來構造一個物件,現在只接受是已經例項化的物件就行了。而且還可在例項化的過程中進行驗證, 如何構造就要看依賴注入中的函式實現了。
使用場景:
- 共享業務邏輯 (複用相同的程式碼邏輯)
- 共享資料庫連線
- 實現安全、驗證、角色許可權
- 等...
一般使用
舉幾個例子說明依賴注入的一般使用方式。
資料庫連線例子
使用SQLAlchemy
連線MYSQL
資料庫, 並通過上下文管理協議自動斷開資料庫連線
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy import create_engine
application = APIRouter()
SQLALCHEMY_DATABASE_URL = 'sqlite:///./coronavirus.sqlite3'
engine = create_engine(SQLALCHEMY_DATABASE_URL, encoding='utf-8', echo=True, connect_args={'check_same_thread': False})
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, expire_on_commit=True)
# 一般來說SessionLocal是從其他py檔案中匯入
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# 通過依賴注入獲取資料庫session
@application.post("/data")
def get_data(db: Session = Depends(get_db)):
"""
通過db運算元據庫
"""
return {}
用到了yield的依賴
許可權驗證例子
一般來說是給路徑注入依賴, 詳見: 許可權
後臺任務例子
from fastapi import BackgroundTasks, FastAPI, Depends
from typing import Optional
app = FastAPI()
def write_notification(email: str, message=""):
# 後臺任務的函式為正常的函式
with open("log.txt", mode="w") as email_file:
content = f"notification for {email}: {message}"
email_file.write(content)
def dependency_email(background_tasks: BackgroundTasks, email: Optional[str] = None):
if email:
# 新增到後臺任務
background_tasks.add_task(write_notification, email, message="some notification")
return email
@app.post("/send-notification/")
async def send_notification(email: str = Depends(dependency_email)):
return {"message": "Notification sent in the background"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app="test:app", port=8000, reload=True)
類作為依賴
from fastapi import FastAPI, Depends
from typing import Optional
app = FastAPI()
# 定義類依賴
class CommonQueryParams:
def __init__(self, query: Optional[str] = None, page: int = 1, limit: int = 10):
self.query = query
self.page = page
self.limit = limit
# 使用依賴
@app.get("/")
# 第一種寫法, 比較簡單, 但無法讓ide .出來
# async def index(params=Depends(CommonQueryParams)):
# 第二種寫法,比較複雜, 可以讓ide .出來
# async def index(params: CommonQueryParams = Depends(CommonQueryParams)):
# 第三種寫法,推薦 相當於第二種寫法的縮寫
async def index(params: CommonQueryParams = Depends()):
return {"params": params}
if __name__ == "__main__":
import uvicorn
uvicorn.run("fastapi-test:app", port=8000, reload=True)
子依賴
子依賴, 即一個依賴作為其他依賴的引數。
from fastapi import FastAPI, Depends
from typing import Dict
app = FastAPI()
# 子依賴
async def dependency_query(query: str):
return query
# 在依賴中使用其他依賴
# * : 將後面的引數變成關鍵字引數
async def sub_dependency_item(*, query: str = Depends(dependency_query), limit: int, skip: int):
return {
"query": query,
"limit": limit,
"skip": skip,
}
# 使用依賴
@app.get("/")
def index(params: Dict = Depends(sub_dependency_item)):
data = {
"index": "/"
}
data.update({"params": params})
return data
if __name__ == "__main__":
import uvicorn
uvicorn.run("fastapi-test:app", port=8000)
路徑依賴
單個路徑的依賴, 即給get
/post
等新增依賴
給出官方的例子:
from fastapi import Depends, FastAPI, Header, HTTPException
app = FastAPI()
async def verify_token(x_token: str = Header(...)):
if x_token != "fake-super-secret-token":
raise HTTPException(status_code=400, detail="X-Token header invalid")
async def verify_key(x_key: str = Header(...)):
if x_key != "fake-super-secret-key":
raise HTTPException(status_code=400, detail="X-Key header invalid")
return x_key
@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)])
async def read_items():
return [{"item": "Foo"}, {"item": "Bar"}]
通過dependencies
引數指定,Depends
指定依賴
全域性依賴
所謂的全域性依賴就是給FastAPI
和APIRouter
新增依賴(通過dependencies
引數指定)
from fastapi import FastAPI, Header, Depends, APIRouter
async def global_dependency(x_token: str = Header(..., alias="x-token")):
# 獲取x-token 請求頭 並 列印
print(x_token)
# 方式一 FastAPI dependencies引數
app = FastAPI(dependencies=[Depends(global_dependency)])
# 方式二 APIRouter dependencies引數
music_router = APIRouter(prefix="/music", dependencies=[Depends(global_dependency)])
@music_router.get("/")
def index():
return {"x_token": "1234"}
# 注意 app.include_router 需要在後面, 否則無法匯入之前定義的 路由
app.include_router(music_router)
if __name__ == "__main__":
import uvicorn
uvicorn.run("fastapi-test:app", port=8000)
yield的依賴注入
我們可以通過yield
的依賴,讓其變成上下文管理協議 (利用contextlib.contextmanager
和contextlib.asynccontextmanager
),上下文管理協議可以讓我們更好地管理資源
例子見上文的: 資料庫連線例子
自定義介面文件
FastAPI可以自動生成文件, 你可以訪問連線,
/docs
(Swagger UI)或/redoc
(ReDoc)
文件資訊
本部分內容包括:
- 文件的標題:
title
- 文件的描述:
description
- 文件的版本:
version
- 文件的json路徑:
openapi_url
- 應用的服務條款:
terms_of_service
- 應用的聯絡資訊:
contact
- 應用的許可資訊:
license_info
- 應用的服務列表:
servers
例子:
from fastapi import FastAPI
# 聯絡資訊 資料
contact = {
# 聯絡的名字
"name": "聯絡名字",
# 聯絡url
"url": "http://x-force.example.com/contact/",
# 聯絡的郵箱
"email": "dp@x-force.example.com",
}
# 許可資訊資料
license_info = {
"name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html",
}
# 服務列表資料
# 將渲染成select元素
servers = [
# 單個元素 為option元素
{"url": "https://stag.example.com", "description": "Staging environment"},
{"url": "https://prod.example.com", "description": "Production environment"},
]
app = FastAPI(
# 文件的標題和描述和版本
title="測試API", description="描述資訊資料", version="1.1",
# 文件的json路徑
openapi_url="/myapi.json",
# 文件的服務條款URL
terms_of_service="http://example.com/terms/",
# 文件的聯絡資訊
contact=contact,
# 文件的許可資訊
license_info=license_info,
# 文件的服務列表
servers=servers
)
標籤與標籤後設資料
關於標籤與標籤後設資料如下圖
- 通過FastAPI類的
openapi_tags
指定標籤後設資料 - 通過
APIRouter
類或app.include_router
或app.get/...
的tags引數指定標籤
例子:
from fastapi import FastAPI
tags_metadata = [
{
"name": "使用者",
"description": "操作使用者, **登入**很重要",
},
{
"name": "資料",
"description": "管理資料",
"externalDocs": {
"description": "fastapi文件",
"url": "https://fastapi.tiangolo.com/",
},
},
]
app = FastAPI(
# 文件的標籤後設資料
openapi_tags=tags_metadata)
@app.get("/app/data", tags=["資料"])
async def root():
return {}
@app.get("/app/user", tags=["使用者"])
async def root():
return {}
上面是通過get...
實現的
下面展示在APIRouter
和include_router
中定義tags
from fastapi import FastAPI, APIRouter
tags_metadata = [
{
"name": "使用者",
"description": "操作使用者, **登入**很重要",
},
{
"name": "資料",
"description": "管理資料",
"externalDocs": {
"description": "fastapi文件",
"url": "https://fastapi.tiangolo.com/",
},
},
]
app = FastAPI(
# 文件的標籤後設資料
openapi_tags=tags_metadata)
# ----------- APIRouter 的 tags
user_application = APIRouter(
prefix="/user",
tags=["使用者"]
)
@user_application.get("/")
async def user_index():
return {}
data_application = APIRouter(
prefix="/data",
)
@data_application.get("/")
async def data_index():
return {}
app.include_router(user_application)
# ----------- include_router中指定 tags
app.include_router(data_application, tags=["資料"])
tags不指定時預設為
default
api的概要及描述
包括當前標籤的概要以及標籤的描述資訊
from fastapi import FastAPI, APIRouter
app = FastAPI()
@app.get("/", summary="獲得主頁", description="通過xxx獲取主頁頁面")
async def index():
return {}
@app.get("/home")
async def index_home():
"""
獲取home主頁
"""
return {}
以上程式碼的文件圖片:
未指定summary時, 概要為
函式名.tiltle()
並替換_
未指定description時, 描述訊息為函式的docstring
補充: docstring
的高階用法:
即一些寫法可以被渲染, 主要有以下2個要點
\f
換頁符, 用於截斷OpenAPI 的輸出- 語法為Markdown語法
例子:
from typing import Optional, Set
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
tags: Set[str] = []
@app.post("/items/", response_model=Item, summary="建立一個item")
async def create_item(item: Item):
"""
建立item
- **name**: 每個item必須要有一個name
- **description**: item的描述資訊
- **price**: 必需的引數
- **tax**: 如果沒有tax引數, 你可以省略它
- **tags**: item的標籤
\f
:param item: User input.
"""
return item
以上程式碼的文件圖片
api的請求引數
在FastAPI中引數型別有: 路徑引數 (Path
), 查詢引數 (Query
), 請求體引數 (pydantic
和Body
), 請求頭引數 (Header
), Cookie引數 (Cookie
), Form表單引數 (Form
), 檔案引數 (File
)
它們之間的關係, 見: Params
文件的Parameters
在文件中的位置:
型別為Path
Query
Header
Cookie
會在這裡展示
一般來說我們只需要引數有:
- default
- alias
- description
- example
這些引數有什麼作用, 見下文的Params
from fastapi import FastAPI
from fastapi import Path, Query, Header, Cookie
app = FastAPI()
@app.get("/data/{id}", summary="獲得資料", description="通過id獲取指定值的資料")
async def index(*,
did: str = Path(..., description="資料ID的描述資訊",
example=1, regex=r"\d+", alias="id"),
limit: int = Query(10, description="要取得的資料", example=10),
user_agent: str = Header(..., description="瀏覽器資訊的描述資訊"),
userid: str = Cookie(..., description="cookie的userid"),
):
return {"id": did, "limit": limit, "user-agent": user_agent, "userid": userid}
以上程式碼對應的文件:
文件的Request body
在文件中的位置:
型別為 pydantic模型
Body``Form
File
會在這裡展示
一般來說我們只需要引數有:
- default
- title
- alias
- description
- example
這些引數有什麼作用, 見下文的Params
pydantic模型
Body
, 預設型別為:application/json
假如有Form
或File
,Request body
的型別會變為:application/x-www-form-urlencoded
或multipart/form-data
例子:
from fastapi import FastAPI
from fastapi import Form, File, UploadFile
app = FastAPI()
@app.post("/update")
async def update(
username: str = Form(..., description="使用者名稱的描述資訊", example="lczmx"),
filename: UploadFile = File(..., description="檔案的描述資訊")):
return {"username": username, "filename": filename.filename}
假如只有pydantic
模型和Body
的話:
from fastapi import FastAPI
from fastapi import Body
from pydantic import BaseModel, Field
app = FastAPI()
class QueryItem(BaseModel):
query: str = Field(..., title="查詢字串", description="查詢字串詳細資訊", example="東方")
@app.post("/search")
async def search(
query_item: QueryItem,
query_charset: str = Body("utf-8", title="編碼方式", description="查詢字元的編碼方式的詳細資訊")):
return {"query": query_item.query, "query_charset": query_charset}
你還可以直接在pydantic的Config
類中統一定義example
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class QueryItem(BaseModel):
query: str
charset: str
class Config:
schema_extra = {
"example": {
"query": "東方",
"charset": "utf-8"
}
}
@app.post("/search")
async def search(query_item: QueryItem):
return {"query": query_item.query, "query_charset": query_item.charset}
你亦可以在Body中統一定義example
from fastapi import FastAPI
from fastapi import Body
from pydantic import BaseModel
app = FastAPI()
class QueryItem(BaseModel):
query: str
charset: str
@app.post("/search")
async def search(
query_item: QueryItem = Body(..., example={
"query": "東方",
"charset": "utf-8"
})):
return {"query": query_item.query, "query_charset": query_item.charset}
api的返回值
文件見: OpenAPI Response 物件
在文件中所在的位置
我們可以通過FastAPI
或APIRouter
或app.include_router
或app.get...
的responses
引數指定返回值的資訊 (越後面優先順序越高)
responses
的值為字典, key為狀態碼, value為字典 (key有model
description
content
)
使用
response_model
引數可以為文件新增狀態碼為200的響應模型
使用response_description
引數, 可以為文件新增狀態碼為200的描述資訊
例子:
from fastapi import FastAPI
from pydantic import BaseModel, Field
class ErrorMessage(BaseModel):
code: int = Field(..., title="狀態碼", example=401)
message: str = Field(..., title="錯誤資訊", example="Unauthorized")
class UserData(BaseModel):
username: str = Field(..., title="使用者名稱", example="lczmx")
age: int = Field(..., title="年齡", example=18)
app = FastAPI()
responses = {
200: {
# 使用response_model的模型
"description": "成功響應的描述資訊",
# 右邊的links
"links": {"連結一": {"operationRef": "www.baidu.com", "description": "連結描述資訊"}},
},
401: {
"description": "401的描述資訊",
# 指定響應模型
"model": ErrorMessage
},
404: {
"description": "404的描述資訊",
# 手動定義響應模型
"content": {
"application/json": {
"schema": {
# 全部模型都在 #/components/schemas 下
"$ref": "#/components/schemas/ErrorMessage"
},
# 手動指定example
"example": {"code": "404", "message": "Not Found"}
},
# 其他格式的響應資料 格式如上面一樣
"multipart/form-data": {
}
}, }
}
@app.get("/data", responses=responses, response_model=UserData)
async def root():
return {}
標記已過時api
我們可以通過FastAPI
或APIRouter
或app.include_router
或app.get...
的deprecated
引數標記當前路由是否已經過時
在文件中, 過時的效果如下圖:
程式碼:
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/", tags=["items"])
async def read_items():
return [{"name": "Foo", "price": 42}]
@app.get("/users/", tags=["users"])
async def read_users():
return [{"username": "johndoe"}]
@app.get("/elements/", tags=["items"], deprecated=True)
async def read_elements():
return [{"item_id": "Foo"}]
同樣, 你也可以將一個引數標記為已過時的:
from fastapi import FastAPI
from fastapi import Query
app = FastAPI()
@app.get("/data/")
async def read_data(username: str = Query(..., description="使用者名稱"),
uid: int = Query(..., description="使用者ID", deprecated=True)):
return {"username": username}
從文件中排除api
我們可以通過FastAPI
或APIRouter
或app.include_router
或app.get...
的include_in_schema
引數將當前路由排除出文件
這對於一些只在測試中的介面十分有用, 需要注意的是: 你仍然可以訪問到該介面, 只是在文件中不顯示而已
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/", tags=["items"])
async def read_items():
return [{"name": "Foo", "price": 42}]
@app.get("/users/", tags=["users"])
async def read_users():
return [{"username": "johndoe"}]
# include_in_schema為False時
# 將 /elements/ 排除出文件
@app.get("/elements/", tags=["items"], include_in_schema=False)
async def read_elements():
return [{"item_id": "Foo"}]
依賴注入在文件中
依賴注入, 也會加入到文件中
比如:
from fastapi import FastAPI
from fastapi import Depends, Query
from pydantic import BaseModel
app = FastAPI()
class DataItem(BaseModel):
id: int
username: str
def get_data(data_id: int = Query(..., description="資料的ID", example=1)):
return {"id": data_id, "username": "lczmx"}
@app.get("/items")
async def read_elements(data: DataItem = Depends(get_data)):
return data
後臺任務
例子:
from fastapi import BackgroundTasks, FastAPI
app = FastAPI()
def write_notification(email: str, message=""):
# 後臺任務的函式為正常的函式
with open("log.txt", mode="w") as email_file:
content = f"notification for {email}: {message}"
email_file.write(content)
@app.post("/send-notification/{email}")
async def send_notification(email: str, background_tasks: BackgroundTasks):
# 新增到後臺任務
background_tasks.add_task(write_notification, email, message="some notification")
return {"message": "Notification sent in the background"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app="test:app", host="127.0.0.1", port=8000, reload=True)
你還可以在依賴注入中, 執行後臺任務
from fastapi import BackgroundTasks, FastAPI, Depends
from typing import Optional
app = FastAPI()
def write_notification(email: str, message=""):
# 後臺任務的函式為正常的函式
with open("log.txt", mode="w") as email_file:
content = f"notification for {email}: {message}"
email_file.write(content)
def dependency_email(background_tasks: BackgroundTasks, email: Optional[str] = None):
if email:
# 新增到後臺任務
background_tasks.add_task(write_notification, email, message="some notification")
return email
@app.post("/send-notification/")
async def send_notification(email: str = Depends(dependency_email)):
return {"message": "Notification sent in the background"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app="test:app", port=8000, reload=True)
Params
當我們匯入Path
等類時:即from fastapi import Path
, 返回特殊類的函式 (__init__.py
檔案匯入了) , 本質上是fastapi.params
下的類
Param
Params
類是Pydantic.FieldInfo
類的子類, Path
/Query
/Header
/Cookie
都繼承Params
類, 故而有共同的方法和屬性, 所以寫在一起.
注:
Pydantic.Field
也會返回一個FieldInfo
的例項。
Path
等類也直接返回FieldInfo
的一個子類的物件。還有其他一些你之後會看到的類是 Body 類的子類。
引數 | 型別 | 描述 |
---|---|---|
default |
Any |
預設值, 注意: ... 表示為必須值 |
alias |
str |
別名, 即請求體等的key |
title |
str |
標題名稱, 預設為欄位名稱的title() 方法, 通常只在文件的請求體可用 |
description |
str |
欄位的描述資訊, 用於文件使用 |
const |
bool |
傳入的值是否只能是預設值 |
gt |
float |
傳入的值 大於 指定值 |
ge |
float |
傳入的值 大於等於 指定值 |
lt |
float |
傳入的值 小於 指定值 |
le |
float |
傳入的值 小於等於 指定值 |
min_length |
int |
傳入的值的最小長度 |
max_length |
int |
傳入的值的最大長度 |
regex |
str |
正規表示式驗證 |
example |
Any |
編寫文件中的例子, 見: api的請求引數 |
examples |
Dict[str, Any] |
編寫文件中的例子, 但在FastAPI中不可用, 見: example 和 examples技術細節 |
deprecated |
bool |
True 時, 在文件標記為已棄用, 見: 標記已過時api |
由於Param呼叫的是
pydantic
的建構函式, 所以例項化的引數類似, 所有引數見官網: Field customization
Body
Body
類可用於接收單個請求體引數, 由於請求體編碼可以為application/json
/multipart/form-data
/application/json
。故而分為Form
和File
和Body
三個類.
- Body的media_type:
application/json
- Form的media_type:
application/x-www-form-urlencoded
- File的media_type:
multipart/form-data
Body
特有的引數:embed
, 見: 嵌入單個請求體引數
其他引數和Param相同
WebSocket
WebSocket概述
注意: 這部分內容轉載於: WebSocket 詳解教程
WebSocket 是什麼?
WebSocket是一種網路通訊協議。RFC6455 定義了它的通訊標準。
WebSocket 是 HTML5 開始提供的一種在單個 TCP 連線上進行全雙工通訊的協議。
為什麼需要 WebSocket?
瞭解計算機網路協議的人,應該都知道:HTTP 協議是一種無狀態的、無連線的、單向的應用層協議。它採用了請求/響應模型。通訊請求只能由客戶端發起,服務端對請求做出應答處理。
這種通訊模型有一個弊端:HTTP 協議無法實現伺服器主動向客戶端發起訊息。
這種單向請求的特點,註定瞭如果伺服器有連續的狀態變化,客戶端要獲知就非常麻煩。大多數 Web 應用程式將通過頻繁的非同步 JavaScript 和 XML(AJAX)請求實現長輪詢。輪詢的效率低,非常浪費資源(因為必須不停連線,或者 HTTP 連線始終開啟)。
因此,工程師們一直在思考,有沒有更好的方法。WebSocket 就是這樣發明的。WebSocket 連線允許客戶端和伺服器之間進行全雙工通訊,以便任一方都可以通過建立的連線將資料推送到另一端。WebSocket 只需要建立一次連線,就可以一直保持連線狀態。這相比於輪詢方式的不停建立連線顯然效率要大大提高。
WebSocket 如何工作
Web 瀏覽器和伺服器都必須實現 WebSockets 協議來建立和維護連線。由於 WebSockets 連線長期存在,與典型的 HTTP 連線不同,對伺服器有重要的影響。
基於多執行緒或多程式的伺服器無法適用於 WebSockets,因為它旨在開啟連線,儘可能快地處理請求,然後關閉連線。任何實際的 WebSockets 伺服器端實現都需要一個非同步伺服器。
WebSocket 客戶端
在客戶端,沒有必要為 WebSockets 使用 JavaScript 庫。實現 WebSockets 的 Web 瀏覽器將通過 WebSockets 物件公開所有必需的客戶端功能(主要指支援 Html5 的瀏覽器)。
以下程式碼可以建立一個WebSocket 物件:
var Socket = new WebSocket(url, [protocol] );
- 第一個引數
url
, 指定連線的URL
- 第二個引數
protocol
是可選的,指定了可接受的子協議
WebSocket 屬性
以下是 WebSocket 物件的屬性。假定我們使用了以上程式碼建立了 Socket 物件:
屬性 | 描述 |
---|---|
Socket.readyState |
只讀屬性readyState 表示連線狀態,可以是以下值:0 - 表示連線尚未建立。1 - 表示連線已建立,可以進行通訊。2 - 表示連線正在進行關閉。3 - 表示連線已經關閉或者連線不能開啟。 |
Socket.bufferedAmount |
只讀屬性bufferedAmount 已被send() 放入正在佇列中等待傳輸,但是還沒有發出的 UTF-8 文字位元組數。 |
WebSocket 事件
以下是 WebSocket 物件的相關事件。假定我們使用了以上程式碼建立了 Socket 物件:
事件 | 事件處理程式 | 描述 |
---|---|---|
open |
Socket.onopen |
連線建立時觸發 |
message |
Socket.onmessage |
客戶端接收服務端資料時觸發 |
error |
Socket.onerror |
通訊發生錯誤時觸發 |
close |
Socket.onclose |
連線關閉時觸發 |
WebSocket 方法
以下是 WebSocket 物件的相關方法。假定我們使用了以上程式碼建立了 Socket 物件:
方法 | 描述 |
---|---|
Socket.send() |
使用連線傳送資料 |
Socket.close() |
關閉連線 |
例子:
// 初始化一個 WebSocket 物件
var ws = new WebSocket('ws://localhost:9998/echo');
// 建立 web socket 連線成功觸發事件
ws.onopen = function() {
// 使用 send() 方法傳送資料
ws.send('傳送資料');
alert('資料傳送中...');
};
// 接收服務端資料時觸發事件
ws.onmessage = function(evt) {
var received_msg = evt.data;
alert('資料已接收...');
};
// 斷開 web socket 連線成功觸發事件
ws.onclose = function() {
alert('連線已關閉...');
};
FastAPI中使用WebSocket
在FastAPI中使用fastapi.WebSocket
(內部使用的是starlette.websockets.WebSocket
) 建立一個WebSocket伺服器
簡單例子:
from fastapi import FastAPI, WebSocket
app = FastAPI()
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
while True:
# 接收
data = await websocket.receive_text()
# 傳送
await websocket.send_text(f"接收到文字: {data}")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app="main:app", host="0.0.0.0", port=8000, reload=True)
接收資料
我們可以使用一些任意方法接收資料:
方法 | 描述 |
---|---|
await websocket.receive |
接收資料, 一些方法內部都呼叫這個方法 |
await websocket.send_text(data) |
接收文字資料 |
await websocket.send_bytes(data) |
接收位元組資料 |
await websocket.send_json(data) |
接收文字資料並解析json (格式不正確會報錯), 當mode="binary" 引數時, 接收二進位制資料 |
傳送資料
我們可以使用一些任意方法傳送資料:
方法 | 描述 |
---|---|
await websocket.send(data) |
傳送資料, 一些方法內部都呼叫這個方法 |
await websocket.send_text(data) |
傳送文字資料 |
await websocket.send_bytes(data) |
傳送位元組資料 |
await websocket.send_json(data) |
將資料dumps 併傳送文字資料, 當mode="binary" 引數時, 傳送位元組資料 |
其他方法和屬性
一些常用方法
方法 / 屬性 | 描述 |
---|---|
await websocket.accept(subprotocol=None) |
接收ws請求 |
await websocket.close(code=1000) |
斷開ws請求 |
websocket.headers |
獲取請求頭, 其格式類似於字典 |
websocket.query_params |
獲取請求引數, 其格式類似於字典 |
websocket.path_params |
獲取路徑引數, 其格式類似於字典 |
websocket.url.path |
獲取url的路徑, 如: ws://127.0.0.1:8000/ws ==>/ws |
websocket.url.port |
獲取url的埠, 如: ws://127.0.0.1:8000/ws ==>8000 |
websocket.url.scheme |
獲取url的協議: 如: ws://127.0.0.1:8000/ws ==>ws |
綜合例子
比如實現一個聊天室
from typing import List
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
app = FastAPI()
class ConnectionManager:
"""
用於管理多個ws連線
"""
def __init__(self):
# 存放所有ws連線, 主要由於廣播
self.active_connections: List[WebSocket] = []
async def connect(self, websocket: WebSocket):
"""
建立連線
呼叫accept並新增到active_connections
"""
await websocket.accept()
self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket):
"""
從active_connections移除當前連線
"""
self.active_connections.remove(websocket)
async def send_personal_message(self, message: str, websocket: WebSocket):
"""
為當前ws 傳送資料
"""""
await websocket.send_text(message)
async def broadcast(self, message: str):
"""
為所有ws 傳送資料
"""""
for connection in self.active_connections:
await connection.send_text(message)
manager = ConnectionManager()
# 你同樣可以使用 Path Cookie Header Query Depends Security
@app.websocket("/ws/{client_id}")
async def websocket_endpoint(websocket: WebSocket, client_id: int):
await manager.connect(websocket)
try:
while True:
data = await websocket.receive_text()
await manager.send_personal_message(f"你傳送了: {data}", websocket)
await manager.broadcast(f"連線 #{client_id} 傳送了: {data}")
# 有使用者斷開連線時觸發
except WebSocketDisconnect:
manager.disconnect(websocket)
await manager.broadcast(f"Client #{client_id} left the chat")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app="main:app", host="0.0.0.0", port=8000, reload=True)
執行上面的程式碼, 並在下面建立兩個連線檢視聊天室功能
<!--@html-start-->
<!DOCTYPE html>
<html lang="en">
<head>
<title>Title</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://blog-static.cnblogs.com/files/lczmx/websocket_tool.min.css" rel="stylesheet">
<style>
</style>
</head>
<body>
<div class="well socketBody">
<div class="socketTop">
<div class="socketTopColLeft">
<div class="btn-group socketSelect">
<button type="button" class="btn btn-default dropdown-toggle socketSelectBtn" data-toggle="dropdown"
aria-expanded="false">
<span class="showHeadWS">WS</span>
<span class="caret"> </span>
</button>
<ul class="dropdown-menu socketSelectshadow">
<li><a onclick="showWS('WS')">WS</a></li>
<li><a onclick="showWS('WSS')">WSS</a></li>
</ul>
</div>
</div>
<div class="socketTopColRight">
<input type="text" list="typelist" class="form-control urlInput"
placeholder="請輸入連線地址~ 如: 127.0.0.1:8000/ws"
oninput="inputChange()">
<datalist id="typelist" class="inputDatalist">
<option>127.0.0.1:8000/ws/233333</option>
</datalist>
</div>
</div>
<div class="socketBG well" id="main"></div>
<div class="socketBottom row">
<div class="col-xs-8 socketTextareaBody">
<textarea rows="5" cols="20" class="form-control socketTextarea" placeholder="請輸入傳送資訊~"></textarea>
</div>
<div class="col-xs-2 socketBtnSendBody">
<button type="button" class="btn btn-success socketBtnSend" onclick="sendBtn()">傳送</button>
</div>
<div class="col-xs-2 socketBtnBody">
<button type="button" class="btn btn-primary socketBtn" onclick="connectBtn()">連線</button>
<button type="button" class="btn btn-info socketBtn" onclick="emptyBtn()">清屏</button>
<button type="button" class="btn btn-warning socketBtn" onclick="closeBtn()">斷開</button>
</div>
</div>
<div class="alert alert-danger socketInfoTips" role="alert">...</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js"></script>
<script src="https://blog-static.cnblogs.com/files/lczmx/websocket_tool.min.js"></script>
</body>
</html>
<!--@html-end-->
<!--@css-start-->
/* 已經在link中引入並壓縮了 */
<!--@css-end-->
<!--@javascript-start-->
/* 已經在script中引入並壓縮了 */
<!--@javascript-end-->
<!--@html-start-->
<!DOCTYPE html>
<html lang="en">
<head>
<title>Title</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://blog-static.cnblogs.com/files/lczmx/websocket_tool.min.css" rel="stylesheet">
<style>
</style>
</head>
<body>
<div class="well socketBody">
<div class="socketTop">
<div class="socketTopColLeft">
<div class="btn-group socketSelect">
<button type="button" class="btn btn-default dropdown-toggle socketSelectBtn" data-toggle="dropdown"
aria-expanded="false">
<span class="showHeadWS">WS</span>
<span class="caret"> </span>
</button>
<ul class="dropdown-menu socketSelectshadow">
<li><a onclick="showWS('WS')">WS</a></li>
<li><a onclick="showWS('WSS')">WSS</a></li>
</ul>
</div>
</div>
<div class="socketTopColRight">
<input type="text" list="typelist" class="form-control urlInput"
placeholder="請輸入連線地址~ 如: 127.0.0.1:8000/ws"
oninput="inputChange()">
<datalist id="typelist" class="inputDatalist">
<option>127.0.0.1:8000/ws/666666</option>
</datalist>
</div>
</div>
<div class="socketBG well" id="main"></div>
<div class="socketBottom row">
<div class="col-xs-8 socketTextareaBody">
<textarea rows="5" cols="20" class="form-control socketTextarea" placeholder="請輸入傳送資訊~"></textarea>
</div>
<div class="col-xs-2 socketBtnSendBody">
<button type="button" class="btn btn-success socketBtnSend" onclick="sendBtn()">傳送</button>
</div>
<div class="col-xs-2 socketBtnBody">
<button type="button" class="btn btn-primary socketBtn" onclick="connectBtn()">連線</button>
<button type="button" class="btn btn-info socketBtn" onclick="emptyBtn()">清屏</button>
<button type="button" class="btn btn-warning socketBtn" onclick="closeBtn()">斷開</button>
</div>
</div>
<div class="alert alert-danger socketInfoTips" role="alert">...</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js"></script>
<script src="https://blog-static.cnblogs.com/files/lczmx/websocket_tool.min.js"></script>
</body>
</html>
<!--@html-end-->
<!--@css-start-->
/* 已經在link中引入並壓縮了 */
<!--@css-end-->
<!--@javascript-start-->
/* 已經在script中引入並壓縮了 */
<!--@javascript-end-->
中介軟體
一般中介軟體
帶yield
的依賴的退出部分的程式碼 (finally
) 和 後臺任務 會在中介軟體之後執行
from fastapi import FastAPI, Request
import time
app = FastAPI()
@app.middleware('http')
async def add_process_time_header(request: Request, call_next):
# 處理request
# ...
start_time = time.time()
# call_next 需要await
# 接收request請求做為引數, 返回response
response = await call_next(request)
# 處理response
# ...
process_time = time.time() - start_time
# 新增自定義的以“X-”開頭的請求頭
response.headers['X-Process-Time'] = str(process_time)
return response
@app.get("/")
async def index():
return {"index": "/"}
if __name__ == '__main__':
import uvicorn
uvicorn.run("fastapi-test:app", port=8000, reload=True)
返回資料的響應頭:
content-length: 13
content-type: application/json
date: Wed,29 Dec 2021 14:25:48 GMT
server: uvicorn
x-process-time: 0.0010099411010742188
CORSMiddleware解決跨域問題
用於同源策略, 我們需要特意指定那些源可以跨域請求
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
# 允許跨域請求的源列表
allow_origins=[
"http://127.0.0.1",
"http://127.0.0.1:8080"
],
# 指示跨域請求支援 cookies。預設是 False
# 為True時, allow_origins 不能設定為 ['*'],必須指定源。
allow_credentials=True,
# 允許跨域請求的 HTTP 方法列表
allow_methods=["*"],
# 允許跨域請求的 HTTP 請求頭列表
allow_headers=["*"],
)
@app.get("/")
async def index():
return {"index": "/"}
if __name__ == '__main__':
import uvicorn
uvicorn.run("fastapi-test:app", port=8000, reload=True)
pycharmHttpClient
pycharm HTTP Client
是pycharm
自帶的工具