(转)从零开始学FastAPI(1)-简明的部分官方文档抽取学习篇
前言之前线上业务一直是使用Bottle和Flask两个框架来编写API接口。bottle和Flask它们都是一些同步的框架,而支持异步框架的最新晋的一个非常不错的角:之前在青南大大的文章里也了解到了它的奇异之处,处于对异步框架的学习兴趣,我还是决定也开始使用Fastapi试一试,虽然…
前言
之前线上业务一直是使用Bottle和Flask两个框架来编写API接口。bottle和Flask它们都是一些同步的框架,而支持异步框架的最新晋的一个非常不错的角:之前在青南大大的文章里也了解到了它的奇异之处,处于对异步框架的学习兴趣,我还是决定也开始使用Fastapi试一试,虽然此前也用过Tornado,但是也仅限于使用同步的框架。
本来想上手试一试看看sanic,处于猎奇心,还是鼓捣以下这个Fastapi!
学一个新的框架最好的方式当然就是框架笨的提供的官方文档了!
文献资料: fastapi.tiangolo.com
参考资料:www.jianshu.com/p/94710ed35…
开始撸码
注意事项,因为FastAPI仅支持Python3.6+的API,所以需要再Python3.6+的环境进行学习实践!
说明因为我是再winddos环境下的,仅用于开放的调试,真正到线上的,肯定都是在linux环境下运行才能到达AWGI的相关的极致的性能!
1:依赖库的安装
或者:
FastAPI - 是一个现代的,快速(高性能)python web框架
pip install fastapi
uvicorn - 主要用于加载和提供应用程序的服务器.
pip install uvicorn
2:第一个Hello World
1
2
3
4
5
6
7
8
9
10
11
12
|
import uvicorn from fastapi import FastAPI app = FastAPI() @app.get("/") async def root(): return {"message": "Hello World"} if __name__ == '__main__': uvicorn.run(app=app) 复制代码 |
深入到uvicorn.run(app=app)方法里面,看到一个:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
def run(app, **kwargs): config = Config(app, **kwargs) server = Server(config=config) if (config.reload or config.workers > 1) and not isinstance(app, str): logger = logging.getLogger("uvicorn.error") logger.warn( "You must pass the application as an import string to enable 'reload' or 'workers'." ) sys.exit(1) if config.should_reload: sock = config.bind_socket() supervisor = StatReload(config, target=server.run, sockets=[sock]) supervisor.run() elif config.workers > 1: sock = config.bind_socket() supervisor = Multiprocess(config, target=server.run, sockets=[sock]) supervisor.run() else: server.run() 复制代码 |
再深入到 config = Config(app, **kwargs)里面,就看到一些很多的相关的配置信息项:
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
|
class Config: def __init__( self, app, host="127.0.0.1", port=8000, uds=None, fd=None, loop="auto", http="auto", ws="auto", lifespan="auto", env_file=None, log_config=LOGGING_CONFIG, log_level=None, access_log=True, use_colors=None, interface="auto", debug=False, reload=False, reload_dirs=None, workers=None, proxy_headers=True, forwarded_allow_ips=None, root_path="", limit_concurrency=None, limit_max_requests=None, backlog=2048, timeout_keep_alive=5, timeout_notify=30, callback_notify=None, ssl_keyfile=None, ssl_certfile=None, ssl_version=SSL_PROTOCOL_VERSION, ssl_cert_reqs=ssl.CERT_NONE, ssl_ca_certs=None, ssl_ciphers="TLSv1", headers=None, ): 复制代码 |
所以还可以添加的参数可以看上面的几个配置的选项的信息来填:
于是乎还可以修改为:
1
2
3
4
5
6
7
8
9
10
11
12
|
import uvicorn from fastapi import FastAPI app = FastAPI() @app.get("/") async def root(): return {"message": "Hello 454533343433World"} if __name__ == '__main__': uvicorn.run(app=app, host="127.0.0.1", port=8000, reload=True, debug=True) 复制代码 |
发现本来想热更新代码,结果呐?有告警信息提示:
1
2
|
WARNING: You must pass the application as an import string to enable 'reload' or 'workers'. 复制代码 |
翻译过来就是说: 警告:必须将应用程序作为导入字符串传递,才能启用“重新加载” 然后呢: 我修改为:
1
2
|
uvicorn.run(app='app', host="127.0.0.1", port=8000, reload=True, debug=True) 复制代码 |
又提示:
1
2
|
ERROR: Error loading ASGI app. Import string "app" must be in format "< module >:< attribute >". 复制代码 |
好吧,我再看看官方文档说是:
在命令行下是需要:模块加app名称:刚好上面的错误提示也是说需要:
好吧,了然:
1
2
|
uvicorn.run(app='main:app', host="127.0.0.1", port=8000, reload=True, debug=True) 复制代码 |
这样之后就可以启动热更新重启服务了!
然后访问地址,正常的获取到接口返回的消息体了:
然后按官网文档查阅API文档交互地址:
路由方法有 GET, POST, PUT, PATCH, DELETE 和 OPTIONS。
1
2
3
4
5
6
7
8
9
10
11
|
@app.post("/") @app.put("/") @app.delete("/") @app.get("/") @app.options("/") @app.head("/") @app.patch("/") @app.trace("/") async def root(): return {"message": "Hello 454533333343433World"} 复制代码 |
3:路由Route上参数获取和校验
一般我们的路由分会静态和动态,静态路由就是参数是固定写死,也就是访问地址是写死的,而动态地址,就是需要动态的生成,类似简书的博文的地址94710ed35b92就是动态,其实和Bottle和Flask一样。
1
2
3
4
5
6
7
8
9
|
from fastapi import FastAPI app = FastAPI() @app.get("/items/{item_id}") async def read_item(item_id): return {"item_id": item_id} 复制代码 |
上述的示例代码中的item_id 就是一个动态的参数,你可以随意传一个进来。
然后就是和bottle一样也可以对传入的参数进行数据验证的定义: 如:
1
2
3
4
5
6
7
8
9
10
|
from fastapi import FastAPI app = FastAPI() @app.get("/items/{item_id}") async def read_item(item_id: int): return {"item_id": item_id} 复制代码 |
item_id: int 这种情况item_id必须是可以转为int类似的数据,否则,肯定会报错!
关于路由覆盖问题: 如下两个路由地址:
1
2
3
4
5
6
7
8
9
10
11
|
from fastapi import FastAPI app = FastAPI() @app.get("/users/me") async def read_user_me(): return {"user_id": "the current user"} @app.get("/users/{user_id}") async def read_user(user_id: str): return {"被优先匹配到:": user_id} 复制代码 |
上面两个路由同时存在的话,则/users/{user_id} 会覆盖/users/me!
3.1 查询路径参数和参数校验
关于查询参数,其实就是在使用POSTMAN 提交的时候的参数信息: 如:
http://127.0.0.1:8000/items/?skip=0&limit=10
skip=0&limit就是所谓的查询参数。
1
2
3
4
5
6
7
8
9
10
11
|
from fastapi import FastAPI app = FastAPI() fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] @app.get("/items/") async def read_item(skip: int = 0, limit: int = 10): return fake_items_db[skip : skip + limit] 复制代码 |
第一种访问的情况:
第二种访问情况:
第三种访问情况:
3.2 多路径和查询参数
所谓的多路径和查询参数就是URL上包含了有动态的参数,还有需要通过&分隔符提交的参数,这情况,通常再GET提交的中也很常见,那么如何处理呐?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
from fastapi import FastAPI app = FastAPI() @app.get("/users/{user_id}/items/{item_id}") async def read_user_item( user_id: int, item_id: str, q: str = None, short: bool = False ): item = {"item_id": item_id, "owner_id": user_id} if q: item.update({"q": q}) if not short: item.update( {"description": "This is an amazing item that has a long description"} ) return item 复制代码 |
请求:
http://127.0.0.1:8000/users/123456/items/items_xinxiid/?q=assa&short=True
请求:
http://127.0.0.1:8000/users/123456/items/items_xinxiid/?q=assa&short=False
3.3 路径参数和查询参数的必选和可选
参数的可选和必选主要是通过是否给默认值来决定的:
如:
1
2
3
4
5
|
@app.get("/items/{item_id}") async def read_user_item(item_id: str, needy: str): item = {"item_id": item_id, "needy": needy} return item 复制代码 |
上述的代码中 needy 没有给与默认的值,当个没提交这个值的时候,会提示错误:
还可以定义可选参数和必选的参数的提交类型: 其中还可以使用Optional来定义需要提交的数据类型: 如:
1
2
3
4
5
6
7
|
from typing import Optional @app.get("/items/{item_id}") async def read_user_item(item_id: str, limit: Optional[int] = None): item = {"item_id": item_id, "limit": limit} return item 复制代码 |
我们把查询参数limit规定为了int类型,但是它是可选的的参数,设置为了None:
3.4 路径参数的枚举
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
|
import uvicorn from fastapi import FastAPI from enum import Enum class ModelName(str, Enum): alexnet = "alexnet" resnet = "resnet" lenet = "lenet" app = FastAPI() @app.get("/model/{model_name}") 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"} if __name__ == '__main__': uvicorn.run(app='main2:app', host="127.0.0.1", port=8000, reload=True, debug=True) 复制代码 |
通过访问地址:
3.5 查询参数Query参数的其他校验
在以前通常是使用wtform来定义提交的字段信息的类似或可选或长度类型。在Fastapi里面,我们是通过: from fastapi import FastAPI, Query 中的Query来定义,如:
1
2
3
4
5
6
7
8
9
|
from fastapi import FastAPI, Query app = FastAPI() @app.get("/items/") async def read_items(q: str = Query(None, min_length=3,max_length=50),regex="^fixedquery$"): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) return results 复制代码 |
q: q: str = Query(None, min_length=3,max_length=50),regex="^fixedquery$") 意思是:q参数是可选的参数,但是如果填写的话,最大长度必须是小于50内,且最小的长度必须大于3: 且需要符合regex的匹配
当然None可以修改为其他默认值,可以写如:
q: q: str = Query('xiaozhong', min_length=3,max_length=50),regex="^fixedquery$")
不传q的情况下:
传q的情况下且长度大于50:
传q的情况下且长度小于3:
查询参数Query的参数正则校验
3.6 查询参数Query参数多值列表
一般在我们的接口中很少说同一个参数提交多个值如:
但也不排查这种情况的存在,所以也可以定义我们的参数类似必须是列表的形式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
from typing import List from fastapi import FastAPI, Query app = FastAPI() @app.get("/items/") async def read_items(q: List[str] = Query(["foo", "bar"])): # <!--也可以使用list直接代替List[str]:--> query_items = {"q": q} return query_items 复制代码 |
默认值:
非默认值:
3.7 路径参数的其他校验方式
对于查询参数可以通过Query,同样对于路径参数也可以使用Fastapi自带的Path来进行校验。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
from fastapi import FastAPI, Path app = FastAPI() @app.get("/items/{item_id}") async def read_items( q: str, item_id: int = Path(..., title="The ID of the item to get") ): results = {"item_id": item_id} if q: results.update({"q": q}) return results 复制代码 |
对于路径参数校验中,还可以对item_id进行大于或等于的校验如:
1
2
3
4
5
6
7
8
9
10
11
12
|
from fastapi import FastAPI, Path app = FastAPI() @app.get("/items/{item_id}") async def read_items( *, item_id: int = Path(..., title="The ID of the item to get", ge=1), q: str ): results = {"item_id": item_id} if q: results.update({"q": q}) return results 复制代码 |
在上面代码意思是,当ge = 1时,item_id必须是整数“ g大于或等于e等于1”。
3.8 参数提交的Request Body
一般对于Request Body不会通过get提交,对于get提交的参数一般称为是查询参数。所以,如果是通过POTS,PUT等方式提交的参数信息,我们一般是放到Request Body来提交到我们的后端。
对于如何接收和校验请求体,FastApi提供的形式是使用:from pydantic import BaseModel
示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
from fastapi import FastAPI from pydantic import BaseModel class Item(BaseModel): name: str description: str = None price: float tax: float = None app = FastAPI() @app.post("/items/") async def create_item(item: Item): return item 复制代码 |
在上面的模型中我,定义如果提交的Item它必须是怎么样的一个格式,比如name是必选字段,description是可选且默认为None, price是必选,且需要是float类型的,tax是可须且默认为None。
那客户端如何提交上面那些参数呐?
尝试提交参数什么都不写的情况下:
使用JSON格式提交参数的情况下:
故意提交错误参数格式请求:
3.8 Request Body 和 Query 和 Path的混合
在设计一些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
|
from fastapi import FastAPI, Path from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str description: str = None price: float tax: float = None @app.put("/items/{item_id}") async def update_item( *, item_id: int = Path(..., title="The ID of the item to get", ge=0, le=1000), q: str = None, item: Item = None, ): results = {"item_id": item_id} if q: results.update({"q": q}) if item: results.update({"item": item}) return results 复制代码 |
通过之前的学习,其实也很简单道理也还是一样,如上的示例请求的话:
3.9 多个Request Body的提交
更复杂的业务其实会存在多体的Boay的提交,之前做的商城下单里面,客户端有可能就会同时提交多个实体的对象信息到后端,如订单实体,地址实体,商品信息实体等。
那么在Fastapi如何接受多个Body实体呐?通常以前的话,在bottle,通常直接的request.body 或 request.json就可以获取客户端部提交的信息了。
在Fastapi假设客户端提交的参数是这样的形式:
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": "dave", "full_name": "Dave Grohl" } } 复制代码 |
那如何的接收处理呐?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str description: str = None price: float tax: float = None class User(BaseModel): username: str full_name: 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 复制代码 |
这种情况,其实就是客户端提交多个实体对象。那可以定义多个模型对象即可。fastapi它会自动帮你处理提取信息。
如果另外再假设:
在Fastapi假设客户端提交的参数是这样的形式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
{ "item": { "name": "Foo", "description": "The pretender", "price": 42.0, "tax": 3.2 }, "user": { "username": "dave", "full_name": "Dave Grohl" }, "importance": 5 } 复制代码 |
其实这种可能也不是不存在滴,那如何的读取解析importance参数呐?既然参数有Query 和 Path,当然也会有 Body 。
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 Body, FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str description: str = None price: float tax: float = None class User(BaseModel): username: str full_name: str = None @app.put("/items/{item_id}") async def update_item( *, item_id: int, item: Item, user: User, importance: int = Body(...,gt=0) ): results = {"item_id": item_id, "item": item, "user": user, "importance": importance} return results 复制代码 |
上面的代码中我们引入了Body 并且在importance: int = Body(...)进行处理和提取:
如果另外再假设,客户端提交的是一个单体对象内嵌的话,我们需要怎么处理?:
1
2
3
4
5
6
7
8
9
|
{ "item": { "name": "Foo", "description": "The pretender", "price": 42.0, "tax": 3.2 } } 复制代码 |
FastAPI提供了一个:
item: Item = Body(..., embed=True) 具体如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
from fastapi import Body, FastAPI from pydantic import BaseModel app = FastAPI() class Item(BaseModel): name: str description: str = None price: float tax: float = None @app.put("/items/{item_id}") async def update_item(*, item_id: int, item: Item = Body(..., embed=True)): results = {"item_id": item_id, "item": item} return results 复制代码 |
请求示例如:
如果另外再假设,客户端提交一个更复杂的嵌套模型的话,怎么办?麻蛋的 肯定也是会有这样的情况滴! 嵌套里面有列表有实体。
如:
1
2
3
4
5
6
7
8
9
10
11
12
|
{ "name": "Foo", "description": "The pretender", "price": 42.0, "tax": 3.2, "tags": ["rock", "metal", "bar"], "image": { "url": "http://example.com/baz.jpg", "name": "The Foo live" } } 复制代码 |
这时候,我们就需要所谓的子内嵌啦:
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 Set from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Image(BaseModel): url: str name: str class Item(BaseModel): name: str description: str = None price: float tax: float = None tags: Set[str] = [] image: 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 复制代码 |
如上代码,Item里面包含了Image,也包含了,tags类型的列表定义。
MMP更深层的嵌套也是可以定义的如:
{ "name":"Foo", "description":"The pretender", "price":42, "items":[ { "name":"Foo", "description":"The pretender", "price":42, "tax":3.2, "tags":[ "rock", "metal", "bar" ], "image":{ "url":"http://example.com/baz.jpg", "name":"The Foo live" } }, { "name":"Foo2", "description":"The 2", "price":422, "tax":3.2, "tags":[ "rock", "metal", "bar" ], "image":{ "url":"http://example.com/baz.jpg", "name":"The Foo live" } } ] }
对应的解析为:
from typing import Set from typing import List, Set class Image(BaseModel): url: str name: str class Item(BaseModel): name: str description: str = None price: float tax: float = None tags: Set[str] = [] # images: List[Image] = None image: Image = None class Offer(BaseModel): name: str description: str = None price: float items: List[Item] @app.post("/offers/") async def create_offer(*, offer: Offer): return offer
3.10 Request Body的Field
Field字段的意思其实就是类似上面Query, Path,也同样给Body内的字段的信息添加相关的校验。
也就是说。通过Field来规范提交的Body参数信息。
如:
from fastapi import Body, FastAPI from pydantic import BaseModel, Field app = FastAPI() class Item(BaseModel): name: str description: str = Field(None, title="标题啊",description="错误提示文字啊", max_length=300) price: float = Field(..., gt=0, description="错误提示文字啊") tax: float = None @app.put("/items/{item_id}") async def update_item(*, item_id: int, item: Item = Body(..., embed=True)): results = {"item_id": item_id, "item": item} return results
上面的意思就是和之前定义参数校验其实一样
正常情况:
异常情况:
3.11 其他数据类型的校验
对于数据格式的校验,通常,我们不止于
-
int
-
float
-
str
-
bool
但是提交参数不止于上述的几种格式,有时候比如是对手机号码的校验,有些时候是时间类型的校验等
其他类型:
其他数据类型¶ 以下是您可以使用的一些其他数据类型(来自官方文档):
-
UUID:
-
一个标准的“通用唯一标识符”,在许多数据库和系统中常见于ID。
-
在请求和答复中,将表示为str.
-
datetime.datetime:
-
一只Pythondatetime.datetime.
-
在请求和答复中,将表示为str采用ISO 8601格式,如:2008-09-15T15:53:00+05:00.
-
datetime.date:
-
Pythondatetime.date.
-
在请求和答复中,将表示为str采用ISO 8601格式,如:2008-09-15.
-
datetime.time:
-
一只Pythondatetime.time.
-
在请求和答复中,将表示为str采用ISO 8601格式,如:14:23:55.003.
-
datetime.timedelta:
-
一只Pythondatetime.timedelta.
-
在请求和答复中,将表示为float总秒数。
-
Pydantic还允许将其表示为“ISO 8601时间差异编码”,有关更多信息,请参阅文档。.
-
frozenset:
-
在请求和答复中,将其视为set:
-
在请求中,将读取列表,消除重复,并将其转换为set.
-
在答复中,set将转换为list.
-
生成的架构将指定set值是唯一的(使用JSONSchema的uniqueItems).
-
bytes:
-
标准Pythonbytes.
-
在请求和答复中将被视为str.
-
生成的架构将指定它是str带着binary“格式”。
-
Decimal:
-
标准PythonDecimal.
-
在请求和响应中,处理方式与float.
所以我还可以使用其他类型来校验:
from datetime import datetime, time, timedelta 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: datetime = Body(None), end_datetime: datetime = Body(None), repeat_at: time = Body(None), process_after: 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, }
4:响应报文
4.1 使用response_model定义
请求一个接口返回来我们客户端可见的东西都是所谓的响应报文,如响应头,响应码,响应内容等。
通常不会那么傻的用户输入什么就返回什么。以下的官网示例纯粹的演示看:
from fastapi import FastAPI from pydantic import BaseModel, EmailStr app = FastAPI() class UserIn(BaseModel): username: str password: str email: str full_name: str = None class UserOut(BaseModel): username: str email: str full_name: str = None @app.post("/user/", response_model=UserOut) async def create_user(*, user: UserIn): return user
请求之后,获取到是UserOut的内容信息:
通常再定义我们的API返回响应的时候,一般是返回固定JSON格式的,所以可以直接使用定义response_model为一个字典:
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}
4.2 关于响应状态码status_code
通常的一个接口请求完成,如果没有什么异常通常会返回200: 如日志打印出来一样:
1
2
3
|
INFO: 127.0.0.1:58141 - "POST /user/ HTTP/1.1" 400 INFO: 127.0.0.1:58315 - "POST /user/ HTTP/1.1" 200 复制代码 |
FastAPI运行我们的指定返回的status_code
如下示例:
@app.post("/user/", response_model=UserOut,status_code=500) async def create_user(*, user: UserIn): return user
导致请求的接口返回:
甚至还可以通过导入status来指定:
from fastapi import FastAPI, status app = FastAPI() @app.post("/items/", status_code=status.HTTP_201_CREATED) async def create_item(name: str): return {"name": name}
5:错误处理
5.1 HTTPException异常抛出
再之前Bottle 中其实有一个就是HttpError异常类,在FastAPI也存在这么一个HTTPException。
如示例:
from fastapi import FastAPI, HTTPException app = FastAPI() items = {"foo": "The Foo Wrestlers"} @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]}
在上面的代码中,通过判断item_id是不是存在于items来主动的抛出了一个404的错误
我们查看HTTPException和StarletteHTTPException的源码发现他们也是继承与Exception:
class HTTPException(StarletteHTTPException): def __init__( self, status_code: int, detail: Any = None, headers: dict = None ) -> None: super().__init__(status_code=status_code, detail=detail) self.headers = headers
所以我们对于异常通常可以直接的使用 raise来抛出异常。
5.2 HTTPException且返回新增自定义请求头
from fastapi import FastAPI, HTTPException app = FastAPI() items = {"foo": "The Foo Wrestlers"} @app.get("/items-header/{item_id}") async def read_item_header(item_id: str): if item_id not in items: raise HTTPException( status_code=404, detail="Item not found", headers={"X-Error": "There goes my error"}, ) return {"item": items[item_id]}
5.3 自定义返回HTTPException
类似之前Bottle我们通过添加一个自定义的全局的错误,来统一的处理返回。FastAPI其实也提供一个自定义错误的机制:
官方示例如下:
from fastapi import FastAPI, Request from fastapi.responses import JSONResponse class UnicornException(Exception): def __init__(self, name: str): self.name = name app = FastAPI() @app.exception_handler(UnicornException) async def unicorn_exception_handler(request: Request, exc: UnicornException): return JSONResponse( status_code=418, content={"message": f"Oops! {exc.name} did something. There goes a rainbow..."}, ) @app.get("/unicorns/{name}") async def read_unicorn(name: str): if name == "yolo": raise UnicornException(name=name) return {"unicorn_name": name}
观察请求结果:
当请求name == yolo的时候,我们主动抛出了UnicornException,而且我们,@app.exception_handler(UnicornException)也捕获到相关的异常信息,且返回了相关的信息。
5.4 覆盖FastAPI默认的异常处理
按官方文档说明就是,当请求包含无效的数据的时候,或参数提交异常错误的时候,会抛出RequestValidationError,
那其实我也可以通过上面的自定义异常的方式来覆盖重写我们的RequestValidationError所返回信息:
如: 默认代码没有添加覆盖处理的话: 发生异常的时候是提示是:
from fastapi import FastAPI, HTTPException from fastapi.exceptions import RequestValidationError from fastapi.responses import PlainTextResponse from starlette.exceptions import HTTPException as StarletteHTTPException app = FastAPI() @app.exception_handler(StarletteHTTPException) async def http_exception_handler(request, exc): return PlainTextResponse(str(exc.detail), status_code=exc.status_code) # @app.exception_handler(RequestValidationError) # async def validation_exception_handler(request, exc): # return JSONResponse({'mes':'触发了RequestValidationError错误,,错误信息:%s 你妹的错了!'%(str(exc))}) @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(app='main4:app', host="127.0.0.1", port=8000, reload=True, debug=True)
发生异常的请求下返回:
恢复覆盖的时候:
from fastapi import FastAPI, HTTPException from fastapi.exceptions import RequestValidationError from fastapi.responses import PlainTextResponse from starlette.exceptions import HTTPException as StarletteHTTPException app = FastAPI() @app.exception_handler(StarletteHTTPException) async def http_exception_handler(request, exc): return PlainTextResponse(str(exc.detail), status_code=exc.status_code) @app.exception_handler(RequestValidationError) async def validation_exception_handler(request, exc): return JSONResponse({'mes':'触发了RequestValidationError错误,,错误信息:%s 你妹的错了!'%(str(exc))}) @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(app='main4:app', host="127.0.0.1", port=8000, reload=True, debug=True)
请求结果:
上面的返回其实我们还可以修改一下返回如下,指定响应码:
@app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError): return JSONResponse( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}), )
说明:
# 注意fastapi包中的HTTPException才可以定义请求头 from fastapi import Depends, status, HTTPException # from starlette.exceptions import HTTPException
6:FastAPI 中间件
所谓的中间件,其实和我们bottle中的中间件作用是一致。有些方法或操作需要在所有路由之前执行,比如要加一个http访问的拦截器,可以对部分接口API需要授权才能访问的接口进行验证之类的。
FastAPI提供了一个@app.middleware("http")可以做到类似上面的拦截功能。其实和bottle或flask 钩子函数很相似
示例如下:
from fastapi import FastAPI, Request from fastapi.responses import JSONResponse import time from fastapi import FastAPI, HTTPException from fastapi.exceptions import RequestValidationError from fastapi.responses import PlainTextResponse from starlette.exceptions import HTTPException as StarletteHTTPException app = FastAPI() @app.exception_handler(StarletteHTTPException) async def http_exception_handler(request, exc): return PlainTextResponse(str(exc.detail), status_code=exc.status_code) @app.exception_handler(RequestValidationError) async def validation_exception_handler(request, exc): return JSONResponse({'mes':'触发了RequestValidationError错误,,错误信息:%s 你妹的错了!'%(str(exc))}) @app.get("/items/{item_id}") async def read_item(item_id: int): return {"item_id": item_id} @app.middleware("http") async def add_process_time_header(request: Request, call_next): start_time = time.time() response = await call_next(request) process_time = time.time() - start_time response.headers["X-Process-Time"] = str(process_time) return response if __name__ == '__main__': import uvicorn uvicorn.run(app='main4:app', host="127.0.0.1", port=8000, reload=True, debug=True)
然后我们请求完成后发现,我们的响应头里多了一个新增的请求头:
7:FastAPI 跨域处理
为啥需要跨域处理,通常我们的API一般是给到前端去调用,但是前端可能使用域名和没提供的API域名是不一样,这就引发了浏览器同源策略问题,所以我们需要做跨域请求支持。
FastAPI支持跨域的话,可以通过添加中间的形式,和bottle也有相似之处。不仅如此他还支持仅限于支持哪些域名进行跨域请求:
from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware app = FastAPI() origins = [ "http://localhost.tiangolo.com", "https://localhost.tiangolo.com", "http://localhost", "http://localhost:8080", ] app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.get("/") async def main(): return {"message": "Hello World"}
懒得起一个js访问了,所以这个暂时不测试了,后期有机会再测试验证一下,感觉应该就是这样的。
8:FastAPI 依赖注入之Depends(补充)
看官网的描述Depends的使用,似乎比较懵懵懂懂的,于是乎还是需要花些时间再次学习一下关于依赖注入。
首先依赖注入它可以是函数也可以是类,如下面的函数形式的依赖注入:
8.1 简单的依赖说明
from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware app = FastAPI() origins = [ "http://localhost.tiangolo.com", "https://localhost.tiangolo.com", "http://localhost", "http://localhost:8080", ] app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.get("/") async def main(): return {"message": "Hello World"}
梳理一下接口请求的流程:
-
1: 上面的commons: dict = Depends(common_parameters)它声明了一个依赖关系: Depends(common_parameters): 这对接口的依赖进行了一个声明,表示的是接口参数请求依赖于common_parameters的函数。
当接口被调用的时候,回调用common_parameters函数进行请求处理。
-
2: common_parameters函数主要是负责接收函数,处理后返回一个字典,
-
3:然后把Depends(common_parameters)返回的结果 传递给commons: dict,这个就是一个依赖注入的过程。
所以在上面的示例中common_parameters是我们被依赖对象
这个被依赖的对象,对接口请求的要求就是:
-
可选查询参数q那是一个str.
-
可选查询参数skip那是int,默认情况下是0.
-
可选查询参数limit那是int,默认情况下是100.
-
返回一个字典
请求示例:
这依赖注入的方式其实也挺方便,类似于接口装饰器的方式,比如common_parameters中我们可以先对相关的参数进行校验拦截,还可以再传递。 场景可以和我们之前的bottle的装饰器差不多类似:
-
相同的逻辑判断处理
-
用户身份鉴权
8.2 把类当作被依赖对象
上面我们的被依赖的对象是以函数的形式出现,那FastAPI它也支持以类的形式来表达。按官网说法被依赖对象它应该是必须一个可以调用的对象比如:类,函数之类的···
这里看一下以类的形式:
from fastapi import Depends, FastAPI app = FastAPI() fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] class CommonQueryParams: def __init__(self, q: str = None, skip: int = 0, limit: int = 100): self.q = q self.skip = skip self.limit = limit @app.get("/items/") async def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)): response = {} # 如果q存在 if commons.q: # 我们就把q加到一个新字典 response.update({"q": commons.q}) response.update({"小钟": '同学'}) #然后在我们的fake_items_db进行截取 items = fake_items_db[commons.skip : commons.skip + commons.limit] response.update({"items": items}) return response
上面我们的CommonQueryParams是一个类,它和我的函数其实差不多,当我们的接口被调用的时候,类对象就回被初始化, 按官网的说法: commons: CommonQueryParams = Depends(CommonQueryParams) 和 commons = Depends(CommonQueryParams) 是等价的。 还有一种是: commons: CommonQueryParams = Depends()
示例运行演示
有Q参数:
没有Q参数:
8.3 多层嵌套依赖
多层嵌套的意思就是可以类可以类的意思。函数可以依赖函数。其实和我们的之前的参数校验一样。
比如下面的代码:
from fastapi import Cookie, Depends, FastAPI app = FastAPI() def query_extractor(q: str = None): return q def query_or_cookie_extractor( q: str = Depends(query_extractor), last_query: str = Cookie(None) ): if not q: return last_query return q @app.get("/items/") async def read_query(query_or_default: str = Depends(query_or_cookie_extractor)): return {"q_or_cookie": query_or_default}
其实意思就是query_or_cookie_extractor 依赖于query_extractor,然后query_or_cookie_extractor被注入到接口上也被依赖的对象。
官网的截图上面接口执行流程如下:
对于同一个依赖,如果处理的结果是一样的,就是返回值是一样的话,我们可以进行多次调用依赖,这时候可以对被依赖的对象设置是否使用缓存机制:
async def needy_dependency(fresh_value: str = Depends(get_value, use_cache=False)): return {"fresh_value": fresh_value}
8.4 list列表依赖
我们先看官方提供的示例代码:
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"}]
上述的代码意思是对我们的请求头部信息Header进行验证,因为示例是...三个点,说明是必选的字段:
分析上述的代码之后,运行一下试一试看看结果:
1:什么头部参数都不传递的情况提示,我们的头部参数异常
2:头部参数填写的情况:
注意点:参数提交的格式,因为是头部的参数,所以我们的代码上的x_token 会应该要写:x-token才对
错误的示例:
所以上面列表的依赖的意思就是必须两天条件都成立才通过。这个感觉后期还是用到的比较多的哟!
8.5 多依赖对象注入和列表其实是一样的:
from fastapi import Depends, FastAPI from fastapi import Depends, FastAPI, Header, HTTPException from fastapi import Depends, FastAPI 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") return x_token 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"}] @app.get("/items2/") async def items2(xt: str = Depends(verify_token),xk: str = Depends(verify_key)): return {"xt": xt,'xk':xk} if __name__ == '__main__': import uvicorn uvicorn.run(app='main:app', host="127.0.0.1", port=8100, reload=True, debug=True)
如上面的xt: str = Depends(verify_token),xk: str = Depends(verify_key),也是需要必须两个条件成立才可以。
正常情况:
非正常情况:
总结
文章总体是跟着官方文档的思路走,简单梳理了一下,后续我们的API用到一些知识点。下一步尝试基于上述知识点弄一个简单的脚手架看看。