需求场景: 假设现在我们有个图书网站,需要客户端在请求头上携带有效的token,才可以获取图书数据,那FastAPI该如何获取客户端传过来的请求头参数呢?
示例1:FastAPI获取,指定请求头的值(比如,请求头的key 是token
)
1 2 3 4 5 6 7 8 9 10 11 12 from fastapi import FastAPI, Header app = FastAPI() books = {i: f"图书{i} " for i in range (10 )}@app.get("/books/{id}" ) def get_book_by_id (id : int , token: str = Header( ) ): if token == "this-is-token" : return books.get(id ) else : return "invalid token"
1、Header和Path() Query()类似,也具有基本的请求头参数校验和API文档设置
2、如果定义的参数是请求头参数,则必须使用Header() 否则FastAPI默认会把这个参数识别的查询参数
3-2 使用请求体提交数据【重点】 向服务器提交数据,可以通过之前咱们学的路径参数、查询参数、也可以在请求体中携带数据(推荐用法)
推荐使用请求体向服务器提交数据的原因:
路径参数和查询参数携带的数据量有限
路径参数和查询参数携带数据不安全
那我们该如何使用请求体向服务端提交数据呢?一般有两种方式:【二选一】
实现方式: 使用Pydantic 来接收请求体数据
第一步:继承Pydantic的BaseModel
,定义模型类User,再类钟定义两个和请求体字段同名的类数学并做类型提示
第二步:再路径函数钟定义形参user ,设置类型未User
第三步:通过对象user
属性username
和password
获取请求体字段的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from fastapi import FastAPIfrom pydantic import BaseModel app = FastAPI()class User (BaseModel ): username: str password: str @app.post("/login" ) def login (user: User ): return { "username" : user.username, "password" : user.password }
这样可以帮我们获取到请求体参数并赋值到参数user
,同时:
1、按照json格式来读取请求体数据
2、把类型转换且进行类型校验(类型必须符合User格式)
3、把请求体中每个字段的值赋值到模型类对象的每个属性(赋值到User类,基础自基础模型类)
4、API文档可以看到类型提示
结论 :
如果请求体参数是JSON格式,就通过继承Pydantic的 BaseModel
的方式来获取数据
BaseModel
的子类中定义的类属性是用来匹配请求体对应的字段值,所以名字包保持一致,字段的顺序没有要求
只要路径函数式中形参的类型是BaseModel
的子类,则FastAPI会默认会把请求体数据保存在这个变量中
定义的模型类的属性,可以有默认值
3-4 多请求体 入参 不常用 原理是一样的,就是多定义一个模型而已,不常用拉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class User (BaseModel ): username: str password: str class Item (BaseModel ): name: str description: typing.Optional [str ] = None price: float tax: typing.Optional [float ] = None @app.post("/login_double" ) def post_login_double (user: User, item: Item ): return {"user" : { "username" : user.username, "password" : user.password, },"item" : { "name" : item.name, "description" : item.description, "price" : item.price, "tax" : item.tax,}}
3-5 使用Body接收请求体数据 当你需要和其他入参一起发出请求的时候,比如说
1 2 3 4 5 6 7 8 9 10 11 12 13 { "item" : { "name" : "Foo" , "description" : "The pretender" , "price" : 42.0 , "tax" : 3.2 }, "user" : { "username" : "liuxu" , "password" : "liuxu" }, "importance" : 5 }
上面的两种情况,直接使用继承Pydantic的BaseModel定义模型类的方式行不通,需要借助Body()
函数来帮忙
示例1:使用Body()获取请求体中单一字段变量,比如需求1中的 "importance": 5
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class User (BaseModel ): username: int password: str class Item (BaseModel ): name: str description: typing.Optional [str ] = None price: float tax: typing.Optional [float ] = None @app.post("/login" ) def login (user: User, item: Item, importance: int = Body( ) ): return importance
1、 Body()函数的室友和前面的Path() Query(),Header()基本一致
2、如果你仅仅需要从请求体中获取importance,其他字段不需要,可直接:def login(importance: int = Body()):
3.6 请求体使用Body校验
Body和前面接收的 Path、Query、Header一样,都可以做数值校验和字符串的基本校验
只不过Body校验的是请求体中的单一数据 ,比如请求体中只有一个数字或者一个字符串才可以
除此之外,还可以设置请求体的类型,默认是application/json
,即JSON格式
1 2 3 4 5 6 7 8 from fastapi import FastAPI, Body app = FastAPI()@app.post("/login" ) def login (name: str = Body(min_length=3 ), age: int = Body(ge=18 ) ): return {"name" : name, "age" : age}
3.7 模型类嵌套 客户端传过来的请求体数据结构是复杂的嵌套结构,比如下面这种,此时后端该如何接收数据呢?
需求3:嵌套列表中的模型类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { "name" : "Foo" , "description" : "The pretender" , "price" : 42.0 , "tax" : 3.2 , "images" : [ { "url" : "http://example.com/baz.jpg" , "name" : "The Foo live" } , { "url" : "http://example.com/dave.jpg" , "name" : "The Baz" } ] }
示例3:解决 嵌套列表中的模型类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from typing import List , Optional from fastapi import FastAPIfrom pydantic import BaseModel app = FastAPI()class Image (BaseModel ): url: str name: str class Item (BaseModel ): name: str price: float images: Optional [List [Image]] = None @app.post("/item" ) def create_item (item: Item ): return item
3.8 请求体字段使用Filed校验 请求体的每个字段可以单独做校验吗,比如:
name
的长度最少是3,
price
不少于10
`tags字段限制元素不重复,限制元素的个数等等
1 2 3 4 5 6 7 { "name" : "Foo" , "description" : "The pretender" , "price" : 42.0 , "tax" : 3.2 , "tags" : [ "rock" , "metal" , "bar" ] }
示例1:使用 Field
来校验每个请求体字段,注意,Field
不是从FastAPI导入的,而是直接从Pydantic导入的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from typing import Optional , List from fastapi import FastAPIfrom pydantic import BaseModel, Field app = FastAPI()class Item (BaseModel ): name: str = Field(min_length=5 ) description: Optional [str ] = Field(default=None , max_length=300 ) price: float = Field(gt=0 , multiple_of=2 ) tax: List [str ] = Field(min_items=2 , max_items=5 , unique_items=True )@app.post("/item" ) def create_item (item: Item ): return item
校验参数总结:
1 2 3 4 5 6 7 8 gt/ge/lt/le: min_length/max_length min_items/max_items unique_items multiple_of max_digits decimal_places
3.9 总结获取请求体数据的3种方式 使用Pydantic 【主要】
定义模型类BaseModle,和请求体中的字段保持一致,定义模型类的属性并做类型提示 (支持类型校验和类型转换)
使用Body()
有个特殊的请求体格式,使用Body()显示获取,比如,单一字段的值,内嵌字段的值
可以与其他参数一起使用:Body()
函数可以与其他参数(如路径参数、查询参数等)一起使用,以便更灵活地定义请求处理函数的参数。
支持更多的参数配置:Body()
函数支持传递更多参数配置,如数据类型、默认值等。
使用Python的字典类型和列表类型
定义在路径函数内的特殊字段会默认被视为请求体参数,比如列表,集合,元祖,字典(容器型数据)
1 2 3 @app.post("/item" ) def create_item (item: typing.Dict [str , str ] ): return item
也可以通过@dataclasses.dataclass类装饰器转装饰成数据类
1 2 3 4 5 6 7 8 9 @dataclasses.dataclass class User : username: str password: str @app.post("/item" ) def create_item (user: User ): return user
3.10 给请求体提供范例 提供范例后,再阅读API文档的时候更方便
FastAPI提供了多种为请求体提供范例的方式
示例1:在模型类中定义 Config
类
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 from typing import Union from fastapi import FastAPIfrom pydantic import BaseModel app = FastAPI()class Item (BaseModel ): name: str description: Union [str , None ] = None price: float tax: Union [float , None ] = None class Config : schema_extra = { "example" : { "name" : "Foo" , "description" : "A very nice Item" , "price" : 35.4 , "tax" : 3.2 , } }@app.post("/item" ) def create_item (item: Item ): return item
示例2:使用 Field(example="xxx")
为每个字段提供例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from typing import Union from fastapi import FastAPIfrom pydantic import BaseModel, Field app = FastAPI()class Item (BaseModel ): name: str = Field(example="Foo" ) description: Union [str , None ] = Field(default=None , example="A very nice Item" ) price: float = Field(example=35.4 ) tax: Union [float , None ] = Field(default=None , example=3.2 )@app.post("/item" ) def create_item (item: Item ): return item
示例3:使用 Body(example="一个例子", examples="多组例子")
,这种方式也适用:Path\Query\Header
等等 【不常用】
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 typing import Union from fastapi import Body, FastAPIfrom pydantic import BaseModel app = FastAPI()class Item (BaseModel ): name: str description: Union [str , None ] = None price: float tax: Union [float , None ] = None @app.post("/item" ) def create_item ( *, item: Item = Body( examples={ "normal" : { "summary" : "A normal example" , "description" : "A **normal** item works correctly." , "value" : { "name" : "Foo" , "description" : "A very nice Item" , "price" : 35.4 , "tax" : 3.2 , }, }, "converted" : { "summary" : "An example with converted data" , "description" : "FastAPI can convert price `strings` to actual `numbers` automatically" , "value" : { "name" : "Bar" , "price" : "35.4" , }, }, "invalid" : { "summary" : "Invalid data is rejected with an error" , "value" : { "name" : "Baz" , "price" : "thirty five point four" , }, }, }, ), ): return item
本章学习总结,
主要是在与对请求头Header()和请求体Body()的定义
这里的Header
的定义只是一个简单的定义,还没有注入依赖项(token令牌等)
请求体的话主要使用Pydantic
模型类BoseModel定义,在路径函数上做类型提示更方便
4-1 设置响应状态码 示例1:直接给status_code字段一个状态码
1 2 3 4 5 6 @app.post("/login" , status_code=200 ) def login (user: User ): return { "username" : user.username, "password" : user.password }
示例3:status_code使用常量
1 2 3 4 from fastapi import FastAPI, Path, Header,status@app.post("/login" , status_code=status.HTTP_200_OK ) def login (): pass
4-2 设置响应头 有的时候,在响应客户端请求的时候,需要在响应头中给客户端返回一些数据,此时该如何设置响应头呢?
示例1:在路径函数中使用 Response
1 2 3 4 5 6 7 8 9 from fastapi import FastAPI, Response app = FastAPI()@app.post("/login" ) def login (response: Response ): response.headers["x-jwt-token" ] = "this_is_jwt_token" return {"message" : "Hello World" }
示例2:直接使用返回一个Response
,比如使用FastAPI的默认响应方式JSONResponse
1 2 3 4 5 6 7 8 9 10 11 12 13 from fastapi import FastAPIfrom fastapi.responses import JSONResponse app = FastAPI()@app.post("/login" ) def login (): response = JSONResponse( content={"message" : "Hello World" }, headers={"x-jwt-token" : "this_is_jwt_token" }, ) return response
扩展:使用返回Response的方式,也可以设置响应状态码,手动设置响应体,操作cookie等等
4-3 响应体数据 FastAPI响应数据时,默认响应是json的格式,具体使用的响应类是 JSONResponse
举两个响应示例:一个Pydantic
模型类,一个Response
对象或 Response
的子类
示例3:响应 基于Pydantic的模型类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from fastapi import FastAPIfrom pydantic import BaseModelfrom fastapi.responses import JSONResponse app = FastAPI(default_response_class=JSONResponse) class User (BaseModel ): username: str password: str @app.post("/login" ) def login (user: User ): return user
示例4:直接响应一个 Response
对象或 Response
的子类
1 2 3 4 5 6 7 8 9 10 11 12 13 from fastapi import FastAPI, Responseimport json app = FastAPI()@app.get("/books" ) def get_books (): response = Response( content=json.dumps([{"id" : i, "name" : f"图书{i} " } for i in range (1 , 11 )]), media_type="application/json" ) return response
4-4 使用响应模型response_model 需求场景
需求1:用户注册接口(需要用户名和密码),后端完成注册后一般会将用户信息返回,但此时不能返回用户密码,该如何操作?
需求2:想让接口返回的字段按照一定的规范来,有数据格式校验和转换等功能,该怎么办?
需求3:API文档展示响应字段和类型
FastAPI提供了响应模型 (response_model
)的概念,就像定义模型类用来接收请求体数据,响应模型用来处理响应数据。
利用响应模型,可以实现返回字段的动态过滤,类型校验和类型转换等。
1 2 3 4 5 6 7 8 9 10 11 12 class User (BaseModel ): username: str password: str email: str class UserOut (BaseModel ): username: str email: str @app.post("/registe" , response_model=UserOut ) def registe (user: User ): return user
示例3:使用response_model做类型校验和类型转换
1 2 3 4 5 6 7 8 9 10 import typingfrom fastapi import FastAPI app = FastAPI()@app.post("/demo" , response_model=typing.Dict [str , int ] ) def demo (): return {"code" : 200 , "value" : "100" }
4-5 用于过滤响应数据的五个参数 很多时候,我们需要对响应模型中的某些字段做一个过滤操作,比如,
没有赋值的字段过滤掉
使用默认值的字段过滤掉
指定某些字段过滤掉
保留指定的字段等等
FastAPI提供了5个配合响应模型一块使用的字段:
response_model_include,只展示选中的字段
response_model_exclude,选中的字段不展示
response_model_exclude_unset,过滤掉没有赋值的字段
response_model_exclude_defaults,过滤掉使用默认值的字段
response_model_exclude_none,排序没有值的字段
1 2 3 4 5 6 7 8 9 10 11 @app.get( "/items/{name}" , response_model=Item, response_model_include={"name" }, response_model_exclude={"tax" }, response_model_exclude_unset=True , response_model_exclude_defaults=True , response_model_exclude_none=True , )def read_item_name (name: str ): return items[name]
4-6 Pydantic模型类对象的dict方法 这个方法好像弃用了
4-7 jsonable_encoder【可能是重点】 客户端向服务端提交的数据需要永久存储保存数据库,那么此时就需要保证数据都是可序列化的,如何保证待序列化的数据都是可序列化的呢?
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 from datetime import datetimefrom typing import Union import jsonfrom fastapi import FastAPIfrom fastapi.encoders import jsonable_encoderfrom pydantic import BaseModel fake_db = {}class Item (BaseModel ): title: str timestamp: datetime description: Union [str , None ] = None app = FastAPI()@app.post("/item" ) def create_item (item: Item ): json_compatible_item_data = jsonable_encoder(item) print (type (json_compatible_item_data)) print (json_compatible_item_data) json.dumps(json_compatible_item_data) return item
4-8 响应部分总结
5 、阶段性总结 关于博客的增删改查练习
5个请求方式的使用规范
错误处理
Restful风格api设计
RESTful是Web API接口的设计规范风格,这种风格的理念认为后端开发任务就是提供数据的,对外提供的是数据资源的访问接口,尤其适用于前后端分离的应用模式中。所以在定义接口时,客户端访问的URL路径就表示这种要操作的数据资源。
任何一个框架都可以实现符合restful规范的API接口。
RESTful十条规范:
1 数据的安全保障:url链接一般都采用https协议进行传输
2 接口特征表现,一看就知道是个api接口
3 多数据版本共存,在url链接中标识数据版本
!!!4 数据即是资源,均使用名词(可复数)
!!!5 资源操作由请求方式决定(method)
操作资源一般都会涉及到增删改查,使用请求方式确定操作类型
6 过滤,通过在url上传参的形式传递搜索条件,或者说查询参数(query_params)
7 响应状态码
8 错误处理 9 返回结果,针对不同操作,服务器向用户返回的结果应该符合以下规范 GET /collection:返回资源对象的列表(数组) GET /collection/resource:返回单个资源对象 POST /collection:返回新生成的资源对象 PUT /collection/resource:返回完整的资源对象 PATCH /collection/resource:返回完整的资源对象 DELETE /collection/resource:返回一个空文档 10 需要url请求的资源需要访问资源的请求链接
Hypermedia API,RESTful API最好做到Hypermedia,
即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么
1 2 3 4 5 6 7 8 9 10 >{ "status" : 0 , "msg" : "ok" , "results" : [ { "name" : "肯德基(罗餐厅)" , "img" : "https://image.baidu.com/kfc/001.png" } ] >}
源码示例:模拟RESTFul风格的api
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 import typingfrom fastapi import FastAPI, HTTPException, statusfrom fastapi.encoders import jsonable_encoderfrom pydantic import BaseModel app = FastAPI(title="Blog CRUD" ) blogs = { 1 : { "id" : 1 , "title" : "blog1" , "body" : "this is blog1" , "desc" : "desc" }, 2 : { "id" : 2 , "title" : "blog2" , "body" : "this is blog2" , "desc" : "desc" } }class Blog (BaseModel ): title: typing.Optional [str ] = None body: typing.Optional [str ] = None desc: str @app.get("/blogs" , tags=["Blog" ] ) def get_blogs (page: int = 1 , size: int = 10 ): blogs_list = list (blogs.values()) return blogs_list[(page - 1 ) * size:page * size]@app.get("/blog" , tags=["Blog" ] ) def get_blog_by_id (blog_id: int ): blog = blogs.get(blog_id) if not blog: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Not found the blog with id: {blog_id} " ) return blog@app.post("/blog" , tags=["Blog" ] ) def create_blog (blog: Blog ): blog_id = len (blogs) + 1 blogs[blog_id] = {"id" : blog_id, **jsonable_encoder(blog)} return blogs[blog_id]@app.put("/blog" , tags=["Blog" ] ) def update_blog (blog_id: int , blog: Blog ): to_update_blog = blogs.get(blog_id) if not to_update_blog: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Not found the blog with id: {blog_id} " ) to_update_blog.update(jsonable_encoder(blog)) blogs[blog_id] = to_update_blog return to_update_blog@app.patch("/blog" , tags=["Blog" ] ) def update_blog2 (blog_id: int , blog: Blog ): to_update_blog = blogs.get(blog_id) if not to_update_blog: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Not found the blog with id: {blog_id} " ) to_update_blog.update(**jsonable_encoder(blog, exclude_unset=True )) blogs[blog_id] = to_update_blog return to_update_blog@app.delete("/blog" , tags=["Blog" ] ) def delete_blog (blog_id: int ): if not blogs.get(blog_id): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Not found the blog with id: {blog_id} " ) return blogs.pop(blog_id, None )