??从零开始学Fastapi(1)-简明的部分官方文档抽取学习篇

前言

之前我们线上业务一直是使用Bottle和Flask两个框架来编写我们的API接口。我们都知道Bottle和Flask它们都是一些同步的框架,而支持异步框架的最新晋的一个非常不错的角:之前在青南大大的文章里也了解到了它的奇异之处,处于对异步框架的学习兴趣,我还是决定也开始使用Fastapi试一试,虽然此前也用过Tornado,但是也仅限于使用同步的框架。

本来想上手试一试看看sanic,处于猎奇心,还是鼓捣以下这个Fastapi!

学一个新的框架最好的方式当然就是框架笨的提供的官方文档了!

文献资料: https://fastapi.tiangolo.com

源代码: https://github.com/tiangolo/fastapi

参考资料:https://www.jianshu.com/p/94710ed35b92

开始撸码

注意事项,因为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
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
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
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
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
WARNING:  You must pass the application as an import string to enable 'reload' or 'workers'.

翻译过来就是说:
警告:必须将应用程序作为导入字符串传递,才能启用“重新加载”
然后呢:
我修改为:

1
  uvicorn.run(app='app', host="127.0.0.1", port=8000, reload=True, debug=True)

又提示:

1
ERROR:    Error loading ASGI app. Import string "app" must be in format "<module>:<attribute>".

好吧,我再看看官方文档说是:

在命令行下是需要:模块加app名称:刚好上面的错误提示也是说需要:

好吧,了然:

1
    uvicorn.run(app='main:app', host="127.0.0.1", port=8000, reload=True, debug=True)

这样之后就可以启动热更新重启服务了!

然后我们的访问我们的地址,正常的获取到我们接口返回的消息体了:

然后我们按官网文档查阅我们的API文档交互地址:

http://127.0.0.1:8000/docs

http://127.0.0.1:8000/redoc

路由方法有 GET, POST, PUT, PATCH, DELETE 和 OPTIONS。

1
2
3
4
5
6
7
8
9
10
@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一样。

https://www.jianshu.com/p/94710ed35b92

1
2
3
4
5
6
7
8
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
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
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
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
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
@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
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
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)

我们通过访问地址:

http://127.0.0.1:8000/model/alexnet

3.5 查询参数Query参数的其他校验

在以前我们通常是使用wtform来定义我们的提交的字段信息的类似或可选或长度类型。在Fastapi里面,我们是通过:
from fastapi import FastAPI, Query 中的Query来定义,如:

1
2
3
4
5
6
7
8
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的情况下:

http://127.0.0.1:8000/items/

传q的情况下且长度大于50:

http://127.0.0.1:8000/items/

传q的情况下且长度小于3:

http://127.0.0.1:8000/items/?q=4

查询参数Query的参数正则校验

3.6 查询参数Query参数多值列表

一般在我们的接口中很少说同一个参数提交多个值如:

http://localhost:8000/items/?q=foo&q=bar

但也不排查这种情况的存在,所以我们的也可以定义我们的参数类似必须是列表的形式:

1
2
3
4
5
6
7
8
9
10
11
12
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
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
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
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
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
{
    "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
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
{
    "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
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
{
    "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
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
{
    "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
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更深层的嵌套也是可以定义的如:

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
{
    "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"
            }
        }
    ]
}

对应的解析为:

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 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参数信息。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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.

所以我还可以使用其他类型来校验:

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 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定义

请求一个接口返回来我们客户端可见的东西都是所谓的响应报文,如响应头,响应码,响应内容等。

通常我们的不会那么傻的用户输入什么就返回什么。以下的官网示例纯粹的演示看:

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, 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为一个字典:

1
2
3
4
5
6
7
8
9
10
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
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

如下示例:

1
2
3
@app.post("/user/", response_model=UserOut,status_code=500)
async def create_user(*, user: UserIn):
    return user

导致我们的请求的接口返回:

甚至我们还可以通过导入status来指定:

1
2
3
4
5
6
7
8
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。

如示例:

1
2
3
4
5
6
7
8
9
10
11
12
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:

1
2
3
4
5
6
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且返回新增自定义请求头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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其实也提供一个自定义错误的机制:

官方示例如下:

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, 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所返回信息:

如:
默认代码没有添加覆盖处理的话:
我们的发生异常的时候是提示是:

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
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)

发生异常的请求下返回:

恢复覆盖的时候:

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
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)

请求结果:

上面的返回其实我们还可以修改一下返回如下,指定响应码:

1
2
3
4
5
6
@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}),
    )

说明:

1
2
3
# 注意fastapi包中的HTTPException才可以定义请求头
from fastapi import Depends, status, HTTPException
# from starlette.exceptions import HTTPException

6:FastAPI 中间件

所谓的中间件,其实和我们bottle中的中间件作用是一致。有些方法或操作需要在所有路由之前执行,比如要加一个http访问的拦截器,可以对部分接口API需要授权才能访问的接口进行验证之类的。

FastAPI提供了我们的一个@app.middleware("http")可以做到类似上面的拦截功能。其实和我们的bottle或flask 钩子函数很相似

示例如下:

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
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也有相似之处。不仅如此他还支持仅限于支持哪些域名进行跨域请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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访问了,所以这个暂时不测试了,后期有机会再测试验证一下,感觉应该就是这样的。

总结

文章总体是跟着我们的官方文档的思路走,简单梳理了一下,后续我们的API用到一些知识点。下一步我们尝试基于上述知识点弄一个简单的脚手架看看。

END

小钟同学 | 文 【原创】【转载请联系本人】| QQ:308711822