基于 官方文档 快速学习一波,我想吐槽这官方文档的组织形式就是我想写的形式,问题驱动,类似 cookbook……这让我感觉做笔记的必要性降低了。
FastAPI 是 Python 上的一个现代的,大量利用类型标注的(基于 pydantic),支持异步的,支持依赖注入的,支持插件的 Web 后端框架。因为基于 pydantic,所以它能利用上你的接口的类型标注,自动生成 API 文档,做类型校验……
我只关注我关心的部分,以 Cookbook 的形式明确常见需求和解决方案。
Hello World
https://fastapi.tiangolo.com/zh/tutorial/
安装 FastAPI:
| pip install fastapi[standard]
|
| from fastapi import FastAPI import uvicorn
app = FastAPI()
@app.get("/")
async def hello_world(): return {"message": "Hello World"}
if __name__ == '__main__': uvicorn.run(app, port=8088)
|
这里的@app.get('/')
,称为路径操作(Path Operation)装饰器,hello_world
函数称为路径操作函数,这里的路径即/
,操作即GET
。就像 flask 一样,FastAPI 直接使用 app 的方法来定义路由(FastAPI 称为路径但我还是喜欢叫路由 Route)。
启动,访问localhost:8088
看到 json 响应,访问localhost:8088/docs
看到自动生成的 swagger 文档(显然,这里的响应体的类型无法产生文档,因为我没有显式地标注,即使 IDE 能推断,FastAPI 无法推断这个)。
查询参数,路径变量
路径变量直接在 path 中通过{path_variable_name}
的形式去定义,然后只需在函数参数中定义同名参数即可,FastAPI 知晓如何进行绑定,参数可以标注类型让 FastAPI 进行校验和转换(不然总是str
)。
| @app.get("/items/{item_id}/{msg}") async def read_item(item_id: int, msg): return { "item_id": item_id, 'msg': msg }
|
查询参数是可选的如果给定默认值或者类型包含| None
。
| @app.get("/items") async def read_item(query: str, asc: bool = False, order_by: str | None = None): return {'query': query, 'order_by': order_by, 'asc': asc}
|
查询参数可以使用 Query 进行更复杂的定义,包括指定校验,文档描述……Query 通常以默认值的方式提供,但也可以通过元信息的方式提供。
同理,路径变量可以用 Path 进行更复杂的定义,这里不表。
| @app.get("/items") async def read_item(query: str | None = Query(default=None, min_length=3, max_length=50, description='item 关键字')): return {'query': query}
@app.get("/items_1") async def read_item_1(query: Annotated[str | None, Query(min_length=3, max_length=50, description='item 关键字')] = None): return {'query': query}
|
FastAPI 能自动识别查询参数,路径变量和请求体:
- 路径中声明了相同参数的参数,是路径变量
- 类型是未给定或者(int、float、str、bool 等)单类型的参数,是查询参数
- 类型是 Pydantic 模型的参数,是请求体
- 实践证明,dict,list 类型也是请求体
这里不学习什么用一个 dict 获取所有查询参数等操作,只有最操蛋的业务才需要关心这个(恼)。但确实存在用 pydantic 模型来定义查询参数模型的操作,只需标注它的类型为Annotated[ParamModel, Query()]
即可。
JSON 请求体
https://fastapi.tiangolo.com/zh/tutorial/body/
dict,list 或 Pydantic 模型参数被视为请求体。
| class Idol(BaseModel): name: str age: int
@app.put("/idol") async def add_idol(idol: Idol): return idol
|
如果有多个复杂类型,它们会被视为请求体 json 中的各个 key。
| class Idol(BaseModel): name: str age: int
@app.put("/idol") async def add_idol(idol: Idol, friends: list[Idol]): return {'idol': idol, 'friends': friends}
|
同上面的 Query 和 Path,FastAPI 提供了 Body 类型,允许添加元信息云云,如果要使用单类型作为请求体或者请求体中特定 key 的类型(它默认会被视为查询参数),必须使用 Body。
要使用表单,需要安装下面的依赖(我猜测fastapi[standard]
自带了它):
| pip install python-multipart
|
同上面的 Query,Path,Body,FastAPI 提供 Form 表示表单数据,File 表示文件数据,其中 Form 继承自 Body,File 继承自 Form(……为何?)。显然——只要使用了 Form,请求体类型就会是 x-www-urlencoded,只要使用了 File,请求体类型就会是 form-data。这里不详细学习,只给示例。
| class FormData(BaseModel): username: str password: str
@app.post("/login")
async def login(form: FormData = Form()): return form @app.post("/upload")
async def upload(override_name: str = Form(default=''), file: UploadFile = File()): return file.filename
|
https://fastapi.tiangolo.com/zh/tutorial/header-params/
就如同上面的 Query,Path……(我不报菜名了),Header 也提供了一个类供你绑定到参数上,FastAPI 会处理名称的转换,将 header 中的中划线替换为下划线。
| @app.get("/items/") async def read_items(user_agent: str = Header(None), x_token: str = Header(None)): return {"User-Agent": user_agent, "X-Token": x_token}
|
自定义响应
这里说的是通过 return 返回的正常响应。
在路径操作装饰器中可以指定响应的状态码等。
| @app.post("/items/", status_code=201) async def create_item(name: str): return {"name": name}
|
这里主要关注如何提供非 json 的响应,如文件下载,图像。
FastAPI 提供了诸多类来自定义响应(显然会自动设置 header),如HTMLResponse
,FileResponse
,以及原始的Response
供你做完全的自定义。当然,你也可以用Request
(给参数标注这个类型,request: Request
)来获取原始 request 来获取所有 header。
异常处理
https://fastapi.tiangolo.com/zh/tutorial/handling-errors/
如果代码出现异常要告知前端,应当 raise HTTPException,这里可以手动指定 status_code 和要返回的 detail,也可以指定响应的 header 等。可以说 raise HTTPException(...)
就等价于 return Fail(...)
,只不过异常是传递性的。
| @app.get("/items/{item_id}") async def read_item(item_id: str): if item_id not in items: raise HTTPException(status_code=404, detail="Item not found") return {"item": items[item_id]}
|
FastAPI 支持全局异常处理器,且支持复用和覆盖,但这里不表,用到的时候 现查 即可。
静态文件
https://fastapi.tiangolo.com/zh/tutorial/static-files/
这样:
| app.mount("/static", StaticFiles(directory="static"), name="static")
|
它其实不止是处理静态文件,实际上它是一个更广泛的东西——去“挂载”一个“子应用”到特定的路径下,这里不表。
按文件划分接口
https://fastapi.tiangolo.com/zh/tutorial/bigger-applications/
不能把所有东西都塞到main.py
吧?按文件划分接口是常见需求,使用APIRouter
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| from fastapi import FastAPI, APIRouter import fastapi import uvicorn
app = FastAPI()
router_user = APIRouter(prefix='/user') @router_user.get('/') def hello(): return 'user module'
router_sys = APIRouter(prefix='/sys') @router_sys.get('/') def world(): return 'sys module'
app.include_router(router_user) app.include_router(router_sys)
if __name__ == '__main__': uvicorn.run(app, port=8088)
|
依赖注入
https://fastapi.tiangolo.com/zh/tutorial/dependencies/
FastAPI 的依赖注入并非像 Spring 那样提前把所有 Bean 给你创建好放到一个全局的容器中,然后后面反复用,而是每次来新请求的时候,都会重新去创建依赖。可以说,依赖是请求作用域的(request-scoped)。以及,在引用其他依赖时,可以配置是否使用缓存(默认为 True),如果使用且这个依赖已经被创建过,则复用它,就是说同一个依赖的实例可能被多个子依赖同时使用。(简单理解这个use_cache
,就是同一个请求中这一个依赖是否会被执行多次)
以 FastAPI 的角度出发,或许可以说就连上面的 Header,Body,Query 等 HTTP 请求中的东西也可以说是依赖了,有趣的心智模型。是时候把我心里的 Spring MVC 的那一套东西丢掉了。
显然——依赖的创建不能太昂贵,比如你不应当在依赖里创建数据库连接(而是从现成的连接池中取连接)……大概吧。
依赖本质来说有两种情况——你需要它的返回值,如数据库连接,或者鉴权并获取用户信息;你不需要它的返回值,而只做一些副作用,如鉴权,限流,在请求前后记录日志。后者会让你疑惑为何不直接使用中间件——中间件不能利用依赖,而副作用型依赖可以。
返回值型依赖
返回值型依赖通过函数参数注入,通过 Depends 去注入。
一个可能常见的需求就是获取当前用户信息,这里就适合使用依赖注入。这里同时演示了嵌套的依赖——get_me 依赖 get_current_user 依赖 UserService 依赖 foo。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| async def foo(): return 'hello'
class UserService: def __init__(self, foo: str = Depends(foo)): print(foo)
def get_current_user(self): if ...: return None return { 'username': 'naer' }
async def get_current_user(user_service: UserService = Depends()): if user := user_service.get_current_user(): return user raise HTTPException(401, 'not logined')
@app.get("/me") async def get_me(current_user: dict = Depends(get_current_user)): return current_user
|
副作用型依赖
副作用型依赖,典型情况就是验证用户是否登陆,以及记录请求日志等……
副作用型依赖就不再使用参数去注入了——而是在装饰器,或者特定 APIRouter,或者整个 app 去注入。
下面是各种依赖注入方式,这里的依赖提供一个计时功能,这里同时演示了生成器函数作为依赖——这允许提供“清理”功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| async def timer(request: Request): url = request.url start_time = time() yield end_time = time() print(f'接口 {url}: {end_time-start_time:.4f} s')
app = FastAPI(dependencies=[Depends(timer)])
some_router = APIRouter(dependencies=[Depends(timer)])
app.include_router(some_router, dependencies=[Depends(timer)])
@app.get("/", dependencies=[Depends(timer)]) async def hello(): await asyncio.sleep(1) return 'Hello, World'
|
考虑到副作用型依赖在我的需求下大概能完全地替代中间件,中间件就不研究了。
Basic Auth 鉴权
直接贴代码,这里使用了 FastAPI 的 security 模块,同时也展示了有状态的依赖是怎么一回事——我给 Depends 的不是一个函数而是一个实例(明显需要覆盖__call__
方法)。
| app = FastAPI() security = HTTPBasic()
def get_current_username(credentials: HTTPBasicCredentials = Depends(security)): correct_username = "naer" correct_password = "aaaaaa" if credentials.username != correct_username or credentials.password != correct_password: raise HTTPException(status_code=401, detail="Incorrect credentials") return credentials.username
@app.get("/") async def hello(user_name: str = Depends(get_current_username)): await asyncio.sleep(1) return f'Hello, {user_name}'
|
WebSocket
https://fastapi.tiangolo.com/zh/advanced/websockets/
WebSocket 的编写在协程编程下还是挺方便的。
安装下面依赖:
直接看代码吧,来自官方的示例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| from fastapi import FastAPI, WebSocket from fastapi.responses import HTMLResponse
app = FastAPI()
html = """ <!DOCTYPE html> <html> <head> <title>Chat</title> </head> <body> <h1>WebSocket Chat</h1> <form action="" onsubmit="sendMessage(event)"> <input type="text" id="messageText" autocomplete="off"/> <button>Send</button> </form> <ul id='messages'> </ul> <script> var ws = new WebSocket("ws://localhost:8000/ws"); ws.onmessage = function(event) { var messages = document.getElementById('messages') var message = document.createElement('li') var content = document.createTextNode(event.data) message.appendChild(content) messages.appendChild(message) }; function sendMessage(event) { var input = document.getElementById("messageText") ws.send(input.value) input.value = '' event.preventDefault() } </script> </body> </html> """
@app.get("/") async def get(): return HTMLResponse(html)
@app.websocket("/ws") async def websocket_endpoint(websocket: WebSocket): await websocket.accept() while True: data = await websocket.receive_text() await websocket.send_text(f"Message text was: {data}")
|
生命周期
https://fastapi.tiangolo.com/zh/advanced/events/
在项目启动、关闭时执行特定操作是常见需求。FastAPI 倒也支持像回调一样去定义生命周期钩子,但更倾向使用 app 的 lifespan 参数去做操作(这会覆盖掉生命周期钩子)。lifespan 接受一个异步上下文处理器。
见代码:
| @asynccontextmanager async def lifespan(app: FastAPI): print('hello') yield print('bye')
app = FastAPI(lifespan=lifespan)
|
and…That’s it.